Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions Usenet/Nntp/CustomCertValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace Usenet.Nntp
{
public static class CustomCertValidator
{
private static readonly HashSet<string> IgnoredHosts =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);

private static bool _ignoreAllNameMismatch = false;

// --------------------------------------------------------
// Static constructor: reads environment variables once
// --------------------------------------------------------
static CustomCertValidator()
{
// Global ignore flag
var ignoreAll = Environment.GetEnvironmentVariable("NZBDAV_NNTP_TLS_IGNORE_NAME_MISMATCH");
if (!string.IsNullOrEmpty(ignoreAll) &&
ignoreAll.Equals("true", StringComparison.OrdinalIgnoreCase))
{
EnableGlobalIgnore();
}

// Per-host ignore list
var hosts = Environment.GetEnvironmentVariable("NZBDAV_NNTP_TLS_IGNORE_HOSTS");
if (!string.IsNullOrWhiteSpace(hosts))
{
foreach (var host in hosts.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries))
{
AddIgnoredHost(host.Trim());
}
}
}

/// <summary>
/// Globally ignore RemoteCertificateNameMismatch for all hosts.
/// </summary>
public static void EnableGlobalIgnore()
{
_ignoreAllNameMismatch = true;
}

/// <summary>
/// Ignore certificate name mismatch for a specific hostname.
/// </summary>
public static void AddIgnoredHost(string host)
{
if (!string.IsNullOrWhiteSpace(host))
IgnoredHosts.Add(host);
}

/// <summary>
/// Validate the server certificate, applying global and per-host overrides.
/// </summary>
public static bool Validate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslErrors,
string targetHost)
{
// Allow all mismatched certs globally
if (_ignoreAllNameMismatch &&
sslErrors == SslPolicyErrors.RemoteCertificateNameMismatch)
return true;

// Allow mismatched certs for specific hosts
if (IgnoredHosts.Contains(targetHost) &&
sslErrors == SslPolicyErrors.RemoteCertificateNameMismatch)
return true;

// Allow only completely clean certificates otherwise
return sslErrors == SslPolicyErrors.None;
}
}
}
39 changes: 24 additions & 15 deletions Usenet/Nntp/NntpConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class NntpConnection : INntpConnection
private readonly TcpClient client = new TcpClient();
private StreamWriter writer;
private NntpStreamReader reader;
private string _targetHost;

/// <inheritdoc/>
public CountingStream Stream { get; private set; }
Expand All @@ -33,23 +34,29 @@ public async Task<TResponse> ConnectAsync<TResponse>(string hostname, int port,
{
log.LogInformation("Connecting: {hostname} {port} (Use SSl = {useSsl})", hostname, port, useSsl);
await client.ConnectAsync(hostname, port);

_targetHost = hostname;

Stream = await GetStreamAsync(hostname, useSsl);

writer = new StreamWriter(Stream, UsenetEncoding.Default) { AutoFlush = true };
reader = new NntpStreamReader(Stream, UsenetEncoding.Default);

return GetResponse(parser);
}

/// <inheritdoc/>
public TResponse Command<TResponse>(string command, IResponseParser<TResponse> parser)
{
ThrowIfNotConnected();
log.LogInformation("Sending command: {Command}",command.StartsWith("AUTHINFO PASS", StringComparison.Ordinal) ? "AUTHINFO PASS [omitted]" : command);
log.LogInformation("Sending command: {Command}",
command.StartsWith("AUTHINFO PASS", StringComparison.Ordinal) ? "AUTHINFO PASS [omitted]" : command);
writer.WriteLine(command);
return GetResponse(parser);
}

/// <inheritdoc/>
public TResponse MultiLineCommand<TResponse>(string command, IMultiLineResponseParser<TResponse> parser) //, bool decompress = false)
public TResponse MultiLineCommand<TResponse>(string command, IMultiLineResponseParser<TResponse> parser)
{
NntpResponse response = Command(command, new ResponseParser());

Expand All @@ -67,13 +74,11 @@ public TResponse GetResponse<TResponse>(IResponseParser<TResponse> parser)
log.LogInformation("Response received: {Response}", responseText);

if (responseText == null)
{
throw new NntpException("Received no response.");
}

if (responseText.Length < 3 || !int.TryParse(responseText.Substring(0, 3), out int code))
{
throw new NntpException("Received invalid response.");
}

return parser.Parse(code, responseText.Substring(3).Trim());
}

Expand All @@ -87,30 +92,34 @@ public void WriteLine(string line)
private void ThrowIfNotConnected()
{
if (!client.Connected)
{
throw new NntpException("Client not connected.");
}
}

private async Task<CountingStream> GetStreamAsync(string hostname, bool useSsl)
{
NetworkStream stream = client.GetStream();
NetworkStream networkStream = client.GetStream();

if (!useSsl)
{
return new CountingStream(stream);
}
var sslStream = new SslStream(stream);
return new CountingStream(networkStream);

// SSL with custom certificate validation
var sslStream = new SslStream(
networkStream,
false,
(sender, cert, chain, errors) =>
CustomCertValidator.Validate(sender, cert, chain, errors, _targetHost)
);

await sslStream.AuthenticateAsClientAsync(hostname);

return new CountingStream(sslStream);
}

private IEnumerable<string> ReadMultiLineDataBlock()
{
string line;
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}

/// <inheritdoc/>
Expand Down
11 changes: 4 additions & 7 deletions Usenet/Usenet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,22 @@
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>Usenet</RootNamespace>
<AssemblyName>Usenet</AssemblyName>
<Authors>Harmen van Keimpema</Authors>
<Authors>Harmen van Keimpema, pannal</Authors>
<Company />
<Description>A library for working with Usenet. It offers an NNTP client, an NZB file parser, builder, writer, a yEnc encoder and decoder. It is mainly focused on keeping memory usage low. Server responses can be enumerated as they come in. Binary messages will be encoded to yEnc format streaming and yEnc-encoded data will be decoded to binary data streaming.</Description>
<PackageProjectUrl>https://github.com/keimpema/Usenet</PackageProjectUrl>
<RepositoryUrl>https://github.com/keimpema/Usenet</RepositoryUrl>
<RepositoryUrl>https://github.com/pannal/Usenet</RepositoryUrl>
<RepositoryType />
<PackageTags>usenet;nntp;nzb;yenc</PackageTags>
<PackageId>Usenet</PackageId>
<PackageId>Usenet.Custom</PackageId>
<Product>Usenet</Product>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Version>3.1.1-nzbdav</Version>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" Version="5.6.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>

Expand Down