diff --git a/csharp/autogen/src/HTTP.cs b/csharp/autogen/src/HTTP.cs
index 1059a54..0e8d372 100644
--- a/csharp/autogen/src/HTTP.cs
+++ b/csharp/autogen/src/HTTP.cs
@@ -31,6 +31,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
@@ -133,6 +134,7 @@ protected ProxyServerAuthenticationException(SerializationInfo info, StreamingCo
public const int MAX_REDIRECTS = 10;
public const int DEFAULT_HTTPS_PORT = 443;
+ private const int NONCE_LENGTH = 16;
public enum ProxyAuthenticationMethod
{
@@ -234,9 +236,9 @@ private static bool ReadHttpHeaders(ref Stream stream, IWebProxy proxy, bool nod
lastChunkSize = chunkSize;
} while (lastChunkSize != 0);
- entityBody = entityBody.TrimEnd('\r', '\n');
- headers.Add(entityBody); // keep entityBody if it's needed for Digest authentication (when qop="auth-int")
- }
+ entityBody = entityBody.TrimEnd('\r', '\n');
+ headers.Add(entityBody); // keep entityBody if it's needed for Digest authentication (when qop="auth-int")
+ }
else
{
// todo: handle other transfer types, in case "Transfer-Encoding: Chunked" isn't used
@@ -251,16 +253,9 @@ private static bool ReadHttpHeaders(ref Stream stream, IWebProxy proxy, bool nod
break;
case 302:
- string url = "";
- foreach (string header in headers)
- {
- if (header.StartsWith("Location: "))
- {
- url = header.Substring(10);
- break;
- }
- }
- Uri redirect = new Uri(url.Trim());
+ var header = headers.FirstOrDefault(h => h.StartsWith("Location:", StringComparison.InvariantCultureIgnoreCase));
+ string url = header == null ? "" : header.Substring(9).Trim();
+ Uri redirect = new Uri(url);
stream.Close();
stream = ConnectStream(redirect, proxy, nodelay, timeout_ms);
return true; // headers need to be sent again
@@ -293,18 +288,58 @@ private static bool ValidateServerCertificate(
return true;
}
+ [Obsolete]
+ public static string MD5Hash(string str)
+ {
+ return _MD5Hash(str);
+ }
+
///
/// Returns a secure MD5 hash of the given input string.
///
/// The string to hash.
/// The secure hash as a hex string.
- public static string MD5Hash(string str)
+ private static string _MD5Hash(string str)
+ {
+ return ComputeHash(str, "MD5");
+ }
+
+ ///
+ /// Returns a secure SHA256 hash of the given input string.
+ ///
+ /// The string to hash.
+ /// The secure hash as a hex string.
+ private static string Sha256Hash(string str)
+ {
+ return ComputeHash(str, "SHA256");
+ }
+
+ private static string ComputeHash(string input, string method)
+ {
+ if (input == null)
+ return null;
+
+ var enc = new UTF8Encoding();
+ byte[] bytes = enc.GetBytes(input);
+
+ using (var hasher = HashAlgorithm.Create(method))
+ {
+ byte[] hash = hasher?.ComputeHash(bytes);
+ if (hash != null)
+ return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ }
+
+ return null;
+ }
+
+ private static string GenerateNonce()
{
- MD5 hasher = MD5.Create();
- ASCIIEncoding enc = new ASCIIEncoding();
- byte[] bytes = enc.GetBytes(str);
- byte[] hash = hasher.ComputeHash(bytes);
- return BitConverter.ToString(hash).Replace("-", "").ToLower();
+ using (var rngCsProvider = new RNGCryptoServiceProvider())
+ {
+ var nonceBytes = new byte[NONCE_LENGTH];
+ rngCsProvider.GetBytes(nonceBytes);
+ return Convert.ToBase64String(nonceBytes);
+ }
}
public static long CopyStream(Stream inStream, Stream outStream,
@@ -473,157 +508,169 @@ public static Stream ConnectStream(Uri uri, IWebProxy proxy, bool nodelay, int t
private static void AuthenticateProxy(ref Stream stream, Uri uri, IWebProxy proxy, bool nodelay, int timeoutMs, List initialResponse, string header)
{
// perform authentication only if proxy requires it
- List fields = initialResponse.FindAll(str => str.StartsWith("Proxy-Authenticate:"));
- if (fields.Count > 0)
+ List fields = initialResponse.FindAll(str => str.StartsWith("Proxy-Authenticate:", StringComparison.InvariantCultureIgnoreCase));
+ if (fields.Count <= 0)
+ return;
+
+ // clean up (if initial server response specifies "Proxy-Connection: Close" then stream cannot be re-used)
+ string field = initialResponse.Find(str => str.StartsWith("Proxy-Connection: Close", StringComparison.InvariantCultureIgnoreCase));
+ if (!string.IsNullOrEmpty(field))
{
- // clean up (if initial server response specifies "Proxy-Connection: Close" then stream cannot be re-used)
- string field = initialResponse.Find(str => str.StartsWith("Proxy-Connection: Close", StringComparison.CurrentCultureIgnoreCase));
- if (!string.IsNullOrEmpty(field))
- {
- stream.Close();
- Uri proxyURI = proxy.GetProxy(uri);
- stream = ConnectSocket(proxyURI, nodelay, timeoutMs);
- }
+ stream.Close();
+ Uri proxyURI = proxy.GetProxy(uri);
+ stream = ConnectSocket(proxyURI, nodelay, timeoutMs);
+ }
- if (proxy.Credentials == null)
- throw new BadServerResponseException(string.Format("Received error code {0} from the server", initialResponse[0]));
- NetworkCredential credentials = proxy.Credentials.GetCredential(uri, null);
+ if (proxy.Credentials == null)
+ throw new BadServerResponseException(string.Format("Received error code {0} from the server", initialResponse[0]));
- string basicField = fields.Find(str => str.StartsWith("Proxy-Authenticate: Basic"));
- string digestField = fields.Find(str => str.StartsWith("Proxy-Authenticate: Digest"));
- if (CurrentProxyAuthenticationMethod == ProxyAuthenticationMethod.Basic)
- {
- if (string.IsNullOrEmpty(basicField))
- throw new ProxyServerAuthenticationException("Basic authentication scheme is not supported/enabled by the proxy server.");
+ NetworkCredential credentials = proxy.Credentials.GetCredential(uri, null);
- string authenticationFieldReply = string.Format("Proxy-Authorization: Basic {0}",
- Convert.ToBase64String(Encoding.UTF8.GetBytes(credentials.UserName + ":" + credentials.Password)));
- WriteLine(header, stream);
- WriteLine(authenticationFieldReply, stream);
- WriteLine(stream);
- }
- else if (CurrentProxyAuthenticationMethod == ProxyAuthenticationMethod.Digest)
- {
- if (string.IsNullOrEmpty(digestField))
- throw new ProxyServerAuthenticationException("Digest authentication scheme is not supported/enabled by the proxy server.");
+ string basicField = fields.Find(str => str.StartsWith("Proxy-Authenticate: Basic", StringComparison.InvariantCultureIgnoreCase));
+ var digestFields = fields.FindAll(str => str.StartsWith("Proxy-Authenticate: Digest", StringComparison.InvariantCultureIgnoreCase));
- string authenticationFieldReply = string.Format(
- "Proxy-Authorization: Digest username=\"{0}\", uri=\"{1}:{2}\"",
- credentials.UserName, uri.Host, uri.Port);
+ if (CurrentProxyAuthenticationMethod == ProxyAuthenticationMethod.Basic)
+ {
+ if (string.IsNullOrEmpty(basicField))
+ throw new ProxyServerAuthenticationException("Basic authentication scheme is not supported/enabled by the proxy server.");
- string directiveString = digestField.Substring(27, digestField.Length - 27);
- string[] directives = directiveString.Split(new string[] { ", ", "\"" }, StringSplitOptions.RemoveEmptyEntries);
+ string authenticationFieldReply = string.Format("Proxy-Authorization: Basic {0}",
+ Convert.ToBase64String(Encoding.UTF8.GetBytes(credentials.UserName + ":" + credentials.Password)));
+ WriteLine(header, stream);
+ WriteLine(authenticationFieldReply, stream);
+ WriteLine(stream);
+ }
+ else if (CurrentProxyAuthenticationMethod == ProxyAuthenticationMethod.Digest)
+ {
+ var digestField = digestFields.FirstOrDefault(f => f.ToLowerInvariant().Contains("sha-256")) ?? digestFields.FirstOrDefault();
- string algorithm = null; // optional
- string opaque = null; // optional
- string qop = null; // optional
- string realm = null;
- string nonce = null;
+ if (string.IsNullOrEmpty(digestField))
+ throw new ProxyServerAuthenticationException("Digest authentication scheme is not supported/enabled by the proxy server.");
- for (int i = 0; i < directives.Length; ++i)
- {
- switch (directives[i])
- {
- case "stale=":
- if (directives[++i].ToLower() == "true")
- throw new ProxyServerAuthenticationException("Stale nonce in Digest authentication attempt.");
- break;
- case "realm=":
- authenticationFieldReply += string.Format(", {0}\"{1}\"", directives[i], directives[++i]);
- realm = directives[i];
- break;
- case "nonce=":
- authenticationFieldReply += string.Format(", {0}\"{1}\"", directives[i], directives[++i]);
- nonce = directives[i];
- break;
- case "opaque=":
- authenticationFieldReply += string.Format(", {0}\"{1}\"", directives[i], directives[++i]);
- opaque = directives[i];
- break;
- case "algorithm=":
- authenticationFieldReply += string.Format(", {0}\"{1}\"", directives[i], directives[++i]);
- algorithm = directives[i];
- break;
- case "qop=":
- List qops = new List(directives[++i].Split(new char[] { ',' }));
- if (qops.Count > 0)
- {
- if (qops.Contains("auth"))
- qop = "auth";
- else if (qops.Contains("auth-int"))
- qop = "auth-int";
- else
- throw new ProxyServerAuthenticationException(
- "Digest authentication's quality-of-protection directive of is not supported.");
- authenticationFieldReply += string.Format(", qop=\"{0}\"", qop);
- }
- break;
- default:
- break;
- }
- }
+ string authenticationFieldReply = string.Format(
+ "Proxy-Authorization: Digest username=\"{0}\", uri=\"{1}:{2}\"",
+ credentials.UserName, uri.Host, uri.Port);
+
+ int len = "Proxy-Authorization: Digest".Length;
+ string directiveString = digestField.Substring(len, digestField.Length - len);
+ string[] directives = directiveString.Split(new[] {", ", "\""}, StringSplitOptions.RemoveEmptyEntries);
+
+ string algorithm = null; // optional
+ string opaque = null; // optional
+ string qop = null; // optional
+ string realm = null;
+ string nonce = null;
- string clientNonce = "X3nC3nt3r"; // todo: generate random string
- if (qop != null)
- authenticationFieldReply += string.Format(", cnonce=\"{0}\"", clientNonce);
-
- string nonceCount = "00000001"; // todo: track nonces and their corresponding nonce counts
- if (qop != null)
- authenticationFieldReply += string.Format(", nc={0}", nonceCount);
-
- string HA1 = "";
- string scratch = string.Format("{0}:{1}:{2}", credentials.UserName, realm, credentials.Password);
- if (algorithm == null || algorithm == "MD5")
- HA1 = MD5Hash(scratch);
- else
- HA1 = MD5Hash(string.Format("{0}:{1}:{2}", MD5Hash(scratch), nonce, clientNonce));
-
- string HA2 = "";
- scratch = GetPartOrNull(header, 0);
- scratch = string.Format("{0}:{1}:{2}", scratch ?? "CONNECT", uri.Host, uri.Port);
- if (qop == null || qop == "auth")
- HA2 = MD5Hash(scratch);
- else
+ for (int i = 0; i < directives.Length; ++i)
+ {
+ switch (directives[i].ToLowerInvariant())
{
- string entityBody = initialResponse[initialResponse.Count - 1]; // entity body should have been stored as last element of initialResponse
- string str = string.Format("{0}:{1}", scratch, MD5Hash(entityBody));
- HA2 = MD5Hash(str);
+ case "stale=":
+ if (directives[++i].ToLowerInvariant() == "true")
+ throw new ProxyServerAuthenticationException("Stale nonce in Digest authentication attempt.");
+ break;
+ case "realm=":
+ authenticationFieldReply += $", realm=\"{directives[++i]}\"";
+ realm = directives[i];
+ break;
+ case "nonce=":
+ authenticationFieldReply += $", nonce=\"{directives[++i]}\"";
+ nonce = directives[i];
+ break;
+ case "opaque=":
+ authenticationFieldReply += $", opaque=\"{directives[++i]}\"";
+ opaque = directives[i];
+ break;
+ case "algorithm=":
+ authenticationFieldReply += $", algorithm={directives[++i]}"; //unquoted; see RFC7616-3.4
+ algorithm = directives[i];
+ break;
+ case "qop=":
+ var qops = directives[++i].Split(',');
+ if (qops.Length > 0)
+ {
+ qop = qops.FirstOrDefault(q => q.ToLowerInvariant() == "auth") ??
+ qops.FirstOrDefault(q => q.ToLowerInvariant() == "auth-int") ??
+ throw new ProxyServerAuthenticationException(
+ "Digest authentication's quality-of-protection directive is not supported.");
+ authenticationFieldReply += $", qop={qop}"; //unquoted; see RFC7616-3.4
+ }
+ break;
}
+ }
- string response = "";
- if (qop == null)
- response = MD5Hash(string.Format("{0}:{1}:{2}", HA1, nonce, HA2));
- else
- response = MD5Hash(string.Format("{0}:{1}:{2}:{3}:{4}:{5}", HA1, nonce, nonceCount, clientNonce, qop, HA2));
+ string clientNonce = GenerateNonce();
+ if (qop != null)
+ authenticationFieldReply += $", cnonce=\"{clientNonce}\"";
- authenticationFieldReply += string.Format(", response=\"{0}\"", response);
+ string nonceCount = "00000001"; // todo: track nonces and their corresponding nonce counts
+ if (qop != null)
+ authenticationFieldReply += $", nc={nonceCount}"; //unquoted; see RFC7616-3.4
- WriteLine(header, stream);
- WriteLine(authenticationFieldReply, stream);
- WriteLine(stream);
- }
- else
- {
- string authType = GetPartOrNull(fields[0], 1);
- throw new ProxyServerAuthenticationException(
- string.Format("Proxy server's {0} authentication method is not supported.", authType ?? "chosen"));
- }
+ Func algFunc;
+ var scratch1 = string.Join(":", credentials.UserName, realm, credentials.Password);
+ string HA1;
+ algorithm = algorithm ?? "md5";
- // handle authentication attempt response
- List authenticatedResponse = new List();
- ReadHttpHeaders(ref stream, proxy, nodelay, timeoutMs, authenticatedResponse);
- if (authenticatedResponse.Count == 0)
- throw new BadServerResponseException("No response from the proxy server after authentication attempt.");
- switch (getResultCode(authenticatedResponse[0]))
+ switch (algorithm.ToLowerInvariant())
{
- case 200:
+ case "sha-256-sess":
+ algFunc = Sha256Hash;
+ HA1 = algFunc(string.Join(":", algFunc(scratch1), nonce, clientNonce));
+ break;
+ case "md5-sess":
+ algFunc = _MD5Hash;
+ HA1 = algFunc(string.Join(":", algFunc(scratch1), nonce, clientNonce));
break;
- case 407:
- throw new ProxyServerAuthenticationException("Proxy server denied access due to wrong credentials.");
+ case "sha-256":
+ algFunc = Sha256Hash;
+ HA1 = algFunc(scratch1);
+ break;
+ case "md5":
default:
- throw new BadServerResponseException(string.Format(
- "Received error code {0} from the server", authenticatedResponse[0]));
+ algFunc = _MD5Hash;
+ HA1 = algFunc(scratch1);
+ break;
}
+
+ var scratch2 = string.Join(":", GetPartOrNull(header, 0) ?? "CONNECT", uri.Host, uri.Port);
+ string HA2 = qop == null || qop.ToLowerInvariant() == "auth"
+ ? algFunc(scratch2)
+ : algFunc(string.Join(":", scratch2, algFunc(initialResponse[initialResponse.Count - 1])));
+
+ string[] array3 = qop == null
+ ? new[] {HA1, nonce, HA2}
+ : new[] {HA1, nonce, nonceCount, clientNonce, qop, HA2};
+ var response = algFunc(string.Join(":", array3));
+
+ authenticationFieldReply += $", response=\"{response}\"";
+
+ WriteLine(header, stream);
+ WriteLine(authenticationFieldReply, stream);
+ WriteLine(stream);
+ }
+ else
+ {
+ string authType = GetPartOrNull(fields[0], 1);
+ throw new ProxyServerAuthenticationException(
+ string.Format("Proxy server's {0} authentication method is not supported.", authType ?? "chosen"));
+ }
+
+ // handle authentication attempt response
+ List authenticatedResponse = new List();
+ ReadHttpHeaders(ref stream, proxy, nodelay, timeoutMs, authenticatedResponse);
+ if (authenticatedResponse.Count == 0)
+ throw new BadServerResponseException("No response from the proxy server after authentication attempt.");
+
+ switch (getResultCode(authenticatedResponse[0]))
+ {
+ case 200:
+ break;
+ case 407:
+ throw new ProxyServerAuthenticationException("Proxy server denied access due to wrong credentials.");
+ default:
+ throw new BadServerResponseException(string.Format(
+ "Received error code {0} from the server", authenticatedResponse[0]));
}
}