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])); } }