From 537229902f97d57fc61191fbf86e6fbc16ff1fe5 Mon Sep 17 00:00:00 2001 From: sta Date: Wed, 1 Jan 2014 21:43:18 +0900 Subject: [PATCH] Fix for HTTP Basic/Digest Authentication --- Example/Program.cs | 1 + Example2/Program.cs | 28 +- Example3/Program.cs | 38 +- websocket-sharp/AuthenticationChallenge.cs | 152 +- websocket-sharp/AuthenticationResponse.cs | 320 +-- websocket-sharp/Ext.cs | 99 +- websocket-sharp/HandshakeBase.cs | 51 +- websocket-sharp/HandshakeRequest.cs | 104 +- websocket-sharp/HandshakeResponse.cs | 75 +- websocket-sharp/Net/AuthenticationSchemes.cs | 128 +- websocket-sharp/Net/EndPointListener.cs | 875 +++--- websocket-sharp/Net/HttpBasicIdentity.cs | 81 + websocket-sharp/Net/HttpConnection.cs | 1000 +++---- websocket-sharp/Net/HttpDigestIdentity.cs | 185 ++ websocket-sharp/Net/HttpListener.cs | 1385 +++++----- websocket-sharp/Net/HttpListenerContext.cs | 359 +-- websocket-sharp/Net/HttpListenerRequest.cs | 1507 +++++----- websocket-sharp/Net/HttpListenerResponse.cs | 1602 ++++++----- websocket-sharp/Net/HttpUtility.cs | 2462 +++++++++-------- websocket-sharp/Net/ListenerAsyncResult.cs | 344 ++- websocket-sharp/Net/NetworkCredential.cs | 179 ++ websocket-sharp/Net/RequestStream.cs | 420 +-- websocket-sharp/Net/ResponseStream.cs | 502 ++-- .../HttpListenerWebSocketContext.cs | 111 +- .../WebSockets/TcpListenerWebSocketContext.cs | 186 +- .../Net/WebSockets/WebSocketContext.cs | 89 +- websocket-sharp/Server/HttpServer.cs | 553 ++-- websocket-sharp/Server/WebSocketServer.cs | 454 ++- websocket-sharp/WebSocket.cs | 336 ++- websocket-sharp/WsCredential.cs | 119 - websocket-sharp/WsStream.cs | 12 +- websocket-sharp/websocket-sharp.csproj | 4 +- 32 files changed, 7488 insertions(+), 6273 deletions(-) create mode 100644 websocket-sharp/Net/HttpBasicIdentity.cs create mode 100644 websocket-sharp/Net/HttpDigestIdentity.cs create mode 100644 websocket-sharp/Net/NetworkCredential.cs delete mode 100644 websocket-sharp/WsCredential.cs diff --git a/Example/Program.cs b/Example/Program.cs index ae8a2089..7134e553 100644 --- a/Example/Program.cs +++ b/Example/Program.cs @@ -122,6 +122,7 @@ namespace Example { //}; //ws.SetCookie(new Cookie("nobita", "\"idiot, gunfighter\"")); //ws.SetCookie(new Cookie("dora", "tanuki")); + //ws.SetCredentials ("nobita", "password", false); ws.Connect(); //Console.WriteLine("Compression: {0}", ws.Compression); diff --git a/Example2/Program.cs b/Example2/Program.cs index 5aad0e28..bc3ba988 100644 --- a/Example2/Program.cs +++ b/Example2/Program.cs @@ -2,6 +2,7 @@ using System; using System.Configuration; using System.Security.Cryptography.X509Certificates; using WebSocketSharp; +using WebSocketSharp.Net; using WebSocketSharp.Server; namespace Example2 @@ -17,10 +18,28 @@ namespace Example2 #if DEBUG wssv.Log.Level = LogLevel.TRACE; #endif - //var cert = ConfigurationManager.AppSettings ["ServerCertFile"]; - //var password = ConfigurationManager.AppSettings ["CertFilePassword"]; - //wssv.Certificate = new X509Certificate2 (cert, password); + + // HTTP Basic/Digest Authentication + /* + wssv.AuthenticationSchemes = AuthenticationSchemes.Digest; + wssv.Realm = "WebSocket Test"; + wssv.UserCredentialsFinder = identity => { + var name = identity.Name; + return name == "nobita" + ? new NetworkCredential (name, "password") + : null; + }; + */ + + // Secure Connection + /* + var cert = ConfigurationManager.AppSettings ["ServerCertFile"]; + var password = ConfigurationManager.AppSettings ["CertFilePassword"]; + wssv.Certificate = new X509Certificate2 (cert, password); + */ + //wssv.KeepClean = false; + wssv.AddWebSocketService ("/Echo"); wssv.AddWebSocketService ("/Chat"); //wssv.AddWebSocketService ("/Chat", () => new Chat ("Anon#")); @@ -28,8 +47,7 @@ namespace Example2 //wssv.AddWebSocketService ("/チャット"); wssv.Start (); - if (wssv.IsListening) - { + if (wssv.IsListening) { Console.WriteLine ( "A WebSocket Server listening on port: {0} service paths:", wssv.Port); diff --git a/Example3/Program.cs b/Example3/Program.cs index 99f78b48..3a1d8e48 100644 --- a/Example3/Program.cs +++ b/Example3/Program.cs @@ -19,24 +19,39 @@ namespace Example3 _httpsv.Log.Level = LogLevel.TRACE; #endif _httpsv.RootPath = ConfigurationManager.AppSettings ["RootPath"]; - //var cert = ConfigurationManager.AppSettings ["ServerCertFile"]; - //var password = ConfigurationManager.AppSettings ["CertFilePassword"]; - //_httpsv.Certificate = new X509Certificate2 (cert, password); + + // HTTP Basic/Digest Authentication + /* + _httpsv.AuthenticationSchemes = AuthenticationSchemes.Digest; + _httpsv.Realm = "WebSocket Test"; + _httpsv.UserCredentialsFinder = identity => { + var name = identity.Name; + return name == "nobita" + ? new NetworkCredential (name, "password") + : null; + }; + */ + + // Secure Connection + /* + var cert = ConfigurationManager.AppSettings ["ServerCertFile"]; + var password = ConfigurationManager.AppSettings ["CertFilePassword"]; + _httpsv.Certificate = new X509Certificate2 (cert, password); + */ + //_httpsv.KeepClean = false; + _httpsv.AddWebSocketService ("/Echo"); _httpsv.AddWebSocketService ("/Chat"); //_httpsv.AddWebSocketService ("/Chat", () => new Chat ("Anon#")); - _httpsv.OnGet += (sender, e) => - { - onGet (e); - }; + _httpsv.OnGet += (sender, e) => onGet (e); _httpsv.Start (); - if (_httpsv.IsListening) - { + if (_httpsv.IsListening) { Console.WriteLine ( - "An HTTP Server listening on port: {0} WebSocket service paths:", _httpsv.Port); + "An HTTP Server listening on port: {0} WebSocket service paths:", + _httpsv.Port); foreach (var path in _httpsv.WebSocketServices.ServicePaths) Console.WriteLine (" {0}", path); @@ -61,8 +76,7 @@ namespace Example3 var request = eventArgs.Request; var response = eventArgs.Response; var content = getContent (request.RawUrl); - if (content != null) - { + if (content != null) { response.WriteContent (content); return; } diff --git a/websocket-sharp/AuthenticationChallenge.cs b/websocket-sharp/AuthenticationChallenge.cs index 8ecae2cd..0c70929b 100644 --- a/websocket-sharp/AuthenticationChallenge.cs +++ b/websocket-sharp/AuthenticationChallenge.cs @@ -4,8 +4,8 @@ * * The MIT License * - * Copyright (c) 2013 sta.blockhead - * + * Copyright (c) 2013-2014 sta.blockhead + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -15,7 +15,7 @@ * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -27,29 +27,36 @@ #endregion using System; +using System.Collections.Specialized; using System.Text; -namespace WebSocketSharp { - - internal class AuthenticationChallenge { - +namespace WebSocketSharp +{ + internal class AuthenticationChallenge + { #region Private Fields - private string _algorithm; - private string _domain; - private string _nonce; - private string _opaque; - private string _qop; - private string _realm; - private string _scheme; - private string _stale; + private NameValueCollection _params; + private string _scheme; #endregion - #region Private Constructors + #region Internal Constructors - private AuthenticationChallenge() + internal AuthenticationChallenge (string authScheme, string authParams) { + _scheme = authScheme; + _params = authParams.ParseAuthParams (); + } + + #endregion + + #region Internal Properties + + internal NameValueCollection Params { + get { + return _params; + } } #endregion @@ -58,81 +65,49 @@ namespace WebSocketSharp { public string Algorithm { get { - return _algorithm ?? String.Empty; - } - - private set { - _algorithm = value; + return _params ["algorithm"]; } } public string Domain { get { - return _domain ?? String.Empty; - } - - private set { - _domain = value; + return _params ["domain"]; } } public string Nonce { get { - return _nonce ?? String.Empty; - } - - private set { - _nonce = value; + return _params ["nonce"]; } } public string Opaque { get { - return _opaque ?? String.Empty; - } - - private set { - _opaque = value; + return _params ["opaque"]; } } public string Qop { get { - return _qop ?? String.Empty; - } - - private set { - _qop = value; + return _params ["qop"]; } } public string Realm { get { - return _realm ?? String.Empty; - } - - private set { - _realm = value; + return _params ["realm"]; } } public string Scheme { get { - return _scheme ?? String.Empty; - } - - private set { - _scheme = value; + return _scheme; } } public string Stale { get { - return _stale ?? String.Empty; - } - - private set { - _stale = value; + return _params ["stale"]; } } @@ -140,64 +115,13 @@ namespace WebSocketSharp { #region Public Methods - public static AuthenticationChallenge Parse(string challenge) + public static AuthenticationChallenge Parse (string value) { - var authChallenge = new AuthenticationChallenge(); - if (challenge.StartsWith("basic", StringComparison.OrdinalIgnoreCase)) - { - authChallenge.Scheme = "Basic"; - authChallenge.Realm = challenge.Substring(6).GetValueInternal("=").Trim('"'); - - return authChallenge; - } - - foreach (var p in challenge.SplitHeaderValue(',')) - { - var param = p.Trim(); - if (param.StartsWith("digest", StringComparison.OrdinalIgnoreCase)) - { - authChallenge.Scheme = "Digest"; - authChallenge.Realm = param.Substring(7).GetValueInternal("=").Trim('"'); - - continue; - } - - var value = param.GetValueInternal("=").Trim('"'); - if (param.StartsWith("domain", StringComparison.OrdinalIgnoreCase)) - { - authChallenge.Domain = value; - continue; - } - - if (param.StartsWith("nonce", StringComparison.OrdinalIgnoreCase)) - { - authChallenge.Nonce = value; - continue; - } - - if (param.StartsWith("opaque", StringComparison.OrdinalIgnoreCase)) - { - authChallenge.Opaque = value; - continue; - } - - if (param.StartsWith("stale", StringComparison.OrdinalIgnoreCase)) - { - authChallenge.Stale = value; - continue; - } - - if (param.StartsWith("algorithm", StringComparison.OrdinalIgnoreCase)) - { - authChallenge.Algorithm = value; - continue; - } - - if (param.StartsWith("qop", StringComparison.OrdinalIgnoreCase)) - authChallenge.Qop = value; - } - - return authChallenge; + var challenge = value.Split (new char [] {' '}, 2); + var scheme = challenge [0].ToLower (); + return scheme == "basic" || scheme == "digest" + ? new AuthenticationChallenge (scheme, challenge [1]) + : null; } #endregion diff --git a/websocket-sharp/AuthenticationResponse.cs b/websocket-sharp/AuthenticationResponse.cs index 7f8b2747..cc81fdaa 100644 --- a/websocket-sharp/AuthenticationResponse.cs +++ b/websocket-sharp/AuthenticationResponse.cs @@ -5,7 +5,7 @@ * The MIT License * * Copyright (c) 2013 sta.blockhead - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -15,7 +15,7 @@ * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -27,56 +27,81 @@ #endregion using System; -using System.Security.Cryptography; +using System.Collections.Specialized; +using System.Security.Principal; using System.Text; +using WebSocketSharp.Net; -namespace WebSocketSharp { - - internal class AuthenticationResponse { - +namespace WebSocketSharp +{ + internal class AuthenticationResponse + { #region Private Fields - private string _algorithm; - private string _cnonce; - private string _method; - private string _nc; - private string _nonce; - private string _opaque; - private string _password; - private string _qop; - private string _realm; - private string _response; - private string _scheme; - private string _uri; - private string _userName; + private uint _nonceCount; + private NameValueCollection _params; + private string _scheme; #endregion #region Private Constructors - private AuthenticationResponse() + private AuthenticationResponse ( + string authScheme, NameValueCollection authParams) { + _scheme = authScheme; + _params = authParams; } #endregion - #region Public Constructors + #region Internal Constructors - public AuthenticationResponse(WsCredential credential) + internal AuthenticationResponse (NetworkCredential credentials) + : this ("Basic", new NameValueCollection (), credentials, 0) { - _userName = credential.UserName; - _password = credential.Password; - _scheme = "Basic"; } - public AuthenticationResponse(WsCredential credential, AuthenticationChallenge challenge) + internal AuthenticationResponse ( + AuthenticationChallenge challenge, + NetworkCredential credentials, + uint nonceCount) + : this (challenge.Scheme, challenge.Params, credentials, nonceCount) { - _userName = credential.UserName; - _password = credential.Password; - _scheme = challenge.Scheme; - _realm = challenge.Realm; - if (_scheme == "Digest") - initForDigest(credential, challenge); + } + + internal AuthenticationResponse ( + string authScheme, + NameValueCollection authParams, + NetworkCredential credentials, + uint nonceCount) + { + _scheme = authScheme.ToLower (); + _params = authParams; + _params ["username"] = credentials.UserName; + _params ["password"] = credentials.Password; + _params ["uri"] = credentials.Domain; + _nonceCount = nonceCount; + if (_scheme == "digest") + initAsDigest (); + } + + #endregion + + #region Internal Properties + + internal uint NonceCount { + get { + return _nonceCount < UInt32.MaxValue + ? _nonceCount + : 0; + } + } + + internal NameValueCollection Params { + get { + return _params; + } } #endregion @@ -85,111 +110,73 @@ namespace WebSocketSharp { public string Algorithm { get { - return _algorithm ?? String.Empty; - } - - private set { - _algorithm = value; + return _params ["algorithm"]; } } public string Cnonce { get { - return _cnonce ?? String.Empty; - } - - private set { - _cnonce = value; + return _params ["cnonce"]; } } public string Nc { get { - return _nc ?? String.Empty; - } - - private set { - _nc = value; + return _params ["nc"]; } } public string Nonce { get { - return _nonce ?? String.Empty; - } - - private set { - _nonce = value; + return _params ["nonce"]; } } public string Opaque { get { - return _opaque ?? String.Empty; + return _params ["opaque"]; } + } - private set { - _opaque = value; + public string Password { + get { + return _params ["password"]; } } public string Qop { get { - return _qop ?? String.Empty; - } - - private set { - _qop = value; + return _params ["qop"]; } } public string Realm { get { - return _realm ?? String.Empty; - } - - private set { - _realm = value; + return _params ["realm"]; } } public string Response { get { - return _response ?? String.Empty; - } - - private set { - _response = value; + return _params ["response"]; } } public string Scheme { get { - return _scheme ?? String.Empty; - } - - private set { - _scheme = value; + return _scheme; } } public string Uri { get { - return _uri ?? String.Empty; - } - - private set { - _uri = value; + return _params ["uri"]; } } public string UserName { get { - return _userName ?? String.Empty; - } - - private set { - _userName = value; + return _params ["username"]; } } @@ -197,130 +184,77 @@ namespace WebSocketSharp { #region Private Methods - private string a1() + private static bool contains (string [] array, string item) { - var result = String.Format("{0}:{1}:{2}", _userName, _realm, _password); - return _algorithm != null && _algorithm.ToLower() == "md5-sess" - ? String.Format("{0}:{1}:{2}", hash(result), _nonce, _cnonce) - : result; + foreach (var i in array) + if (i.Trim ().ToLower () == item) + return true; + + return false; } - private string a2() + private void initAsDigest () { - return String.Format("{0}:{1}", _method, _uri); - } - - private static string createNonceValue() - { - var src = new byte[16]; - var rand = new Random(); - rand.NextBytes(src); - var nonce = new StringBuilder(32); - foreach (var b in src) - nonce.Append(b.ToString("x2")); - - return nonce.ToString(); - } - - private string createRequestDigest() - { - if (Qop == "auth") - { - var data = String.Format("{0}:{1}:{2}:{3}:{4}", - _nonce, _nc, _cnonce, _qop, hash(a2())); - return kd(hash(a1()), data); - } - - return kd(hash(a1()), String.Format("{0}:{1}", _nonce, hash(a2()))); - } - - private static string hash(string value) - { - var md5 = MD5.Create(); - var src = Encoding.UTF8.GetBytes(value); - var hashed = md5.ComputeHash(src); - var result = new StringBuilder(64); - foreach (var b in hashed) - result.Append(b.ToString("x2")); - - return result.ToString(); - } - - private void initForDigest(WsCredential credential, AuthenticationChallenge challenge) - { - _nonce = challenge.Nonce; - _method = "GET"; - _uri = credential.Domain; - _algorithm = challenge.Algorithm; - _opaque = challenge.Opaque; - foreach (var qop in challenge.Qop.Split(',')) - { - if (qop.Trim().ToLower() == "auth") - { - _qop = "auth"; - _nc = "00000001"; - break; + var qops = _params ["qop"]; + if (qops != null) { + var qop = "auth"; + if (contains (qops.Split (','), qop)) { + _params ["qop"] = qop; + _params ["nc"] = String.Format ("{0:x8}", ++_nonceCount); + _params ["cnonce"] = HttpUtility.CreateNonceValue (); } + else + _params ["qop"] = null; } - _cnonce = createNonceValue(); - _response = createRequestDigest(); - } - - private static string kd(string secret, string data) - { - var concatenated = String.Format("{0}:{1}", secret, data); - return hash(concatenated); - } - - private string toBasicCredentials() - { - var userPass = String.Format("{0}:{1}", _userName, _password); - var base64UserPass = Convert.ToBase64String(Encoding.UTF8.GetBytes(userPass)); - - return "Basic " + base64UserPass; - } - - private string toDigestCredentials() - { - var digestResponse = new StringBuilder(64); - digestResponse.AppendFormat("username={0}", _userName.Quote()); - digestResponse.AppendFormat(", realm={0}", _realm.Quote()); - digestResponse.AppendFormat(", nonce={0}", _nonce.Quote()); - digestResponse.AppendFormat(", uri={0}", _uri.Quote()); - digestResponse.AppendFormat(", response={0}", _response.Quote()); - if (!_algorithm.IsNullOrEmpty()) - digestResponse.AppendFormat(", algorithm={0}", _algorithm); - - if (!_opaque.IsNullOrEmpty()) - digestResponse.AppendFormat(", opaque={0}", _opaque.Quote()); - - if (!_qop.IsNullOrEmpty()) - digestResponse.AppendFormat(", qop={0}", _qop); - - if (!_nc.IsNullOrEmpty()) - digestResponse.AppendFormat(", nc={0}", _nc); - - if (!_qop.IsNullOrEmpty()) - digestResponse.AppendFormat(", cnonce={0}", _cnonce.Quote()); - - return "Digest " + digestResponse.ToString(); + _params ["method"] = "GET"; + _params ["response"] = HttpUtility.CreateRequestDigest (_params); } #endregion #region Public Methods - public static AuthenticationResponse Parse(string response) + public static AuthenticationResponse Parse (string value) { - throw new NotImplementedException(); + try { + var credentials = value.Split (new char [] { ' ' }, 2); + if (credentials.Length != 2) + return null; + + var scheme = credentials [0].ToLower (); + return scheme == "basic" + ? new AuthenticationResponse ( + scheme, credentials [1].ParseBasicAuthResponseParams ()) + : scheme == "digest" + ? new AuthenticationResponse ( + scheme, credentials [1].ParseAuthParams ()) + : null; + } + catch { + } + + return null; } - public override string ToString() + public IIdentity ToIdentity () { - return _scheme == "Basic" - ? toBasicCredentials() - : toDigestCredentials(); + return _scheme == "basic" + ? new HttpBasicIdentity ( + _params ["username"], _params ["password"]) as IIdentity + : _scheme == "digest" + ? new HttpDigestIdentity (_params) + : null; + } + + public override string ToString () + { + return _scheme == "basic" + ? HttpUtility.CreateBasicAuthCredentials ( + _params ["username"], _params ["password"]) + : _scheme == "digest" + ? HttpUtility.CreateDigestAuthCredentials (_params) + : String.Empty; } #endregion diff --git a/websocket-sharp/Ext.cs b/websocket-sharp/Ext.cs index d5a25717..1eb0a1ff 100644 --- a/websocket-sharp/Ext.cs +++ b/websocket-sharp/Ext.cs @@ -1,23 +1,22 @@ #region License /* * Ext.cs - * IsPredefinedScheme and MaybeUri methods are derived from System.Uri.cs - * GetStatusDescription method is derived from System.Net.HttpListenerResponse.cs + * + * Some part of this code are derived from Mono (http://www.mono-project.com). + * + * ParseBasicAuthResponseParams is derived from System.Net.HttpListenerContext.cs + * GetStatusDescription is derived from System.Net.HttpListenerResponse.cs + * IsPredefinedScheme and MaybeUri are derived from System.Uri.cs * * The MIT License * + * Copyright (c) 2001 Garrett Rooney + * Copyright (c) 2003 Ian MacLean + * Copyright (c) 2003 Ben Maurer + * Copyright (c) 2003, 2005, 2009 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2009 Stephane Delcroix * Copyright (c) 2010-2013 sta.blockhead * - * System.Uri.cs - * (C) 2001 Garrett Rooney - * (C) 2003 Ian MacLean - * (C) 2003 Ben Maurer - * Copyright (C) 2003, 2005, 2009 Novell, Inc. (http://www.novell.com) - * Copyright (c) 2009 Stephane Delcroix - * - * System.Net.HttpListenerResponse.cs - * Copyright (C) 2003, 2005, 2009 Novell, Inc. (http://www.novell.com) - * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -27,7 +26,7 @@ * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -279,6 +278,20 @@ namespace WebSocketSharp : null; } + internal static void Close ( + this HttpListenerResponse response, HttpStatusCode code) + { + response.StatusCode = (int) code; + response.OutputStream.Close (); + } + + internal static void CloseWithAuthChallenge ( + this HttpListenerResponse response, string challenge) + { + response.Headers.SetInternal ("WWW-Authenticate", challenge, true); + response.Close (HttpStatusCode.Unauthorized); + } + internal static byte [] Compress (this byte [] value, CompressionMethod method) { return method == CompressionMethod.DEFLATE @@ -447,9 +460,9 @@ namespace WebSocketSharp } internal static TcpListenerWebSocketContext GetWebSocketContext ( - this TcpClient client, bool secure, X509Certificate cert) + this TcpClient client, X509Certificate cert, bool secure, Logger logger) { - return new TcpListenerWebSocketContext (client, secure, cert); + return new TcpListenerWebSocketContext (client, cert, secure, logger); } internal static bool IsCompressionExtension (this string value) @@ -517,6 +530,57 @@ namespace WebSocketSharp : String.Format ("\"{0}\"", value.Replace ("\"", "\\\"")); } + internal static NameValueCollection ParseBasicAuthResponseParams ( + this string value) + { + // HTTP Basic Authentication response is a formatted Base64 string. + var userPass = Encoding.Default.GetString ( + Convert.FromBase64String (value)); + + // The format is [\]:. + // 'domain' is optional. + var i = userPass.IndexOf (':'); + var username = userPass.Substring (0, i); + var password = i < userPass.Length - 1 + ? userPass.Substring (i + 1) + : String.Empty; + + // Check if 'domain' exists. + i = username.IndexOf ('\\'); + if (i > 0) + username = username.Substring (i + 1); + + var result = new NameValueCollection (); + result ["username"] = username; + result ["password"] = password; + + return result; + } + + internal static NameValueCollection ParseAuthParams (this string value) + { + var result = new NameValueCollection (); + var i = 0; + string name, val; + foreach (var param in value.SplitHeaderValue (',')) { + i = param.IndexOf ('='); + if (i > 0) { + name = param.Substring (0, i).Trim (); + val = i < param.Length - 1 + ? param.Substring (i + 1).Trim ().Trim ('"') + : String.Empty; + } + else { + name = param; + val = String.Empty; + } + + result.Add (name, val); + } + + return result; + } + internal static byte [] ReadBytes (this Stream stream, int length) { return stream.readBytes (new byte [length], 0, length); @@ -1642,8 +1706,9 @@ namespace WebSocketSharp /// Converts the specified to a . /// /// - /// A converted from , or - /// if is or . + /// A converted from , or + /// if is + /// or empty. /// /// /// A to convert. diff --git a/websocket-sharp/HandshakeBase.cs b/websocket-sharp/HandshakeBase.cs index d64260e3..9d16165a 100644 --- a/websocket-sharp/HandshakeBase.cs +++ b/websocket-sharp/HandshakeBase.cs @@ -5,7 +5,7 @@ * The MIT License * * Copyright (c) 2012-2013 sta.blockhead - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -15,7 +15,7 @@ * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -35,6 +35,13 @@ namespace WebSocketSharp { internal abstract class HandshakeBase { + #region Private Fields + + private NameValueCollection _headers; + private Version _version; + + #endregion + #region Protected Const Fields protected const string CrLf = "\r\n"; @@ -45,8 +52,8 @@ namespace WebSocketSharp protected HandshakeBase () { - ProtocolVersion = HttpVersion.Version11; - Headers = new NameValueCollection (); + _version = HttpVersion.Version11; + _headers = new NameValueCollection (); } #endregion @@ -54,37 +61,29 @@ namespace WebSocketSharp #region Public Properties public NameValueCollection Headers { - get; protected set; + get { + return _headers; + } + + protected set { + _headers = value; + } } public Version ProtocolVersion { - get; protected set; + get { + return _version; + } + + protected set { + _version = value; + } } #endregion #region Public Methods - public void AddHeader (string name, string value) - { - Headers.Add (name, value); - } - - public bool ContainsHeader (string name) - { - return Headers.Contains (name); - } - - public bool ContainsHeader (string name, string value) - { - return Headers.Contains (name, value); - } - - public string [] GetHeaderValues (string name) - { - return Headers.GetValues (name); - } - public byte [] ToByteArray () { return Encoding.UTF8.GetBytes (ToString ()); diff --git a/websocket-sharp/HandshakeRequest.cs b/websocket-sharp/HandshakeRequest.cs index 606fb103..46fc463b 100644 --- a/websocket-sharp/HandshakeRequest.cs +++ b/websocket-sharp/HandshakeRequest.cs @@ -4,8 +4,8 @@ * * The MIT License * - * Copyright (c) 2012-2013 sta.blockhead - * + * Copyright (c) 2012-2014 sta.blockhead + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -15,7 +15,7 @@ * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,7 +31,6 @@ using System.Collections.Specialized; using System.Linq; using System.Text; using WebSocketSharp.Net; -using WebSocketSharp.Net.WebSockets; namespace WebSocketSharp { @@ -39,7 +38,10 @@ namespace WebSocketSharp { #region Private Fields + private string _method; private NameValueCollection _queryString; + private string _rawUrl; + private Uri _uri; #endregion @@ -55,17 +57,31 @@ namespace WebSocketSharp public HandshakeRequest (string uriString) { - HttpMethod = "GET"; - RequestUri = uriString.ToUri (); - AddHeader ("User-Agent", "websocket-sharp/1.0"); - AddHeader ("Upgrade", "websocket"); - AddHeader ("Connection", "Upgrade"); + _method = "GET"; + _uri = uriString.ToUri (); + _rawUrl = _uri.IsAbsoluteUri + ? _uri.PathAndQuery + : uriString; + + var headers = Headers; + headers ["User-Agent"] = "websocket-sharp/1.0"; + headers ["Upgrade"] = "websocket"; + headers ["Connection"] = "Upgrade"; } #endregion #region Public Properties + public AuthenticationResponse AuthResponse { + get { + var response = Headers ["Authorization"]; + return !response.IsNullOrEmpty () + ? AuthenticationResponse.Parse (response) + : null; + } + } + public CookieCollection Cookies { get { return Headers.GetCookies (false); @@ -73,33 +89,36 @@ namespace WebSocketSharp } public string HttpMethod { - get; private set; + get { + return _method; + } + + private set { + _method = value; + } } public bool IsWebSocketRequest { get { - return HttpMethod == "GET" && + var headers = Headers; + return _method == "GET" && ProtocolVersion >= HttpVersion.Version11 && - Headers.Contains ("Upgrade", "websocket") && - Headers.Contains ("Connection", "Upgrade"); + headers.Contains ("Upgrade", "websocket") && + headers.Contains ("Connection", "Upgrade"); } } public NameValueCollection QueryString { get { - if (_queryString == null) - { + if (_queryString == null) { _queryString = new NameValueCollection (); var i = RawUrl.IndexOf ('?'); - if (i > 0) - { + if (i > 0) { var query = RawUrl.Substring (i + 1); var components = query.Split ('&'); - foreach (var c in components) - { + foreach (var c in components) { var nv = c.GetNameAndValue ("="); - if (nv.Key != null) - { + if (nv.Key != null) { var name = nv.Key.UrlDecode (); var val = nv.Value.UrlDecode (); _queryString.Add (name, val); @@ -114,14 +133,22 @@ namespace WebSocketSharp public string RawUrl { get { - return RequestUri.IsAbsoluteUri - ? RequestUri.PathAndQuery - : RequestUri.OriginalString; + return _rawUrl; + } + + private set { + _rawUrl = value; } } public Uri RequestUri { - get; private set; + get { + return _uri; + } + + private set { + _uri = value; + } } #endregion @@ -132,10 +159,8 @@ namespace WebSocketSharp { var requestLine = request [0].Split (' '); if (requestLine.Length != 3) - { - var msg = "Invalid HTTP Request-Line: " + request [0]; - throw new ArgumentException (msg, "request"); - } + throw new ArgumentException ( + "Invalid HTTP Request-Line: " + request [0], "request"); var headers = new WebHeaderCollection (); for (int i = 1; i < request.Length; i++) @@ -144,8 +169,9 @@ namespace WebSocketSharp return new HandshakeRequest { Headers = headers, HttpMethod = requestLine [0], - RequestUri = requestLine [1].ToUri (), - ProtocolVersion = new Version (requestLine [2].Substring (5)) + ProtocolVersion = new Version (requestLine [2].Substring (5)), + RawUrl = requestLine [1], + RequestUri = requestLine [1].ToUri () }; } @@ -160,21 +186,17 @@ namespace WebSocketSharp if (!sorted [i].Expired) header.AppendFormat ("; {0}", sorted [i].ToString ()); - AddHeader ("Cookie", header.ToString ()); - } - - public void SetAuthorization (AuthenticationResponse response) - { - var credentials = response.ToString (); - AddHeader ("Authorization", credentials); + Headers ["Cookie"] = header.ToString (); } public override string ToString () { var buffer = new StringBuilder (64); - buffer.AppendFormat ("{0} {1} HTTP/{2}{3}", HttpMethod, RawUrl, ProtocolVersion, CrLf); - foreach (string key in Headers.AllKeys) - buffer.AppendFormat ("{0}: {1}{2}", key, Headers [key], CrLf); + buffer.AppendFormat ("{0} {1} HTTP/{2}{3}", _method, _rawUrl, ProtocolVersion, CrLf); + + var headers = Headers; + foreach (var key in headers.AllKeys) + buffer.AppendFormat ("{0}: {1}{2}", key, headers [key], CrLf); buffer.Append (CrLf); return buffer.ToString (); diff --git a/websocket-sharp/HandshakeResponse.cs b/websocket-sharp/HandshakeResponse.cs index 1f5bd23a..8bf33847 100644 --- a/websocket-sharp/HandshakeResponse.cs +++ b/websocket-sharp/HandshakeResponse.cs @@ -4,8 +4,8 @@ * * The MIT License * - * Copyright (c) 2012-2013 sta.blockhead - * + * Copyright (c) 2012-2014 sta.blockhead + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -15,7 +15,7 @@ * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -35,20 +35,34 @@ namespace WebSocketSharp { internal class HandshakeResponse : HandshakeBase { - #region Public Constructors + #region Private Fields - public HandshakeResponse () - : this (HttpStatusCode.SwitchingProtocols) + private string _code; + private string _reason; + + #endregion + + #region Private Constructors + + private HandshakeResponse () { - AddHeader ("Upgrade", "websocket"); - AddHeader ("Connection", "Upgrade"); } + #endregion + + #region Public Constructors + public HandshakeResponse (HttpStatusCode code) { - StatusCode = ((int) code).ToString (); - Reason = code.GetDescription (); - AddHeader ("Server", "websocket-sharp/1.0"); + _code = ((int) code).ToString (); + _reason = code.GetDescription (); + + var headers = Headers; + headers ["Server"] = "websocket-sharp/1.0"; + if (code == HttpStatusCode.SwitchingProtocols) { + headers ["Upgrade"] = "websocket"; + headers ["Connection"] = "Upgrade"; + } } #endregion @@ -72,25 +86,38 @@ namespace WebSocketSharp public bool IsUnauthorized { get { - return StatusCode == "401"; + return _code == "401"; } } public bool IsWebSocketResponse { get { + var headers = Headers; return ProtocolVersion >= HttpVersion.Version11 && - StatusCode == "101" && - Headers.Contains ("Upgrade", "websocket") && - Headers.Contains ("Connection", "Upgrade"); + _code == "101" && + headers.Contains ("Upgrade", "websocket") && + headers.Contains ("Connection", "Upgrade"); } } public string Reason { - get; private set; + get { + return _reason; + } + + private set { + _reason = value; + } } public string StatusCode { - get; private set; + get { + return _code; + } + + private set { + _code = value; + } } #endregion @@ -100,7 +127,7 @@ namespace WebSocketSharp public static HandshakeResponse CreateCloseResponse (HttpStatusCode code) { var res = new HandshakeResponse (code); - res.AddHeader ("Connection", "close"); + res.Headers ["Connection"] = "close"; return res; } @@ -132,16 +159,20 @@ namespace WebSocketSharp if (cookies == null || cookies.Count == 0) return; + var headers = Headers; foreach (var cookie in cookies.Sorted) - AddHeader ("Set-Cookie", cookie.ToResponseString ()); + headers.Add ("Set-Cookie", cookie.ToResponseString ()); } public override string ToString () { var buffer = new StringBuilder (64); - buffer.AppendFormat ("HTTP/{0} {1} {2}{3}", ProtocolVersion, StatusCode, Reason, CrLf); - foreach (string key in Headers.AllKeys) - buffer.AppendFormat ("{0}: {1}{2}", key, Headers [key], CrLf); + buffer.AppendFormat ( + "HTTP/{0} {1} {2}{3}", ProtocolVersion, _code, _reason, CrLf); + + var headers = Headers; + foreach (var key in headers.AllKeys) + buffer.AppendFormat ("{0}: {1}{2}", key, headers [key], CrLf); buffer.Append (CrLf); return buffer.ToString (); diff --git a/websocket-sharp/Net/AuthenticationSchemes.cs b/websocket-sharp/Net/AuthenticationSchemes.cs index 0068baa9..915ad14f 100644 --- a/websocket-sharp/Net/AuthenticationSchemes.cs +++ b/websocket-sharp/Net/AuthenticationSchemes.cs @@ -1,69 +1,67 @@ -// -// AuthenticationSchemes.cs -// Copied from System.Net.AuthenticationSchemes.cs -// -// Author: -// Atsushi Enomoto -// -// Copyright (C) 2005 Novell, Inc. (http://www.novell.com) -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// +#region License +/* + * AuthenticationSchemes.cs + * + * This code is derived from System.Net.AuthenticationSchemes.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2013 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * Atsushi Enomoto + */ +#endregion using System; -namespace WebSocketSharp.Net { - - /// - /// Contains the values of the schemes for authentication. - /// - [Flags] - public enum AuthenticationSchemes { - - /// - /// Indicates that no authentication is allowed. - /// - None, - /// - /// Indicates digest authentication. - /// - Digest = 1, - /// - /// Indicates negotiating with the client to determine the authentication scheme. - /// - Negotiate = 2, - /// - /// Indicates NTLM authentication. - /// - Ntlm = 4, - /// - /// Indicates Windows authentication. - /// - IntegratedWindowsAuthentication = 6, - /// - /// Indicates basic authentication. - /// - Basic = 8, - /// - /// Indicates anonymous authentication. - /// - Anonymous = 0x8000, - } +namespace WebSocketSharp.Net +{ + /// + /// Contains the values of the schemes for authentication. + /// + [Flags] + public enum AuthenticationSchemes + { + /// + /// Indicates that no authentication is allowed. + /// + None, + /// + /// Indicates digest authentication. + /// + Digest = 1, + /// + /// Indicates basic authentication. + /// + Basic = 8, + /// + /// Indicates anonymous authentication. + /// + Anonymous = 0x8000, + } } diff --git a/websocket-sharp/Net/EndPointListener.cs b/websocket-sharp/Net/EndPointListener.cs index 15758e78..eccc6b46 100644 --- a/websocket-sharp/Net/EndPointListener.cs +++ b/websocket-sharp/Net/EndPointListener.cs @@ -1,32 +1,41 @@ -// -// EndPointListener.cs -// Copied from System.Net.EndPointListener.cs -// -// Author: -// Gonzalo Paniagua Javier (gonzalo@novell.com) -// -// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) -// Copyright (c) 2012-2013 sta.blockhead -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// +#region License +/* + * EndPointListener.cs + * + * This code is derived from System.Net.EndPointListener.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2013 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * Gonzalo Paniagua Javier + */ +#endregion using System; using System.Collections; @@ -40,411 +49,405 @@ using System.Threading; namespace WebSocketSharp.Net { - internal sealed class EndPointListener - { - #region Private Fields - - List _all; // host = '+' - X509Certificate2 _cert; - IPEndPoint _endpoint; - Dictionary _prefixes; - bool _secure; - Socket _socket; - List _unhandled; // host = '*' - Dictionary _unregistered; - - #endregion - - #region Public Constructors - - public EndPointListener ( - IPAddress address, - int port, - bool secure, - string certFolderPath, - X509Certificate2 defaultCert - ) - { - if (secure) { - _secure = secure; - _cert = getCertificate (port, certFolderPath, defaultCert); - if (_cert == null) - throw new ArgumentException ("Server certificate not found."); - } - - _endpoint = new IPEndPoint (address, port); - _socket = new Socket (address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - _socket.Bind (_endpoint); - _socket.Listen (500); - var args = new SocketAsyncEventArgs (); - args.UserToken = this; - args.Completed += onAccept; - _socket.AcceptAsync (args); - _prefixes = new Dictionary (); - _unregistered = new Dictionary (); - } - - #endregion - - #region Private Methods - - private static void addSpecial (List prefixes, ListenerPrefix prefix) - { - if (prefixes == null) - return; - - foreach (var p in prefixes) - if (p.Path == prefix.Path) // TODO: code - throw new HttpListenerException (400, "Prefix already in use."); - - prefixes.Add (prefix); - } - - private void checkIfRemove () - { - if (_prefixes.Count > 0) - return; - - if (_unhandled != null && _unhandled.Count > 0) - return; - - if (_all != null && _all.Count > 0) - return; - - EndPointManager.RemoveEndPoint (this, _endpoint); - } - - private static RSACryptoServiceProvider createRSAFromFile (string filename) - { - var rsa = new RSACryptoServiceProvider (); - byte[] pvk = null; - using (var fs = File.Open (filename, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - pvk = new byte [fs.Length]; - fs.Read (pvk, 0, pvk.Length); - } - - rsa.ImportCspBlob (pvk); - return rsa; - } - - private static X509Certificate2 getCertificate ( - int port, string certFolderPath, X509Certificate2 defaultCert) - { - try { - var cer = Path.Combine (certFolderPath, String.Format ("{0}.cer", port)); - var key = Path.Combine (certFolderPath, String.Format ("{0}.key", port)); - if (File.Exists (cer) && File.Exists (key)) - { - var cert = new X509Certificate2 (cer); - cert.PrivateKey = createRSAFromFile (key); - - return cert; - } - } - catch { - } - - return defaultCert; - } - - private static HttpListener matchFromList ( - string host, string path, List list, out ListenerPrefix prefix) - { - prefix = null; - if (list == null) - return null; - - HttpListener best_match = null; - var best_length = -1; - foreach (var p in list) - { - var ppath = p.Path; - if (ppath.Length < best_length) - continue; - - if (path.StartsWith (ppath)) - { - best_length = ppath.Length; - best_match = p.Listener; - prefix = p; - } - } - - return best_match; - } - - private static void onAccept (object sender, EventArgs e) - { - var args = (SocketAsyncEventArgs) e; - var epListener = (EndPointListener) args.UserToken; - Socket accepted = null; - if (args.SocketError == SocketError.Success) - { - accepted = args.AcceptSocket; - args.AcceptSocket = null; - } - - try { - epListener._socket.AcceptAsync (args); - } - catch { - if (accepted != null) - accepted.Close (); - - return; - } - - if (accepted == null) - return; - - HttpConnection conn = null; - try { - conn = new HttpConnection (accepted, epListener, epListener._secure, epListener._cert); - lock (((ICollection) epListener._unregistered).SyncRoot) - { - epListener._unregistered [conn] = conn; - } - - conn.BeginReadRequest (); - } - catch { - if (conn != null) - { - conn.Close (true); - return; - } - - accepted.Close (); - } - } - - private static bool removeSpecial (List prefixes, ListenerPrefix prefix) - { - if (prefixes == null) - return false; - - var count = prefixes.Count; - for (int i = 0; i < count; i++) - { - if (prefixes [i].Path == prefix.Path) - { - prefixes.RemoveAt (i); - return true; - } - } - - return false; - } - - private HttpListener searchListener (Uri uri, out ListenerPrefix prefix) - { - prefix = null; - if (uri == null) - return null; - - var host = uri.Host; - var port = uri.Port; - var path = HttpUtility.UrlDecode (uri.AbsolutePath); - var path_slash = path [path.Length - 1] == '/' ? path : path + "/"; - HttpListener best_match = null; - var best_length = -1; - if (host != null && host.Length > 0) - { - foreach (var p in _prefixes.Keys) - { - var ppath = p.Path; - if (ppath.Length < best_length) - continue; - - if (p.Host != host || p.Port != port) - continue; - - if (path.StartsWith (ppath) || path_slash.StartsWith (ppath)) - { - best_length = ppath.Length; - best_match = _prefixes [p]; - prefix = p; - } - } - - if (best_length != -1) - return best_match; - } - - var list = _unhandled; - best_match = matchFromList (host, path, list, out prefix); - if (path != path_slash && best_match == null) - best_match = matchFromList (host, path_slash, list, out prefix); - - if (best_match != null) - return best_match; - - list = _all; - best_match = matchFromList (host, path, list, out prefix); - if (path != path_slash && best_match == null) - best_match = matchFromList (host, path_slash, list, out prefix); - - if (best_match != null) - return best_match; - - return null; - } - - #endregion - - #region Internal Methods - - internal static bool CertificateExists (int port, string certFolderPath) - { - var cer = Path.Combine (certFolderPath, String.Format ("{0}.cer", port)); - var key = Path.Combine (certFolderPath, String.Format ("{0}.key", port)); - - return File.Exists (cer) && File.Exists (key); - } - - internal void RemoveConnection (HttpConnection connection) - { - lock (((ICollection) _unregistered).SyncRoot) - { - _unregistered.Remove (connection); - } - } - - #endregion - - #region Public Methods - - public void AddPrefix (ListenerPrefix prefix, HttpListener listener) - { - List current, future; - if (prefix.Host == "*") - { - do { - current = _unhandled; - future = current != null - ? new List (current) - : new List (); - prefix.Listener = listener; - addSpecial (future, prefix); - } while (Interlocked.CompareExchange (ref _unhandled, future, current) != current); - - return; - } - - if (prefix.Host == "+") - { - do { - current = _all; - future = current != null - ? new List (current) - : new List (); - prefix.Listener = listener; - addSpecial (future, prefix); - } while (Interlocked.CompareExchange (ref _all, future, current) != current); - - return; - } - - Dictionary prefs, p2; - do { - prefs = _prefixes; - if (prefs.ContainsKey (prefix)) - { - HttpListener other = prefs [prefix]; - if (other != listener) // TODO: code. - throw new HttpListenerException (400, "There's another listener for " + prefix); - - return; - } - - p2 = new Dictionary (prefs); - p2 [prefix] = listener; - } while (Interlocked.CompareExchange (ref _prefixes, p2, prefs) != prefs); - } - - public bool BindContext (HttpListenerContext context) - { - var req = context.Request; - ListenerPrefix prefix; - var listener = searchListener (req.Url, out prefix); - if (listener == null) - return false; - - context.Listener = listener; - context.Connection.Prefix = prefix; - - return true; - } - - public void Close () - { - _socket.Close (); - lock (((ICollection) _unregistered).SyncRoot) - { - var copy = new Dictionary (_unregistered); - foreach (var conn in copy.Keys) - conn.Close (true); - - copy.Clear (); - _unregistered.Clear (); - } - } - - public void RemovePrefix (ListenerPrefix prefix, HttpListener listener) - { - List current, future; - if (prefix.Host == "*") - { - do { - current = _unhandled; - future = current != null - ? new List (current) - : new List (); - if (!removeSpecial (future, prefix)) - break; // Prefix not found. - } while (Interlocked.CompareExchange (ref _unhandled, future, current) != current); - - checkIfRemove (); - return; - } - - if (prefix.Host == "+") - { - do { - current = _all; - future = current != null - ? new List (current) - : new List (); - if (!removeSpecial (future, prefix)) - break; // Prefix not found. - } while (Interlocked.CompareExchange (ref _all, future, current) != current); - - checkIfRemove (); - return; - } - - Dictionary prefs, p2; - do { - prefs = _prefixes; - if (!prefs.ContainsKey (prefix)) - break; - - p2 = new Dictionary (prefs); - p2.Remove (prefix); - } while (Interlocked.CompareExchange (ref _prefixes, p2, prefs) != prefs); - - checkIfRemove (); - } - - public void UnbindContext (HttpListenerContext context) - { - if (context == null || context.Request == null) - return; - - context.Listener.UnregisterContext (context); - } - - #endregion - } + internal sealed class EndPointListener + { + #region Private Fields + + private List _all; // host = '+' + private X509Certificate2 _cert; + private IPEndPoint _endpoint; + private Dictionary _prefixes; + private bool _secure; + private Socket _socket; + private List _unhandled; // host = '*' + private Dictionary _unregistered; + + #endregion + + #region Public Constructors + + public EndPointListener ( + IPAddress address, + int port, + bool secure, + string certFolderPath, + X509Certificate2 defaultCert) + { + if (secure) { + _secure = secure; + _cert = getCertificate (port, certFolderPath, defaultCert); + if (_cert == null) + throw new ArgumentException ("Server certificate not found."); + } + + _endpoint = new IPEndPoint (address, port); + _socket = new Socket ( + address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + _socket.Bind (_endpoint); + _socket.Listen (500); + var args = new SocketAsyncEventArgs (); + args.UserToken = this; + args.Completed += onAccept; + _socket.AcceptAsync (args); + _prefixes = new Dictionary (); + _unregistered = new Dictionary (); + } + + #endregion + + #region Private Methods + + private static void addSpecial ( + List prefixes, ListenerPrefix prefix) + { + if (prefixes == null) + return; + + foreach (var p in prefixes) + if (p.Path == prefix.Path) // TODO: code + throw new HttpListenerException (400, "Prefix already in use."); + + prefixes.Add (prefix); + } + + private void checkIfRemove () + { + if (_prefixes.Count > 0) + return; + + if (_unhandled != null && _unhandled.Count > 0) + return; + + if (_all != null && _all.Count > 0) + return; + + EndPointManager.RemoveEndPoint (this, _endpoint); + } + + private static RSACryptoServiceProvider createRSAFromFile (string filename) + { + var rsa = new RSACryptoServiceProvider (); + byte[] pvk = null; + using (var fs = File.Open (filename, FileMode.Open, FileAccess.Read, FileShare.Read)) { + pvk = new byte [fs.Length]; + fs.Read (pvk, 0, pvk.Length); + } + + rsa.ImportCspBlob (pvk); + return rsa; + } + + private static X509Certificate2 getCertificate ( + int port, string certFolderPath, X509Certificate2 defaultCert) + { + try { + var cer = Path.Combine (certFolderPath, String.Format ("{0}.cer", port)); + var key = Path.Combine (certFolderPath, String.Format ("{0}.key", port)); + if (File.Exists (cer) && File.Exists (key)) { + var cert = new X509Certificate2 (cer); + cert.PrivateKey = createRSAFromFile (key); + + return cert; + } + } + catch { + } + + return defaultCert; + } + + private static HttpListener matchFromList ( + string host, string path, List list, out ListenerPrefix prefix) + { + prefix = null; + if (list == null) + return null; + + HttpListener bestMatch = null; + var bestLength = -1; + foreach (var p in list) { + var ppath = p.Path; + if (ppath.Length < bestLength) + continue; + + if (path.StartsWith (ppath)) { + bestLength = ppath.Length; + bestMatch = p.Listener; + prefix = p; + } + } + + return bestMatch; + } + + private static void onAccept (object sender, EventArgs e) + { + var args = (SocketAsyncEventArgs) e; + var epListener = (EndPointListener) args.UserToken; + Socket accepted = null; + if (args.SocketError == SocketError.Success) { + accepted = args.AcceptSocket; + args.AcceptSocket = null; + } + + try { + epListener._socket.AcceptAsync (args); + } + catch { + if (accepted != null) + accepted.Close (); + + return; + } + + if (accepted == null) + return; + + HttpConnection conn = null; + try { + conn = new HttpConnection ( + accepted, epListener, epListener._secure, epListener._cert); + lock (((ICollection) epListener._unregistered).SyncRoot) { + epListener._unregistered [conn] = conn; + } + + conn.BeginReadRequest (); + } + catch { + if (conn != null) { + conn.Close (true); + return; + } + + accepted.Close (); + } + } + + private static bool removeSpecial ( + List prefixes, ListenerPrefix prefix) + { + if (prefixes == null) + return false; + + var count = prefixes.Count; + for (int i = 0; i < count; i++) { + if (prefixes [i].Path == prefix.Path) { + prefixes.RemoveAt (i); + return true; + } + } + + return false; + } + + private HttpListener searchListener (Uri uri, out ListenerPrefix prefix) + { + prefix = null; + if (uri == null) + return null; + + var host = uri.Host; + var port = uri.Port; + var path = HttpUtility.UrlDecode (uri.AbsolutePath); + var pathSlash = path [path.Length - 1] == '/' ? path : path + "/"; + HttpListener bestMatch = null; + var bestLength = -1; + if (host != null && host.Length > 0) { + foreach (var p in _prefixes.Keys) { + var ppath = p.Path; + if (ppath.Length < bestLength) + continue; + + if (p.Host != host || p.Port != port) + continue; + + if (path.StartsWith (ppath) || pathSlash.StartsWith (ppath)) { + bestLength = ppath.Length; + bestMatch = _prefixes [p]; + prefix = p; + } + } + + if (bestLength != -1) + return bestMatch; + } + + var list = _unhandled; + bestMatch = matchFromList (host, path, list, out prefix); + if (path != pathSlash && bestMatch == null) + bestMatch = matchFromList (host, pathSlash, list, out prefix); + + if (bestMatch != null) + return bestMatch; + + list = _all; + bestMatch = matchFromList (host, path, list, out prefix); + if (path != pathSlash && bestMatch == null) + bestMatch = matchFromList (host, pathSlash, list, out prefix); + + if (bestMatch != null) + return bestMatch; + + return null; + } + + #endregion + + #region Internal Methods + + internal static bool CertificateExists (int port, string certFolderPath) + { + var cer = Path.Combine (certFolderPath, String.Format ("{0}.cer", port)); + var key = Path.Combine (certFolderPath, String.Format ("{0}.key", port)); + + return File.Exists (cer) && File.Exists (key); + } + + internal void RemoveConnection (HttpConnection connection) + { + lock (((ICollection) _unregistered).SyncRoot) + _unregistered.Remove (connection); + } + + #endregion + + #region Public Methods + + public void AddPrefix (ListenerPrefix prefix, HttpListener listener) + { + List current, future; + if (prefix.Host == "*") { + do { + current = _unhandled; + future = current != null + ? new List (current) + : new List (); + + prefix.Listener = listener; + addSpecial (future, prefix); + } + while (Interlocked.CompareExchange (ref _unhandled, future, current) != current); + + return; + } + + if (prefix.Host == "+") { + do { + current = _all; + future = current != null + ? new List (current) + : new List (); + + prefix.Listener = listener; + addSpecial (future, prefix); + } + while (Interlocked.CompareExchange (ref _all, future, current) != current); + + return; + } + + Dictionary prefs, p2; + do { + prefs = _prefixes; + if (prefs.ContainsKey (prefix)) { + var other = prefs [prefix]; + if (other != listener) // TODO: code. + throw new HttpListenerException ( + 400, "There's another listener for " + prefix); + + return; + } + + p2 = new Dictionary (prefs); + p2 [prefix] = listener; + } + while (Interlocked.CompareExchange (ref _prefixes, p2, prefs) != prefs); + } + + public bool BindContext (HttpListenerContext context) + { + var req = context.Request; + ListenerPrefix prefix; + var listener = searchListener (req.Url, out prefix); + if (listener == null) + return false; + + context.Listener = listener; + context.Connection.Prefix = prefix; + + return true; + } + + public void Close () + { + _socket.Close (); + lock (((ICollection) _unregistered).SyncRoot) { + var copy = new Dictionary (_unregistered); + foreach (var conn in copy.Keys) + conn.Close (true); + + copy.Clear (); + _unregistered.Clear (); + } + } + + public void RemovePrefix (ListenerPrefix prefix, HttpListener listener) + { + List current, future; + if (prefix.Host == "*") { + do { + current = _unhandled; + future = current != null + ? new List (current) + : new List (); + + if (!removeSpecial (future, prefix)) + break; // Prefix not found. + } + while (Interlocked.CompareExchange (ref _unhandled, future, current) != current); + + checkIfRemove (); + return; + } + + if (prefix.Host == "+") { + do { + current = _all; + future = current != null + ? new List (current) + : new List (); + + if (!removeSpecial (future, prefix)) + break; // Prefix not found. + } + while (Interlocked.CompareExchange (ref _all, future, current) != current); + + checkIfRemove (); + return; + } + + Dictionary prefs, p2; + do { + prefs = _prefixes; + if (!prefs.ContainsKey (prefix)) + break; + + p2 = new Dictionary (prefs); + p2.Remove (prefix); + } + while (Interlocked.CompareExchange (ref _prefixes, p2, prefs) != prefs); + + checkIfRemove (); + } + + public void UnbindContext (HttpListenerContext context) + { + if (context == null || context.Listener == null) + return; + + context.Listener.UnregisterContext (context); + } + + #endregion + } } diff --git a/websocket-sharp/Net/HttpBasicIdentity.cs b/websocket-sharp/Net/HttpBasicIdentity.cs new file mode 100644 index 00000000..2dfbea36 --- /dev/null +++ b/websocket-sharp/Net/HttpBasicIdentity.cs @@ -0,0 +1,81 @@ +#region License +/* + * HttpBasicIdentity.cs + * + * This code is derived from System.Net.HttpListenerBasicIdentity.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2014 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * Gonzalo Paniagua Javier + */ +#endregion + +using System.Security.Principal; + +namespace WebSocketSharp.Net +{ + /// + /// Holds the user name and password from an HTTP Basic authentication request. + /// + public class HttpBasicIdentity : GenericIdentity + { + #region Private Fields + + private string _password; + + #endregion + + #region internal Constructors + + internal HttpBasicIdentity (string username, string password) + : base (username, "Basic") + { + _password = password; + } + + #endregion + + #region Public Properties + + /// + /// Gets the password from an HTTP Basic authentication request. + /// + /// + /// A that represents the password. + /// + public virtual string Password { + get { + return _password; + } + } + + #endregion + } +} diff --git a/websocket-sharp/Net/HttpConnection.cs b/websocket-sharp/Net/HttpConnection.cs index 2578f4bb..02cb0271 100644 --- a/websocket-sharp/Net/HttpConnection.cs +++ b/websocket-sharp/Net/HttpConnection.cs @@ -1,33 +1,40 @@ #region License -// -// HttpConnection.cs -// Copied from System.Net.HttpConnection.cs -// -// Author: -// Gonzalo Paniagua Javier (gonzalo@novell.com) -// -// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) -// Copyright (c) 2012-2013 sta.blockhead -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// +/* + * HttpConnection.cs + * + * This code is derived from System.Net.HttpConnection.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2013 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * Gonzalo Paniagua Javier + */ #endregion using System; @@ -43,471 +50,472 @@ using WebSocketSharp.Net.Security; namespace WebSocketSharp.Net { - internal sealed class HttpConnection - { - #region Enums - - enum InputState { - RequestLine, - Headers - } - - enum LineState { - None, - CR, - LF - } - - #endregion - - #region Private Const Field - - private const int BufferSize = 8192; - - #endregion - - #region Private Fields - - private byte [] _buffer; - private bool _chunked; - private HttpListenerContext _context; - private bool _contextWasBound; - private StringBuilder _currentLine; - private EndPointListener _epListener; - private InputState _inputState; - private RequestStream _inputStream; - private HttpListener _lastListener; - private LineState _lineState; - private ResponseStream _outputStream; - private int _position; - private ListenerPrefix _prefix; - private MemoryStream _requestBuffer; - private int _reuses; - private bool _secure; - private Socket _socket; - private Stream _stream; - private int _timeout; - private Timer _timer; - - #endregion - - #region Public Constructors - - public HttpConnection ( - Socket socket, - EndPointListener listener, - bool secure, - X509Certificate2 cert - ) - { - _socket = socket; - _epListener = listener; - _secure = secure; - - var netStream = new NetworkStream (socket, false); - if (!secure) - { - _stream = netStream; - } - else - { - var sslStream = new SslStream (netStream, false); - sslStream.AuthenticateAsServer (cert); - _stream = sslStream; - } - - _timer = new Timer (OnTimeout, null, Timeout.Infinite, Timeout.Infinite); - Init (); - } - - #endregion - - #region Public Properties - - public bool IsClosed { - get { - return _socket == null; - } - } - - public bool IsSecure { - get { - return _secure; - } - } - - public IPEndPoint LocalEndPoint { - get { - return (IPEndPoint) _socket.LocalEndPoint; - } - } - - public ListenerPrefix Prefix { - get { - return _prefix; - } - - set { - _prefix = value; - } - } - - public IPEndPoint RemoteEndPoint { - get { - return (IPEndPoint) _socket.RemoteEndPoint; - } - } - - public int Reuses { - get { - return _reuses; - } - } - - public Stream Stream { - get { - return _stream; - } - } - - #endregion - - #region Private Methods - - private void CloseSocket () - { - if (_socket == null) - return; - - try { - _socket.Close (); - } - catch { - } - finally { - _socket = null; - } - - RemoveConnection (); - } - - private void Init () - { - _chunked = false; - _context = new HttpListenerContext (this); - _contextWasBound = false; - _inputState = InputState.RequestLine; - _inputStream = null; - _lineState = LineState.None; - _outputStream = null; - _position = 0; - _prefix = null; - _requestBuffer = new MemoryStream (); - _timeout = 90000; // 90k ms for first request, 15k ms from then on. - } - - private static void OnRead (IAsyncResult asyncResult) - { - var conn = (HttpConnection) asyncResult.AsyncState; - conn.OnReadInternal (asyncResult); - } - - private void OnReadInternal (IAsyncResult asyncResult) - { - _timer.Change (Timeout.Infinite, Timeout.Infinite); - var nread = -1; - try { - nread = _stream.EndRead (asyncResult); - _requestBuffer.Write (_buffer, 0, nread); - if (_requestBuffer.Length > 32768) { - SendError (); - Close (true); - - return; - } - } catch { - if (_requestBuffer != null && _requestBuffer.Length > 0) - SendError (); - - if (_socket != null) { - CloseSocket (); - Unbind (); - } - - return; - } - - if (nread == 0) { - //if (_requestBuffer.Length > 0) - // SendError (); // Why bother? - CloseSocket (); - Unbind (); - - return; - } - - if (ProcessInput (_requestBuffer.GetBuffer ())) { - if (!_context.HaveError) - _context.Request.FinishInitialization (); - - if (_context.HaveError) { - SendError (); - Close (true); - - return; - } - - if (!_epListener.BindContext (_context)) { - SendError ("Invalid host", 400); - Close (true); - - return; - } - - var listener = _context.Listener; - if (_lastListener != listener) { - RemoveConnection (); - listener.AddConnection (this); - _lastListener = listener; - } - - listener.RegisterContext (_context); - _contextWasBound = true; - - return; - } - - _stream.BeginRead (_buffer, 0, BufferSize, OnRead, this); - } - - private void OnTimeout (object unused) - { - CloseSocket (); - Unbind (); - } - - // true -> Done processing. - // false -> Need more input. - private bool ProcessInput (byte [] data) - { - var length = data.Length; - var used = 0; - string line; - try { - while ((line = ReadLine (data, _position, length - _position, ref used)) != null) { - _position += used; - if (line.Length == 0) { - if (_inputState == InputState.RequestLine) - continue; - - _currentLine = null; - return true; - } - - if (_inputState == InputState.RequestLine) { - _context.Request.SetRequestLine (line); - _inputState = InputState.Headers; - } else { - _context.Request.AddHeader (line); - } - - if (_context.HaveError) - return true; - } - } catch (Exception e) { - _context.ErrorMessage = e.Message; - return true; - } - - _position += used; - if (used == length) { - _requestBuffer.SetLength (0); - _position = 0; - } - - return false; - } - - private string ReadLine (byte [] buffer, int offset, int length, ref int used) - { - if (_currentLine == null) - _currentLine = new StringBuilder (); - - var last = offset + length; - used = 0; - for (int i = offset; i < last && _lineState != LineState.LF; i++) { - used++; - var b = buffer [i]; - if (b == 13) { - _lineState = LineState.CR; - } - else if (b == 10) { - _lineState = LineState.LF; - } - else { - _currentLine.Append ((char) b); - } - } - - string result = null; - if (_lineState == LineState.LF) { - _lineState = LineState.None; - result = _currentLine.ToString (); - _currentLine.Length = 0; - } - - return result; - } - - private void RemoveConnection () - { - if (_lastListener == null) - _epListener.RemoveConnection (this); - else - _lastListener.RemoveConnection (this); - } - - private void Unbind () - { - if (_contextWasBound) { - _epListener.UnbindContext (_context); - _contextWasBound = false; - } - } - - #endregion - - #region Internal Method - - internal void Close (bool force) - { - if (_socket != null) { - if (_outputStream != null) { - _outputStream.Close (); - _outputStream = null; - } - - force |= !_context.Request.KeepAlive; - if (!force) - force = _context.Response.Headers ["Connection"] == "close"; - - if (!force && _context.Request.FlushInput ()) { - if (_chunked && !_context.Response.ForceCloseChunked) { - // Don't close. Keep working. - _reuses++; - Unbind (); - Init (); - BeginReadRequest (); - - return; - } - -// _reuses++; -// Unbind (); -// Init (); -// BeginReadRequest (); -// -// return; - } - - var socket = _socket; - _socket = null; - try { - if (socket != null) - socket.Shutdown (SocketShutdown.Both); - } catch { - } finally { - if (socket != null) - socket.Close (); - } - - Unbind (); - RemoveConnection (); - - return; - } - } - - #endregion - - #region Public Methods - - public void BeginReadRequest () - { - if (_buffer == null) - _buffer = new byte [BufferSize]; - - try { - if (_reuses == 1) - _timeout = 15000; - - _timer.Change (_timeout, Timeout.Infinite); - _stream.BeginRead (_buffer, 0, BufferSize, OnRead, this); - } catch { - _timer.Change (Timeout.Infinite, Timeout.Infinite); - CloseSocket (); - Unbind (); - } - } - - public void Close () - { - Close (false); - } - - public RequestStream GetRequestStream (bool chunked, long contentlength) - { - if (_inputStream == null) { - var buffer = _requestBuffer.GetBuffer (); - var length = buffer.Length; - _requestBuffer = null; - if (chunked) { - _chunked = true; - _context.Response.SendChunked = true; - _inputStream = new ChunkedInputStream (_context, _stream, buffer, _position, length - _position); - } else { - _inputStream = new RequestStream (_stream, buffer, _position, length - _position, contentlength); - } - } - - return _inputStream; - } - - public ResponseStream GetResponseStream () - { - // TODO: Can we get this stream before reading the input? - if (_outputStream == null) { - var listener = _context.Listener; - var ignore = listener == null ? true : listener.IgnoreWriteExceptions; - _outputStream = new ResponseStream (_stream, _context.Response, ignore); - } - - return _outputStream; - } - - public void SendError () - { - SendError (_context.ErrorMessage, _context.ErrorStatus); - } - - public void SendError (string message, int status) - { - try { - var response = _context.Response; - response.StatusCode = status; - response.ContentType = "text/html"; - var description = status.GetStatusDescription (); - var error = !message.IsNullOrEmpty () - ? String.Format ("

{0} ({1})

", description, message) - : String.Format ("

{0}

", description); - - var entity = _context.Response.ContentEncoding.GetBytes (error); - response.Close (entity, false); - } catch { - // Response was already closed. - } - } - - #endregion - } + internal sealed class HttpConnection + { + #region Internal Enums + + enum InputState { + RequestLine, + Headers + } + + enum LineState { + None, + CR, + LF + } + + #endregion + + #region Private Const Fields + + private const int _bufferSize = 8192; + + #endregion + + #region Private Fields + + private byte [] _buffer; + private bool _chunked; + private HttpListenerContext _context; + private bool _contextWasBound; + private StringBuilder _currentLine; + private EndPointListener _epListener; + private InputState _inputState; + private RequestStream _inputStream; + private HttpListener _lastListener; + private LineState _lineState; + private ResponseStream _outputStream; + private int _position; + private ListenerPrefix _prefix; + private MemoryStream _requestBuffer; + private int _reuses; + private bool _secure; + private Socket _socket; + private Stream _stream; + private int _timeout; + private Timer _timer; + + #endregion + + #region Public Constructors + + public HttpConnection ( + Socket socket, + EndPointListener listener, + bool secure, + X509Certificate2 cert) + { + _socket = socket; + _epListener = listener; + _secure = secure; + + var netStream = new NetworkStream (socket, false); + if (!secure) + _stream = netStream; + else { + var sslStream = new SslStream (netStream, false); + sslStream.AuthenticateAsServer (cert); + _stream = sslStream; + } + + _timeout = 90000; // 90k ms for first request, 15k ms from then on. + _timer = new Timer (onTimeout, null, Timeout.Infinite, Timeout.Infinite); + init (); + } + + #endregion + + #region Public Properties + + public bool IsClosed { + get { + return _socket == null; + } + } + + public bool IsSecure { + get { + return _secure; + } + } + + public IPEndPoint LocalEndPoint { + get { + return (IPEndPoint) _socket.LocalEndPoint; + } + } + + public ListenerPrefix Prefix { + get { + return _prefix; + } + + set { + _prefix = value; + } + } + + public IPEndPoint RemoteEndPoint { + get { + return (IPEndPoint) _socket.RemoteEndPoint; + } + } + + public int Reuses { + get { + return _reuses; + } + } + + public Stream Stream { + get { + return _stream; + } + } + + #endregion + + #region Private Methods + + private void closeSocket () + { + if (_socket == null) + return; + + try { + _socket.Close (); + } + catch { + } + finally { + _socket = null; + } + + removeConnection (); + } + + private void init () + { + _chunked = false; + _context = new HttpListenerContext (this); + _inputState = InputState.RequestLine; + _inputStream = null; + _lineState = LineState.None; + _outputStream = null; + _position = 0; + _prefix = null; + _requestBuffer = new MemoryStream (); + } + + private static void onRead (IAsyncResult asyncResult) + { + var conn = (HttpConnection) asyncResult.AsyncState; + conn.onReadInternal (asyncResult); + } + + private void onReadInternal (IAsyncResult asyncResult) + { + _timer.Change (Timeout.Infinite, Timeout.Infinite); + + var read = -1; + try { + read = _stream.EndRead (asyncResult); + _requestBuffer.Write (_buffer, 0, read); + if (_requestBuffer.Length > 32768) { + SendError (); + Close (true); + + return; + } + } + catch { + if (_requestBuffer != null && _requestBuffer.Length > 0) + SendError (); + + if (_socket != null) { + closeSocket (); + unbind (); + } + + return; + } + + if (read <= 0) { + closeSocket (); + unbind (); + + return; + } + + if (processInput (_requestBuffer.GetBuffer ())) { + if (!_context.HaveError) + _context.Request.FinishInitialization (); + else { + SendError (); + Close (true); + + return; + } + + if (!_epListener.BindContext (_context)) { + SendError ("Invalid host", 400); + Close (true); + + return; + } + + var listener = _context.Listener; + if (_lastListener != listener) { + removeConnection (); + listener.AddConnection (this); + _lastListener = listener; + } + + _contextWasBound = true; + listener.RegisterContext (_context); + + return; + } + + _stream.BeginRead (_buffer, 0, _bufferSize, onRead, this); + } + + private void onTimeout (object unused) + { + closeSocket (); + unbind (); + } + + // true -> Done processing. + // false -> Need more input. + private bool processInput (byte [] data) + { + var length = data.Length; + var used = 0; + string line; + try { + while ((line = readLine ( + data, _position, length - _position, ref used)) != null) { + _position += used; + if (line.Length == 0) { + if (_inputState == InputState.RequestLine) + continue; + + _currentLine = null; + return true; + } + + if (_inputState == InputState.RequestLine) { + _context.Request.SetRequestLine (line); + _inputState = InputState.Headers; + } + else { + _context.Request.AddHeader (line); + } + + if (_context.HaveError) + return true; + } + } + catch (Exception ex) { + _context.ErrorMessage = ex.Message; + return true; + } + + _position += used; + if (used == length) { + _requestBuffer.SetLength (0); + _position = 0; + } + + return false; + } + + private string readLine ( + byte [] buffer, int offset, int length, ref int used) + { + if (_currentLine == null) + _currentLine = new StringBuilder (); + + var last = offset + length; + used = 0; + for (int i = offset; i < last && _lineState != LineState.LF; i++) { + used++; + var b = buffer [i]; + if (b == 13) { + _lineState = LineState.CR; + } + else if (b == 10) { + _lineState = LineState.LF; + } + else { + _currentLine.Append ((char) b); + } + } + + string result = null; + if (_lineState == LineState.LF) { + _lineState = LineState.None; + result = _currentLine.ToString (); + _currentLine.Length = 0; + } + + return result; + } + + private void removeConnection () + { + if (_lastListener == null) + _epListener.RemoveConnection (this); + else + _lastListener.RemoveConnection (this); + } + + private void unbind () + { + if (_contextWasBound) { + _epListener.UnbindContext (_context); + _contextWasBound = false; + } + } + + #endregion + + #region Internal Methods + + internal void Close (bool force) + { + if (_socket != null) { + if (_outputStream != null) { + _outputStream.Close (); + _outputStream = null; + } + + var req = _context.Request; + var res = _context.Response; + force |= !req.KeepAlive; + if (!force) + force = res.Headers ["Connection"] == "close"; + + if (!force && + req.FlushInput () && + (!_chunked || (_chunked && !res.ForceCloseChunked))) { + // Don't close. Keep working. + _reuses++; + unbind (); + init (); + BeginReadRequest (); + + return; + } + + var socket = _socket; + _socket = null; + try { + socket.Shutdown (SocketShutdown.Both); + } + catch { + } + finally { + if (socket != null) + socket.Close (); + } + + unbind (); + removeConnection (); + + return; + } + } + + #endregion + + #region Public Methods + + public void BeginReadRequest () + { + if (_buffer == null) + _buffer = new byte [_bufferSize]; + + try { + if (_reuses == 1) + _timeout = 15000; + + _timer.Change (_timeout, Timeout.Infinite); + _stream.BeginRead (_buffer, 0, _bufferSize, onRead, this); + } + catch { + _timer.Change (Timeout.Infinite, Timeout.Infinite); + closeSocket (); + unbind (); + } + } + + public void Close () + { + Close (false); + } + + public RequestStream GetRequestStream (bool chunked, long contentlength) + { + if (_inputStream == null) { + var buffer = _requestBuffer.GetBuffer (); + var length = buffer.Length; + + _requestBuffer = null; + if (chunked) { + _chunked = true; + _context.Response.SendChunked = true; + _inputStream = new ChunkedInputStream ( + _context, _stream, buffer, _position, length - _position); + } + else { + _inputStream = new RequestStream ( + _stream, buffer, _position, length - _position, contentlength); + } + } + + return _inputStream; + } + + public ResponseStream GetResponseStream () + { + // TODO: Can we get this stream before reading the input? + if (_outputStream == null) { + var listener = _context.Listener; + var ignore = listener == null ? true : listener.IgnoreWriteExceptions; + _outputStream = new ResponseStream (_stream, _context.Response, ignore); + } + + return _outputStream; + } + + public void SendError () + { + SendError (_context.ErrorMessage, _context.ErrorStatus); + } + + public void SendError (string message, int status) + { + try { + var res = _context.Response; + res.StatusCode = status; + res.ContentType = "text/html"; + + var description = status.GetStatusDescription (); + var error = message != null && message.Length > 0 + ? String.Format ("

{0} ({1})

", description, message) + : String.Format ("

{0}

", description); + + var entity = res.ContentEncoding.GetBytes (error); + res.Close (entity, false); + } + catch { + // Response was already closed. + } + } + + #endregion + } } diff --git a/websocket-sharp/Net/HttpDigestIdentity.cs b/websocket-sharp/Net/HttpDigestIdentity.cs new file mode 100644 index 00000000..7e994f2f --- /dev/null +++ b/websocket-sharp/Net/HttpDigestIdentity.cs @@ -0,0 +1,185 @@ +#region License +/* + * HttpDigestIdentity.cs + * + * The MIT License + * + * Copyright (c) 2014 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.Collections.Specialized; +using System.Security.Principal; + +namespace WebSocketSharp.Net +{ + /// + /// Holds the user name and other authentication parameters from an HTTP Digest + /// authentication request. + /// + public class HttpDigestIdentity : GenericIdentity + { + #region Private Fields + + private NameValueCollection _params; + + #endregion + + #region Internal Constructors + + internal HttpDigestIdentity (NameValueCollection authParams) + : base (authParams ["username"], "Digest") + { + _params = authParams; + } + + #endregion + + #region Public Properties + + /// + /// Gets the algorithm parameter from an HTTP Digest authentication request. + /// + /// + /// A that represents the algorithm parameter. + /// + public string Algorithm { + get { + return _params ["algorithm"]; + } + } + + /// + /// Gets the cnonce parameter from an HTTP Digest authentication request. + /// + /// + /// A that represents the cnonce parameter. + /// + public string Cnonce { + get { + return _params ["cnonce"]; + } + } + + /// + /// Gets the nc parameter from an HTTP Digest authentication request. + /// + /// + /// A that represents the nc parameter. + /// + public string Nc { + get { + return _params ["nc"]; + } + } + + /// + /// Gets the nonce parameter from an HTTP Digest authentication request. + /// + /// + /// A that represents the nonce parameter. + /// + public string Nonce { + get { + return _params ["nonce"]; + } + } + + /// + /// Gets the opaque parameter from an HTTP Digest authentication request. + /// + /// + /// A that represents the opaque parameter. + /// + public string Opaque { + get { + return _params ["opaque"]; + } + } + + /// + /// Gets the qop parameter from an HTTP Digest authentication request. + /// + /// + /// A that represents the qop parameter. + /// + public string Qop { + get { + return _params ["qop"]; + } + } + + /// + /// Gets the realm parameter from an HTTP Digest authentication request. + /// + /// + /// A that represents the realm parameter. + /// + public string Realm { + get { + return _params ["realm"]; + } + } + + /// + /// Gets the response parameter from an HTTP Digest authentication request. + /// + /// + /// A that represents the response parameter. + /// + public string Response { + get { + return _params ["response"]; + } + } + + /// + /// Gets the uri parameter from an HTTP Digest authentication request. + /// + /// + /// A that represents the uri parameter. + /// + public string Uri { + get { + return _params ["uri"]; + } + } + + #endregion + + #region Internal Methods + + internal bool IsValid ( + string password, string realm, string method, string entity) + { + var parameters = new NameValueCollection (_params); + parameters ["password"] = password; + parameters ["realm"] = realm; + parameters ["method"] = method; + parameters ["entity"] = entity; + + return _params ["response"] == HttpUtility.CreateRequestDigest (parameters); + } + + #endregion + } +} diff --git a/websocket-sharp/Net/HttpListener.cs b/websocket-sharp/Net/HttpListener.cs index a886c668..350ef53a 100644 --- a/websocket-sharp/Net/HttpListener.cs +++ b/websocket-sharp/Net/HttpListener.cs @@ -1,653 +1,750 @@ -// -// HttpListener.cs -// Copied from System.Net.HttpListener.cs -// -// Author: -// Gonzalo Paniagua Javier (gonzalo@novell.com) -// sta (sta.blockhead@gmail.com) -// -// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) -// Copyright (c) 2012-2013 sta.blockhead -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// +#region License +/* + * HttpListener.cs + * + * This code is derived from System.Net.HttpListener.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2014 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * Gonzalo Paniagua Javier + */ +#endregion using System; using System.Collections; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; using System.Threading; // TODO: logging -namespace WebSocketSharp.Net { - - /// - /// Provides a simple, programmatically controlled HTTP listener. - /// - public sealed class HttpListener : IDisposable { - - #region Private Fields - - AuthenticationSchemes auth_schemes; - AuthenticationSchemeSelector auth_selector; - string cert_folder_path; - Dictionary connections; - List ctx_queue; - X509Certificate2 default_cert; - bool disposed; - bool ignore_write_exceptions; - bool listening; - HttpListenerPrefixCollection prefixes; - string realm; - Dictionary registry; - bool unsafe_ntlm_auth; - List wait_queue; - - #endregion - - #region Public Constructors - - /// - /// Initializes a new instance of the class. - /// - public HttpListener () - { - prefixes = new HttpListenerPrefixCollection (this); - registry = new Dictionary (); - connections = new Dictionary (); - ctx_queue = new List (); - wait_queue = new List (); - auth_schemes = AuthenticationSchemes.Anonymous; - } - - #endregion - - #region Public Properties - - /// - /// Gets or sets the scheme used to authenticate the clients. - /// - /// - /// One of the values that indicates - /// the scheme used to authenticate the clients. - /// The default value is . - /// - /// - /// This object has been closed. - /// - public AuthenticationSchemes AuthenticationSchemes { - // TODO: Digest, NTLM and Negotiate require ControlPrincipal - get { - CheckDisposed (); - return auth_schemes; - } - set { - CheckDisposed (); - auth_schemes = value; - } - } - - /// - /// Gets or sets the delegate called to determine the scheme used to authenticate clients. - /// - /// - /// A delegate that invokes the method(s) used to select - /// an authentication scheme. The default value is . - /// - /// - /// This object has been closed. - /// - public AuthenticationSchemeSelector AuthenticationSchemeSelectorDelegate { - get { - CheckDisposed (); - return auth_selector; - } - set { - CheckDisposed (); - auth_selector = value; - } - } - - /// - /// Gets or sets the path to the folder stored the certificate files used to authenticate - /// the server on the secure connection. - /// - /// - /// This property represents the path to the folder stored the certificate files associated with - /// the port number of each added URI prefix. A set of the certificate files is a pair of the - /// 'port number'.cer (DER) and 'port number'.key (DER, RSA Private Key). - /// - /// - /// A that contains the path to the certificate folder. The default value is - /// the result of Environment.GetFolderPath (). - /// - /// - /// This object has been closed. - /// - public string CertificateFolderPath { - get { - CheckDisposed (); - if (cert_folder_path.IsNullOrEmpty ()) - cert_folder_path = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData); - - return cert_folder_path; - } - - set { - CheckDisposed (); - cert_folder_path = value; - } - } - - /// - /// Gets or sets the default certificate used to authenticate the server on the secure connection. - /// - /// - /// A used to authenticate the server if the certificate associated with - /// the port number of each added URI prefix is not found in the . - /// - /// - /// This object has been closed. - /// - public X509Certificate2 DefaultCertificate { - get { - CheckDisposed (); - return default_cert; - } - - set { - CheckDisposed (); - default_cert = value; - } - } - - /// - /// Gets or sets a value indicating whether the returns exceptions - /// that occur when sending the response to the client. - /// - /// - /// true if does not return exceptions that occur when sending the response to the client; - /// otherwise, false. The default value is false. - /// - /// - /// This object has been closed. - /// - public bool IgnoreWriteExceptions { - get { - CheckDisposed (); - return ignore_write_exceptions; - } - set { - CheckDisposed (); - ignore_write_exceptions = value; - } - } - - /// - /// Gets a value indicating whether the has been started. - /// - /// - /// true if the has been started; otherwise, false. - /// - public bool IsListening { - get { return listening; } - } - - /// - /// Gets a value indicating whether the can be used with the current operating system. - /// - /// - /// true. - /// - public static bool IsSupported { - get { return true; } - } - - /// - /// Gets the URI prefixes handled by the . - /// - /// - /// A that contains the URI prefixes. - /// - /// - /// This object has been closed. - /// - public HttpListenerPrefixCollection Prefixes { - get { - CheckDisposed (); - return prefixes; - } - } - - /// - /// Gets or sets the name of the realm associated with the . - /// - /// - /// A that contains the name of the realm. - /// - /// - /// This object has been closed. - /// - public string Realm { - // TODO: Use this - get { - CheckDisposed (); - return realm; - } - set { - CheckDisposed (); - realm = value; - } - } - - /// - /// Gets or sets a value indicating whether, when NTLM authentication is used, - /// the authentication information of first request is used to authenticate - /// additional requests on the same connection. - /// - /// - /// true if the authentication information of first request is used; - /// otherwise, false. The default value is false. - /// - /// - /// This object has been closed. - /// - public bool UnsafeConnectionNtlmAuthentication { - // TODO: Support for NTLM needs some loving. - get { - CheckDisposed (); - return unsafe_ntlm_auth; - } - set { - CheckDisposed (); - unsafe_ntlm_auth = value; - } - } - - #endregion - - #region Private Methods - - void Cleanup (bool force) - { - lock (((ICollection)registry).SyncRoot) { - if (!force) - SendServiceUnavailable (); - - CleanupContextRegistry (); - CleanupConnections (); - CleanupWaitQueue (); - } - } - - void CleanupConnections () - { - lock (((ICollection)connections).SyncRoot) { - if (connections.Count == 0) - return; - - // Need to copy this since closing will call RemoveConnection - ICollection keys = connections.Keys; - var conns = new HttpConnection [keys.Count]; - keys.CopyTo (conns, 0); - connections.Clear (); - for (int i = conns.Length - 1; i >= 0; i--) - conns [i].Close (true); - } - } - - void CleanupContextRegistry () - { - lock (((ICollection)registry).SyncRoot) { - if (registry.Count == 0) - return; - - // Need to copy this since closing will call UnregisterContext - ICollection keys = registry.Keys; - var all = new HttpListenerContext [keys.Count]; - keys.CopyTo (all, 0); - registry.Clear (); - for (int i = all.Length - 1; i >= 0; i--) - all [i].Connection.Close (true); - } - } - - void CleanupWaitQueue () - { - lock (((ICollection)wait_queue).SyncRoot) { - if (wait_queue.Count == 0) - return; - - var exc = new ObjectDisposedException (GetType ().ToString ()); - foreach (var ares in wait_queue) { - ares.Complete (exc); - } - - wait_queue.Clear (); - } - } - - void Close (bool force) - { - EndPointManager.RemoveListener (this); - Cleanup (force); - } - - // Must be called with a lock on ctx_queue - HttpListenerContext GetContextFromQueue () - { - if (ctx_queue.Count == 0) - return null; - - var context = ctx_queue [0]; - ctx_queue.RemoveAt (0); - return context; - } - - void SendServiceUnavailable () - { - lock (((ICollection)ctx_queue).SyncRoot) { - if (ctx_queue.Count == 0) - return; - - var ctxs = ctx_queue.ToArray (); - ctx_queue.Clear (); - foreach (var ctx in ctxs) { - var res = ctx.Response; - res.StatusCode = (int)HttpStatusCode.ServiceUnavailable; - res.Close(); - } - } - } - - #endregion - - #region Internal Methods - - internal void AddConnection (HttpConnection cnc) - { - connections [cnc] = cnc; - } - - internal void CheckDisposed () - { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); - } - - internal void RegisterContext (HttpListenerContext context) - { - lock (((ICollection)registry).SyncRoot) - registry [context] = context; - - ListenerAsyncResult ares = null; - lock (((ICollection)wait_queue).SyncRoot) { - if (wait_queue.Count == 0) { - lock (((ICollection)ctx_queue).SyncRoot) - ctx_queue.Add (context); - } else { - ares = wait_queue [0]; - wait_queue.RemoveAt (0); - } - } - - if (ares != null) - ares.Complete (context); - } - - internal void RemoveConnection (HttpConnection cnc) - { - connections.Remove (cnc); - } - - internal AuthenticationSchemes SelectAuthenticationScheme (HttpListenerContext context) - { - if (AuthenticationSchemeSelectorDelegate != null) - return AuthenticationSchemeSelectorDelegate (context.Request); - else - return auth_schemes; - } - - internal void UnregisterContext (HttpListenerContext context) - { - lock (((ICollection)registry).SyncRoot) - registry.Remove (context); - - lock (((ICollection)ctx_queue).SyncRoot) { - int idx = ctx_queue.IndexOf (context); - if (idx >= 0) - ctx_queue.RemoveAt (idx); - } - } - - #endregion - - #region Public Methods - - /// - /// Shuts down the immediately. - /// - public void Abort () - { - if (disposed) - return; - - Close (true); - disposed = true; - } - - /// - /// Begins getting an incoming request information asynchronously. - /// - /// - /// This asynchronous operation must be completed by calling the method. - /// Typically, the method is invoked by the delegate. - /// - /// - /// An that contains the status of the asynchronous operation. - /// - /// - /// An delegate that references the method(s) - /// called when the asynchronous operation completes. - /// - /// - /// An that contains a user defined object to pass to the delegate. - /// - /// - /// This object has been closed. - /// - /// - /// The has not been started or is stopped currently. - /// - public IAsyncResult BeginGetContext (AsyncCallback callback, Object state) - { - CheckDisposed (); - if (!listening) - throw new InvalidOperationException ("Please, call Start before using this method."); - - ListenerAsyncResult ares = new ListenerAsyncResult (callback, state); - - // lock wait_queue early to avoid race conditions - lock (((ICollection)wait_queue).SyncRoot) { - lock (((ICollection)ctx_queue).SyncRoot) { - HttpListenerContext ctx = GetContextFromQueue (); - if (ctx != null) { - ares.Complete (ctx, true); - return ares; - } - } - - wait_queue.Add (ares); - } - - return ares; - } - - /// - /// Shuts down the . - /// - public void Close () - { - if (disposed) - return; - - Close (false); - disposed = true; - } - - /// - /// Ends an asynchronous operation to get an incoming request information. - /// - /// - /// This method completes an asynchronous operation started by calling the method. - /// - /// - /// A that contains a client's request information. - /// - /// - /// An obtained by calling the method. - /// - /// - /// This object has been closed. - /// - /// - /// is . - /// - /// - /// was not obtained by calling the method. - /// - /// - /// The EndGetContext method was already called for the specified . - /// - public HttpListenerContext EndGetContext (IAsyncResult asyncResult) - { - CheckDisposed (); - if (asyncResult == null) - throw new ArgumentNullException ("asyncResult"); - - ListenerAsyncResult ares = asyncResult as ListenerAsyncResult; - if (ares == null) - throw new ArgumentException ("Wrong IAsyncResult.", "asyncResult"); - - if (ares.EndCalled) - throw new InvalidOperationException ("Cannot reuse this IAsyncResult."); - ares.EndCalled = true; - - if (!ares.IsCompleted) - ares.AsyncWaitHandle.WaitOne (); - - lock (((ICollection)wait_queue).SyncRoot) { - int idx = wait_queue.IndexOf (ares); - if (idx >= 0) - wait_queue.RemoveAt (idx); - } - - HttpListenerContext context = ares.GetContext (); - context.ParseAuthentication (SelectAuthenticationScheme (context)); - return context; // This will throw on error. - } - - /// - /// Gets an incoming request information. - /// - /// - /// This method waits for an incoming request and returns the request information - /// when received the request. - /// - /// - /// A that contains a client's request information. - /// - /// - /// - /// The does not have any URI prefixes to listen on. - /// - /// - /// -or- - /// - /// - /// The has not been started or is stopped currently. - /// - /// - /// - /// This object has been closed. - /// - public HttpListenerContext GetContext () - { - // The prefixes are not checked when using the async interface!? - if (prefixes.Count == 0) - throw new InvalidOperationException ("Please, call AddPrefix before using this method."); - - ListenerAsyncResult ares = (ListenerAsyncResult) BeginGetContext (null, null); - ares.InGet = true; - return EndGetContext (ares); - } - - /// - /// Starts to receive incoming requests. - /// - /// - /// This object has been closed. - /// - public void Start () - { - CheckDisposed (); - if (listening) - return; - - EndPointManager.AddListener (this); - listening = true; - } - - /// - /// Stops receiving incoming requests. - /// - /// - /// This object has been closed. - /// - public void Stop () - { - CheckDisposed (); - if (!listening) - return; - - listening = false; - EndPointManager.RemoveListener (this); - SendServiceUnavailable (); - } - - #endregion - - #region Explicit Interface Implementation - - /// - /// Releases all resource used by the . - /// - void IDisposable.Dispose () - { - if (disposed) - return; - - Close (true); // TODO: Should we force here or not? - disposed = true; - } - - #endregion - } +namespace WebSocketSharp.Net +{ + /// + /// Provides a simple, programmatically controlled HTTP listener. + /// + public sealed class HttpListener : IDisposable + { + #region Private Fields + + private AuthenticationSchemes _authSchemes; + private AuthenticationSchemeSelector _authSchemeSelector; + private string _certFolderPath; + private Dictionary _connections; + private List _contextQueue; + private Func _credentialsFinder; + private X509Certificate2 _defaultCert; + private bool _disposed; + private bool _ignoreWriteExceptions; + private bool _listening; + private HttpListenerPrefixCollection _prefixes; + private string _realm; + private Dictionary _registry; + private List _waitQueue; + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + public HttpListener () + { + _authSchemes = AuthenticationSchemes.Anonymous; + _connections = new Dictionary (); + _contextQueue = new List (); + _prefixes = new HttpListenerPrefixCollection (this); + _registry = new Dictionary (); + _waitQueue = new List (); + } + + #endregion + + #region Internal Properties + + internal bool IsDisposed { + get { + return _disposed; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets the scheme used to authenticate the clients. + /// + /// + /// One of the values + /// that indicates the scheme used to authenticate the clients. The default + /// value is . + /// + /// + /// This object has been closed. + /// + public AuthenticationSchemes AuthenticationSchemes { + get { + CheckDisposed (); + return _authSchemes; + } + + set { + CheckDisposed (); + _authSchemes = value; + } + } + + /// + /// Gets or sets the delegate called to determine the scheme used to + /// authenticate clients. + /// + /// + /// A delegate that invokes the + /// method(s) used to select an authentication scheme. The default value is + /// . + /// + /// + /// This object has been closed. + /// + public AuthenticationSchemeSelector AuthenticationSchemeSelectorDelegate { + get { + CheckDisposed (); + return _authSchemeSelector; + } + + set { + CheckDisposed (); + _authSchemeSelector = value; + } + } + + /// + /// Gets or sets the path to the folder stored the certificate files used to + /// authenticate the server on the secure connection. + /// + /// + /// This property represents the path to the folder stored the certificate + /// files associated with the port number of each added URI prefix. A set of + /// the certificate files is a pair of the 'port number'.cer (DER) and + /// 'port number'.key (DER, RSA Private Key). + /// + /// + /// A that contains the path to the certificate folder. + /// The default value is the result of Environment.GetFolderPath + /// (). + /// + /// + /// This object has been closed. + /// + public string CertificateFolderPath { + get { + CheckDisposed (); + return _certFolderPath == null || _certFolderPath.Length == 0 + ? (_certFolderPath = Environment.GetFolderPath ( + Environment.SpecialFolder.ApplicationData)) + : _certFolderPath; + } + + set { + CheckDisposed (); + _certFolderPath = value; + } + } + + /// + /// Gets or sets the default certificate used to authenticate the server on + /// the secure connection. + /// + /// + /// A used to authenticate the server if the + /// certificate associated with the port number of each added URI prefix is + /// not found in the . + /// + /// + /// This object has been closed. + /// + public X509Certificate2 DefaultCertificate { + get { + CheckDisposed (); + return _defaultCert; + } + + set { + CheckDisposed (); + _defaultCert = value; + } + } + + /// + /// Gets or sets a value indicating whether the + /// returns exceptions that occur when sending the response to the client. + /// + /// + /// true if the doesn't return exceptions + /// that occur when sending the response to the client; otherwise, + /// false. The default value is false. + /// + /// + /// This object has been closed. + /// + public bool IgnoreWriteExceptions { + get { + CheckDisposed (); + return _ignoreWriteExceptions; + } + + set { + CheckDisposed (); + _ignoreWriteExceptions = value; + } + } + + /// + /// Gets a value indicating whether the has been + /// started. + /// + /// + /// true if the has been started; otherwise, + /// false. + /// + public bool IsListening { + get { + return _listening; + } + } + + /// + /// Gets a value indicating whether the can be + /// used with the current operating system. + /// + /// + /// true. + /// + public static bool IsSupported { + get { + return true; + } + } + + /// + /// Gets the URI prefixes handled by the . + /// + /// + /// A that contains the URI + /// prefixes. + /// + /// + /// This object has been closed. + /// + public HttpListenerPrefixCollection Prefixes { + get { + CheckDisposed (); + return _prefixes; + } + } + + /// + /// Gets or sets the name of the realm associated with the + /// . + /// + /// + /// A that contains the name of the realm. The default + /// value is SECRET AREA. + /// + /// + /// This object has been closed. + /// + public string Realm { + get { + CheckDisposed (); + return _realm == null || _realm.Length == 0 + ? (_realm = "SECRET AREA") + : _realm; + } + + set { + CheckDisposed (); + _realm = value; + } + } + + /// + /// Gets or sets a value indicating whether, when NTLM authentication is used, + /// the authentication information of first request is used to authenticate + /// additional requests on the same connection. + /// + /// + /// This property isn't currently supported and always throws + /// a . + /// + /// + /// true if the authentication information of first request is used; + /// otherwise, false. + /// + /// + /// Any use of this property. + /// + public bool UnsafeConnectionNtlmAuthentication { + get { + throw new NotSupportedException (); + } + + set { + throw new NotSupportedException (); + } + } + + /// + /// Gets or sets the delegate called to find the credentials for an identity + /// used to authenticate a client. + /// + /// + /// A Func<, > + /// delegate that references the method(s) used to find the credentials. The + /// default value is a function that only returns . + /// + /// + /// This object has been closed. + /// + public Func UserCredentialsFinder { + get { + CheckDisposed (); + return _credentialsFinder ?? (_credentialsFinder = identity => null); + } + + set { + CheckDisposed (); + _credentialsFinder = value; + } + } + + #endregion + + #region Private Methods + + private void cleanup (bool force) + { + lock (((ICollection) _registry).SyncRoot) { + if (!force) + sendServiceUnavailable (); + + cleanupContextRegistry (); + cleanupConnections (); + cleanupWaitQueue (); + } + } + + private void cleanupConnections () + { + lock (((ICollection) _connections).SyncRoot) { + if (_connections.Count == 0) + return; + + // Need to copy this since closing will call RemoveConnection + var keys = _connections.Keys; + var conns = new HttpConnection [keys.Count]; + keys.CopyTo (conns, 0); + _connections.Clear (); + for (var i = conns.Length - 1; i >= 0; i--) + conns [i].Close (true); + } + } + + private void cleanupContextRegistry () + { + lock (((ICollection) _registry).SyncRoot) { + if (_registry.Count == 0) + return; + + // Need to copy this since closing will call UnregisterContext + var keys = _registry.Keys; + var all = new HttpListenerContext [keys.Count]; + keys.CopyTo (all, 0); + _registry.Clear (); + for (var i = all.Length - 1; i >= 0; i--) + all [i].Connection.Close (true); + } + } + + private void cleanupWaitQueue () + { + lock (((ICollection) _waitQueue).SyncRoot) { + if (_waitQueue.Count == 0) + return; + + var ex = new ObjectDisposedException (GetType ().ToString ()); + foreach (var ares in _waitQueue) { + ares.Complete (ex); + } + + _waitQueue.Clear (); + } + } + + private void close (bool force) + { + EndPointManager.RemoveListener (this); + cleanup (force); + } + + // Must be called with a lock on _contextQueue + private HttpListenerContext getContextFromQueue () + { + if (_contextQueue.Count == 0) + return null; + + var context = _contextQueue [0]; + _contextQueue.RemoveAt (0); + + return context; + } + + private void sendServiceUnavailable () + { + lock (((ICollection) _contextQueue).SyncRoot) { + if (_contextQueue.Count == 0) + return; + + var contexts = _contextQueue.ToArray (); + _contextQueue.Clear (); + foreach (var context in contexts) { + var res = context.Response; + res.StatusCode = (int) HttpStatusCode.ServiceUnavailable; + res.Close (); + } + } + } + + #endregion + + #region Internal Methods + + internal void AddConnection (HttpConnection connection) + { + _connections [connection] = connection; + } + + internal ListenerAsyncResult BeginGetContext (ListenerAsyncResult asyncResult) + { + CheckDisposed (); + if (_prefixes.Count == 0) + throw new InvalidOperationException ( + "Please, call AddPrefix before using this method."); + + if (!_listening) + throw new InvalidOperationException ( + "Please, call Start before using this method."); + + // Lock _waitQueue early to avoid race conditions + lock (((ICollection) _waitQueue).SyncRoot) { + lock (((ICollection) _contextQueue).SyncRoot) { + var context = getContextFromQueue (); + if (context != null) { + asyncResult.Complete (context, true); + return asyncResult; + } + } + + _waitQueue.Add (asyncResult); + } + + return asyncResult; + } + + internal void CheckDisposed () + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + } + + internal void RegisterContext (HttpListenerContext context) + { + lock (((ICollection) _registry).SyncRoot) + _registry [context] = context; + + ListenerAsyncResult ares = null; + lock (((ICollection) _waitQueue).SyncRoot) { + if (_waitQueue.Count == 0) { + lock (((ICollection) _contextQueue).SyncRoot) + _contextQueue.Add (context); + } + else { + ares = _waitQueue [0]; + _waitQueue.RemoveAt (0); + } + } + + if (ares != null) + ares.Complete (context); + } + + internal void RemoveConnection (HttpConnection connection) + { + _connections.Remove (connection); + } + + internal AuthenticationSchemes SelectAuthenticationScheme ( + HttpListenerContext context) + { + return AuthenticationSchemeSelectorDelegate != null + ? AuthenticationSchemeSelectorDelegate (context.Request) + : _authSchemes; + } + + internal void UnregisterContext (HttpListenerContext context) + { + lock (((ICollection) _registry).SyncRoot) + _registry.Remove (context); + + lock (((ICollection) _contextQueue).SyncRoot) { + var i = _contextQueue.IndexOf (context); + if (i >= 0) + _contextQueue.RemoveAt (i); + } + } + + #endregion + + #region Public Methods + + /// + /// Shuts down the immediately. + /// + public void Abort () + { + if (_disposed) + return; + + close (true); + _disposed = true; + } + + /// + /// Begins getting an incoming request information asynchronously. + /// + /// + /// This asynchronous operation must be completed by calling the + /// EndGetContext method. Typically, that method is invoked by the + /// delegate. + /// + /// + /// An that contains the status of the + /// asynchronous operation. + /// + /// + /// An delegate that references the method(s) + /// called when the asynchronous operation completes. + /// + /// + /// An that contains a user defined object to pass to + /// the delegate. + /// + /// + /// + /// The does not have any URI prefixes to listen + /// on. + /// + /// + /// -or- + /// + /// + /// The has not been started or is stopped + /// currently. + /// + /// + /// + /// This object has been closed. + /// + public IAsyncResult BeginGetContext (AsyncCallback callback, Object state) + { + return BeginGetContext (new ListenerAsyncResult (callback, state)); + } + + /// + /// Shuts down the . + /// + public void Close () + { + if (_disposed) + return; + + close (false); + _disposed = true; + } + + /// + /// Ends an asynchronous operation to get an incoming request information. + /// + /// + /// This method completes an asynchronous operation started by calling the + /// BeginGetContext method. + /// + /// + /// A that contains a client's request + /// information. + /// + /// + /// An obtained by calling the + /// BeginGetContext method. + /// + /// + /// was not obtained by calling the + /// BeginGetContext method. + /// + /// + /// is . + /// + /// + /// This method was already called for the specified + /// . + /// + /// + /// This object has been closed. + /// + public HttpListenerContext EndGetContext (IAsyncResult asyncResult) + { + CheckDisposed (); + if (asyncResult == null) + throw new ArgumentNullException ("asyncResult"); + + var ares = asyncResult as ListenerAsyncResult; + if (ares == null) + throw new ArgumentException ("Wrong IAsyncResult.", "asyncResult"); + + if (ares.EndCalled) + throw new InvalidOperationException ("Cannot reuse this IAsyncResult."); + + ares.EndCalled = true; + if (!ares.IsCompleted) + ares.AsyncWaitHandle.WaitOne (); + + lock (((ICollection) _waitQueue).SyncRoot) { + var i = _waitQueue.IndexOf (ares); + if (i >= 0) + _waitQueue.RemoveAt (i); + } + + var context = ares.GetContext (); + var authScheme = SelectAuthenticationScheme (context); + if (authScheme != AuthenticationSchemes.Anonymous) + context.SetUser (authScheme, Realm, UserCredentialsFinder); + + return context; // This will throw on error. + } + + /// + /// Gets an incoming request information. + /// + /// + /// This method waits for an incoming request and returns the request + /// information when received the request. + /// + /// + /// A that contains a client's request + /// information. + /// + /// + /// + /// The does not have any URI prefixes to listen + /// on. + /// + /// + /// -or- + /// + /// + /// The has not been started or is stopped + /// currently. + /// + /// + /// + /// This object has been closed. + /// + public HttpListenerContext GetContext () + { + var ares = BeginGetContext (new ListenerAsyncResult (null, null)); + ares.InGet = true; + + return EndGetContext (ares); + } + + /// + /// Starts to receive incoming requests. + /// + /// + /// This object has been closed. + /// + public void Start () + { + CheckDisposed (); + if (_listening) + return; + + EndPointManager.AddListener (this); + _listening = true; + } + + /// + /// Stops receiving incoming requests. + /// + /// + /// This object has been closed. + /// + public void Stop () + { + CheckDisposed (); + if (!_listening) + return; + + _listening = false; + EndPointManager.RemoveListener (this); + sendServiceUnavailable (); + } + + #endregion + + #region Explicit Interface Implementation + + /// + /// Releases all resource used by the . + /// + void IDisposable.Dispose () + { + if (_disposed) + return; + + close (true); // TODO: Should we force here or not? + _disposed = true; + } + + #endregion + } } diff --git a/websocket-sharp/Net/HttpListenerContext.cs b/websocket-sharp/Net/HttpListenerContext.cs index d178db87..e8fff469 100644 --- a/websocket-sharp/Net/HttpListenerContext.cs +++ b/websocket-sharp/Net/HttpListenerContext.cs @@ -1,33 +1,40 @@ #region License -// -// HttpListenerContext.cs -// Copied from System.Net.HttpListenerContext.cs -// -// Author: -// Gonzalo Paniagua Javier (gonzalo@novell.com) -// -// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) -// Copyright (c) 2012-2013 sta.blockhead (sta.blockhead@gmail.com) -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// +/* + * HttpListenerContext.cs + * + * This code is derived from System.Net.HttpListenerContext.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2013 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * Gonzalo Paniagua Javier + */ #endregion using System; @@ -37,182 +44,184 @@ using System.Security.Principal; using System.Text; using WebSocketSharp.Net.WebSockets; -namespace WebSocketSharp.Net { +namespace WebSocketSharp.Net +{ + /// + /// Provides access to the HTTP request and response information used by the + /// . + /// + /// + /// The HttpListenerContext class cannot be inherited. + /// + public sealed class HttpListenerContext + { + #region Private Fields - /// - /// Provides access to the HTTP request and response objects used by the class. - /// - /// - /// The HttpListenerContext class cannot be inherited. - /// - public sealed class HttpListenerContext { + private HttpConnection _connection; + private string _error; + private int _errorStatus; + private HttpListenerRequest _request; + private HttpListenerResponse _response; + private IPrincipal _user; - #region Private Fields + #endregion - private HttpConnection _connection; - private string _error; - private int _errorStatus; - private HttpListenerRequest _request; - private HttpListenerResponse _response; - private IPrincipal _user; + #region Internal Fields - #endregion + internal HttpListener Listener; - #region Internal Fields + #endregion - internal HttpListener Listener; + #region Internal Constructors - #endregion + internal HttpListenerContext (HttpConnection connection) + { + _connection = connection; + _errorStatus = 400; + _request = new HttpListenerRequest (this); + _response = new HttpListenerResponse (this); + } - #region Constructor + #endregion - internal HttpListenerContext (HttpConnection connection) - { - _connection = connection; - _errorStatus = 400; - _request = new HttpListenerRequest (this); - _response = new HttpListenerResponse (this); - } + #region Internal Properties - #endregion + internal HttpConnection Connection { + get { + return _connection; + } + } - #region Internal Properties + internal string ErrorMessage { + get { + return _error; + } - internal HttpConnection Connection { - get { - return _connection; - } - } + set { + _error = value; + } + } - internal string ErrorMessage { - get { - return _error; - } + internal int ErrorStatus { + get { + return _errorStatus; + } - set { - _error = value; - } - } + set { + _errorStatus = value; + } + } - internal int ErrorStatus { - get { - return _errorStatus; - } + internal bool HaveError { + get { + return _error != null && _error.Length > 0; + } + } - set { - _errorStatus = value; - } - } + #endregion - internal bool HaveError { - get { - return _error != null; - } - } + #region Public Properties - #endregion + /// + /// Gets the that contains the HTTP + /// request information from a client. + /// + /// + /// A that contains the HTTP request + /// information. + /// + public HttpListenerRequest Request { + get { + return _request; + } + } - #region Public Properties + /// + /// Gets the that contains the HTTP + /// response information to send to the client in response to the client's + /// request. + /// + /// + /// A that contains the HTTP response + /// information. + /// + public HttpListenerResponse Response { + get { + return _response; + } + } - /// - /// Gets the that contains the HTTP request from a client. - /// - /// - /// A that contains the HTTP request objects. - /// - public HttpListenerRequest Request { - get { - return _request; - } - } + /// + /// Gets the client information (identity, authentication information, and + /// security roles). + /// + /// + /// A contains the client information. + /// + public IPrincipal User { + get { + return _user; + } + } - /// - /// Gets the that contains the HTTP response to send to - /// the client in response to the client's request. - /// - /// - /// A that contains the HTTP response objects. - /// - public HttpListenerResponse Response { - get { - return _response; - } - } + #endregion - /// - /// Gets the client information (identity, authentication information and security roles). - /// - /// - /// A contains the client information. - /// - public IPrincipal User { - get { - return _user; - } - } + #region Internal Methods - #endregion + internal void SetUser ( + AuthenticationSchemes expectedScheme, + string realm, + Func credentialsFinder) + { + var authRes = AuthenticationResponse.Parse (_request.Headers ["Authorization"]); + if (authRes == null) + return; - #region Internal Methods + var identity = authRes.ToIdentity (); + if (identity == null) + return; - internal void ParseAuthentication (AuthenticationSchemes expectedSchemes) - { - if (expectedSchemes == AuthenticationSchemes.Anonymous) - return; + NetworkCredential credentials = null; + try { + credentials = credentialsFinder (identity); + } + catch { + } - // TODO: Handle NTLM/Digest modes. - var header = _request.Headers ["Authorization"]; - if (header == null || header.Length < 2) - return; + if (credentials == null) + return; - var authData = header.Split (new char [] {' '}, 2); - if (authData [0].ToLower () == "basic") - _user = ParseBasicAuthentication (authData [1]); + var valid = expectedScheme == AuthenticationSchemes.Basic + ? ((HttpBasicIdentity) identity).Password == credentials.Password + : expectedScheme == AuthenticationSchemes.Digest + ? ((HttpDigestIdentity) identity).IsValid ( + credentials.Password, realm, _request.HttpMethod, null) + : false; - // TODO: Throw if malformed -> 400 bad request. - } + if (valid) + _user = new GenericPrincipal (identity, credentials.Roles); + } - internal IPrincipal ParseBasicAuthentication (string authData) - { - try { - // HTTP Basic Authentication data is a formatted Base64 string. - var authString = Encoding.Default.GetString (Convert.FromBase64String (authData)); + #endregion - // The format is domain\username:password. - // Domain is optional. + #region Public Method - var pos = authString.IndexOf (':'); - var user = authString.Substring (0, pos); - var password = authString.Substring (pos + 1); + /// + /// Accepts a WebSocket connection request. + /// + /// + /// A that contains a WebSocket + /// connection request information. + /// + /// + /// A that provides the logging functions used in the + /// WebSocket attempts. + /// + public HttpListenerWebSocketContext AcceptWebSocket (Logger logger) + { + return new HttpListenerWebSocketContext (this, logger); + } - // Check if there is a domain. - pos = user.IndexOf ('\\'); - if (pos > 0) - user = user.Substring (pos + 1); - - var identity = new System.Net.HttpListenerBasicIdentity (user, password); - // TODO: What are the roles MS sets? - return new GenericPrincipal (identity, new string [0]); - } catch { - return null; - } - } - - #endregion - - #region Public Method - - /// - /// Accepts a WebSocket connection by the . - /// - /// - /// A that contains a WebSocket connection. - /// - public HttpListenerWebSocketContext AcceptWebSocket () - { - return new HttpListenerWebSocketContext (this); - } - - #endregion - } + #endregion + } } diff --git a/websocket-sharp/Net/HttpListenerRequest.cs b/websocket-sharp/Net/HttpListenerRequest.cs index 86a4e974..ad36028b 100644 --- a/websocket-sharp/Net/HttpListenerRequest.cs +++ b/websocket-sharp/Net/HttpListenerRequest.cs @@ -1,757 +1,806 @@ #region License -// -// HttpListenerRequest.cs -// Copied from System.Net.HttpListenerRequest.cs -// -// Author: -// Gonzalo Paniagua Javier (gonzalo@novell.com) -// -// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) -// Copyright (c) 2012-2013 sta.blockhead -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// +/* + * HttpListenerRequest.cs + * + * This code is derived from System.Net.HttpListenerRequest.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2014 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * Gonzalo Paniagua Javier + */ #endregion using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.IO; using System.Linq; -using System.Net; using System.Security.Cryptography.X509Certificates; using System.Text; namespace WebSocketSharp.Net { - /// - /// Provides access to a request to a instance. - /// - /// - /// The HttpListenerRequest class cannot be inherited. - /// - public sealed class HttpListenerRequest - { - #region Private Static Fields + /// + /// Provides access to a request to the . + /// + /// + /// The HttpListenerRequest class cannot be inherited. + /// + public sealed class HttpListenerRequest + { + #region Private Static Fields - private static byte [] _100continue = Encoding.ASCII.GetBytes ("HTTP/1.1 100 Continue\r\n\r\n"); + private static byte [] _100continue = + Encoding.ASCII.GetBytes ("HTTP/1.1 100 Continue\r\n\r\n"); - #endregion + #endregion - #region Private Fields + #region Private Fields - private string [] _acceptTypes; - private bool _chunked; - private Encoding _contentEncoding; - private long _contentLength; - private bool _contentLengthWasSet; - private HttpListenerContext _context; - private CookieCollection _cookies; - private WebHeaderCollection _headers; - private Guid _identifier; - private Stream _inputStream; - private bool _keepAlive; - private bool _keepAliveWasSet; - private string _method; - private NameValueCollection _queryString; - private string _rawUrl; - private Uri _referer; - private Uri _url; - private string [] _userLanguages; - private Version _version; + private string [] _acceptTypes; + private bool _chunked; + private Encoding _contentEncoding; + private long _contentLength; + private bool _contentLengthWasSet; + private HttpListenerContext _context; + private CookieCollection _cookies; + private WebHeaderCollection _headers; + private Guid _identifier; + private Stream _inputStream; + private bool _keepAlive; + private bool _keepAliveWasSet; + private string _method; + private NameValueCollection _queryString; + private string _rawUrl; + private Uri _referer; + private Uri _url; + private string [] _userLanguages; + private Version _version; - #endregion + #endregion - #region Internal Constructors + #region Internal Constructors - internal HttpListenerRequest (HttpListenerContext context) - { - _context = context; - _contentLength = -1; - _headers = new WebHeaderCollection (); - _identifier = Guid.NewGuid (); - _version = HttpVersion.Version10; - } + internal HttpListenerRequest (HttpListenerContext context) + { + _context = context; + _contentLength = -1; + _headers = new WebHeaderCollection (); + _identifier = Guid.NewGuid (); + _version = HttpVersion.Version10; + } - #endregion + #endregion - #region Public Properties + #region Public Properties - /// - /// Gets the media types which are acceptable for the response. - /// - /// - /// An array of that contains the media type names in the Accept request-header - /// or if the request did not include an Accept header. - /// - public string [] AcceptTypes { - get { - return _acceptTypes; - } - } + /// + /// Gets the media types which are acceptable for the response. + /// + /// + /// An array of that contains the media type names in + /// the Accept request-header or if the request didn't + /// include an Accept header. + /// + public string [] AcceptTypes { + get { + return _acceptTypes; + } + } - /// - /// Gets an error code that identifies a problem with the client's certificate. - /// - /// - /// Always returns 0. - /// - public int ClientCertificateError { - get { - // TODO: Always returns 0. + /// + /// Gets an error code that identifies a problem with the client's + /// certificate. + /// + /// + /// Always returns 0. + /// + public int ClientCertificateError { + get { + // TODO: Always returns 0. /* - if (no_get_certificate) - throw new InvalidOperationException ( - "Call GetClientCertificate method before accessing this property."); + if (no_get_certificate) + throw new InvalidOperationException ( + "Call GetClientCertificate method before accessing this property."); - return client_cert_error; + return client_cert_error; */ - return 0; - } - } - - /// - /// Gets the encoding used with the entity body data included in the request. - /// - /// - /// A that indicates the encoding used with the entity body data - /// or if the request did not include the information about the encoding. - /// - public Encoding ContentEncoding { - get { - if (_contentEncoding == null) - _contentEncoding = Encoding.Default; - - return _contentEncoding; - } - } - - /// - /// Gets the size of the entity body data included in the request. - /// - /// - /// A that contains the value of the Content-Length entity-header. - /// The value is a number of bytes in the entity body data. -1 if the size is not known. - /// - public long ContentLength64 { - get { - return _contentLength; - } - } - - /// - /// Gets the media type of the entity body included in the request. - /// - /// - /// A that contains the value of the Content-Type entity-header. - /// - public string ContentType { - get { - return _headers ["Content-Type"]; - } - } - - /// - /// Gets the cookies included in the request. - /// - /// - /// A that contains the cookies included in the request. - /// - public CookieCollection Cookies { - get { - if (_cookies == null) - _cookies = _headers.GetCookies (false); - - return _cookies; - } - } - - /// - /// Gets a value indicating whether the request has the entity body. - /// - /// - /// true if the request has the entity body; otherwise, false. - /// - public bool HasEntityBody { - get { - return _contentLength > 0 || _chunked; - } - } - - /// - /// Gets the HTTP headers used in the request. - /// - /// - /// A that contains the HTTP headers used in the request. - /// - public NameValueCollection Headers { - get { - return _headers; - } - } - - /// - /// Gets the HTTP method used in the request. - /// - /// - /// A that contains the HTTP method used in the request. - /// - public string HttpMethod { - get { - return _method; - } - } - - /// - /// Gets a that contains the entity body data included in the request. - /// - /// - /// A that contains the entity body data included in the request. - /// - public Stream InputStream { - get { - if (_inputStream == null) - _inputStream = HasEntityBody - ? _context.Connection.GetRequestStream (_chunked, _contentLength) - : Stream.Null; - - return _inputStream; - } - } - - /// - /// Gets a value indicating whether the client that sent the request is authenticated. - /// - /// - /// Always returns false. - /// - public bool IsAuthenticated { - get { - // TODO: Always returns false. - return false; - } - } - - /// - /// Gets a value indicating whether the request is sent from the local computer. - /// - /// - /// true if the request is sent from the local computer; otherwise, false. - /// - public bool IsLocal { - get { - return RemoteEndPoint.Address.IsLocal (); - } - } - - /// - /// Gets a value indicating whether the HTTP connection is secured using the SSL protocol. - /// - /// - /// true if the HTTP connection is secured; otherwise, false. - /// - public bool IsSecureConnection { - get { - return _context.Connection.IsSecure; - } - } - - /// - /// Gets a value indicating whether the request is a WebSocket connection request. - /// - /// - /// true if the request is a WebSocket connection request; otherwise, false. - /// - public bool IsWebSocketRequest { - get { - return _method == "GET" && - _version >= HttpVersion.Version11 && - _headers.Contains ("Upgrade", "websocket") && - _headers.Contains ("Connection", "Upgrade"); - } - } - - /// - /// Gets a value indicating whether the client requests a persistent connection. - /// - /// - /// true if the client requests a persistent connection; otherwise, false. - /// - public bool KeepAlive { - get { - if (!_keepAliveWasSet) { - _keepAlive = _headers.Contains ("Connection", "keep-alive") || _version == HttpVersion.Version11 - ? true - : _headers.Contains ("Keep-Alive") - ? !_headers.Contains ("Keep-Alive", "closed") - : false; - - _keepAliveWasSet = true; - } - - return _keepAlive; - } - } - - /// - /// Gets the server endpoint as an IP address and a port number. - /// - /// - /// A that contains the server endpoint. - /// - public IPEndPoint LocalEndPoint { - get { - return _context.Connection.LocalEndPoint; - } - } - - /// - /// Gets the HTTP version used in the request. - /// - /// - /// A that contains the HTTP version used in the request. - /// - public Version ProtocolVersion { - get { - return _version; - } - } - - /// - /// Gets the collection of query string variables used in the request. - /// - /// - /// A that contains the collection of query string variables used in the request. - /// - public NameValueCollection QueryString { - get { - return _queryString; - } - } - - /// - /// Gets the raw URL (without the scheme, host and port) requested by the client. - /// - /// - /// A that contains the raw URL requested by the client. - /// - public string RawUrl { - get { - return _rawUrl; - } - } - - /// - /// Gets the client endpoint as an IP address and a port number. - /// - /// - /// A that contains the client endpoint. - /// - public IPEndPoint RemoteEndPoint { - get { - return _context.Connection.RemoteEndPoint; - } - } - - /// - /// Gets the request identifier of a incoming HTTP request. - /// - /// - /// A that contains the identifier of a request. - /// - public Guid RequestTraceIdentifier { - get { - return _identifier; - } - } - - /// - /// Gets the URL requested by the client. - /// - /// - /// A that contains the URL requested by the client. - /// - public Uri Url { - get { - return _url; - } - } - - /// - /// Gets the URL of the resource from which the requested URL was obtained. - /// - /// - /// A that contains the value of the Referer request-header - /// or if the request did not include an Referer header. - /// - public Uri UrlReferrer { - get { - return _referer; - } - } - - /// - /// Gets the information about the user agent originating the request. - /// - /// - /// A that contains the value of the User-Agent request-header. - /// - public string UserAgent { - get { - return _headers ["User-Agent"]; - } - } - - /// - /// Gets the server endpoint as an IP address and a port number. - /// - /// - /// A that contains the server endpoint. - /// - public string UserHostAddress { - get { - return LocalEndPoint.ToString (); - } - } - - /// - /// Gets the internet host name and port number (if present) specified by the client. - /// - /// - /// A that contains the value of the Host request-header. - /// - public string UserHostName { - get { - return _headers ["Host"]; - } - } - - /// - /// Gets the natural languages which are preferred for the response. - /// - /// - /// An array of that contains the natural language names in the Accept-Language request-header - /// or if the request did not include an Accept-Language header. - /// - public string [] UserLanguages { - get { - return _userLanguages; - } - } - - #endregion - - #region Private Methods - - private void CreateQueryString (string query) - { - if (query == null || query.Length == 0) { - _queryString = new NameValueCollection (1); - return; - } - - _queryString = new NameValueCollection (); - if (query [0] == '?') - query = query.Substring (1); - - var components = query.Split ('&'); - foreach (var kv in components) { - var pos = kv.IndexOf ('='); - if (pos == -1) { - _queryString.Add (null, HttpUtility.UrlDecode (kv)); - } else { - var key = HttpUtility.UrlDecode (kv.Substring (0, pos)); - var val = HttpUtility.UrlDecode (kv.Substring (pos + 1)); - _queryString.Add (key, val); - } - } - } - - #endregion - - #region Internal Methods - - internal void AddHeader (string header) - { - var colon = header.IndexOf (':'); - if (colon <= 0) { - _context.ErrorMessage = "Invalid header"; - return; - } - - var name = header.Substring (0, colon).Trim (); - var val = header.Substring (colon + 1).Trim (); - var lower = name.ToLower (CultureInfo.InvariantCulture); - _headers.SetInternal (name, val, false); - - if (lower == "accept") { - _acceptTypes = val.SplitHeaderValue (',').ToArray (); - return; - } - - if (lower == "accept-language") { - _userLanguages = val.Split (','); - return; - } - - if (lower == "content-length") { - long length; - if (Int64.TryParse (val, out length) && length >= 0) { - _contentLength = length; - _contentLengthWasSet = true; - } else { - _context.ErrorMessage = "Invalid Content-Length header"; - } - - return; - } - - if (lower == "content-type") { - var contents = val.Split (';'); - foreach (var content in contents) { - var tmp = content.Trim (); - if (tmp.StartsWith ("charset")) { - var charset = tmp.GetValue ("="); - if (!charset.IsNullOrEmpty ()) { - try { - _contentEncoding = Encoding.GetEncoding (charset); - } catch { - _context.ErrorMessage = "Invalid Content-Type header"; - } - } - - break; - } - } - - return; - } - - if (lower == "referer") - _referer = val.ToUri (); - } - - internal void FinishInitialization () - { - var host = UserHostName; - if (_version > HttpVersion.Version10 && host.IsNullOrEmpty ()) { - _context.ErrorMessage = "Invalid Host header"; - return; - } - - Uri rawUri = null; - var path = _rawUrl.MaybeUri () && Uri.TryCreate (_rawUrl, UriKind.Absolute, out rawUri) - ? rawUri.PathAndQuery - : HttpUtility.UrlDecode (_rawUrl); - - if (host.IsNullOrEmpty ()) - host = UserHostAddress; - - if (rawUri != null) - host = rawUri.Host; - - var colon = host.IndexOf (':'); - if (colon >= 0) - host = host.Substring (0, colon); - - var baseUri = String.Format ("{0}://{1}:{2}", - IsSecureConnection ? "https" : "http", - host, - LocalEndPoint.Port); - - if (!Uri.TryCreate (baseUri + path, UriKind.Absolute, out _url)) { - _context.ErrorMessage = "Invalid request url: " + baseUri + path; - return; - } - - CreateQueryString (_url.Query); - - var encoding = Headers ["Transfer-Encoding"]; - if (_version >= HttpVersion.Version11 && !encoding.IsNullOrEmpty ()) { - _chunked = encoding.ToLower () == "chunked"; - // 'identity' is not valid! - if (!_chunked) { - _context.ErrorMessage = String.Empty; - _context.ErrorStatus = 501; - - return; - } - } - - if (!_chunked && !_contentLengthWasSet) { - var method = _method.ToLower (); - if (method == "post" || method == "put") { - _context.ErrorMessage = String.Empty; - _context.ErrorStatus = 411; - - return; - } - } - - var expect = Headers ["Expect"]; - if (!expect.IsNullOrEmpty () && expect.ToLower () == "100-continue") { - var output = _context.Connection.GetResponseStream (); - output.InternalWrite (_100continue, 0, _100continue.Length); - } - } - - // Returns true is the stream could be reused. - internal bool FlushInput () - { - if (!HasEntityBody) - return true; - - var length = 2048; - if (_contentLength > 0) - length = (int) Math.Min (_contentLength, (long) length); - - var buffer = new byte [length]; - while (true) { - // TODO: Test if MS has a timeout when doing this. - try { - var ares = InputStream.BeginRead (buffer, 0, length, null, null); - if (!ares.IsCompleted && !ares.AsyncWaitHandle.WaitOne (100)) - return false; - - if (InputStream.EndRead (ares) <= 0) - return true; - } catch { - return false; - } - } - } - - internal void SetRequestLine (string requestLine) - { - var parts = requestLine.Split (new char [] { ' ' }, 3); - if (parts.Length != 3) { - _context.ErrorMessage = "Invalid request line (parts)"; - return; - } - - _method = parts [0]; - if (!_method.IsToken ()) { - _context.ErrorMessage = "Invalid request line (method)"; - return; - } - - _rawUrl = parts [1]; - - if (parts [2].Length != 8 || !parts [2].StartsWith ("HTTP/")) { - _context.ErrorMessage = "Invalid request line (version)"; - return; - } - - try { - _version = new Version (parts [2].Substring (5)); - if (_version.Major < 1) - throw new Exception (); - } catch { - _context.ErrorMessage = "Invalid request line (version)"; - } - } - - #endregion - - #region Public Methods - - /// - /// Begins getting the client's X.509 v.3 certificate asynchronously. - /// - /// - /// This asynchronous operation must be completed by calling the method. - /// Typically, the method is invoked by the delegate. - /// - /// - /// An that contains the status of the asynchronous operation. - /// - /// - /// An delegate that references the method(s) - /// called when the asynchronous operation completes. - /// - /// - /// An that contains a user defined object to pass to the delegate. - /// - /// - /// This method is not implemented. - /// - public IAsyncResult BeginGetClientCertificate (AsyncCallback requestCallback, Object state) - { - // TODO: Not Implemented. - throw new NotImplementedException (); - } - - /// - /// Ends an asynchronous operation to get the client's X.509 v.3 certificate. - /// - /// - /// This method completes an asynchronous operation started by calling the method. - /// - /// - /// A that contains the client's X.509 v.3 certificate. - /// - /// - /// An obtained by calling the method. - /// - /// - /// This method is not implemented. - /// - public X509Certificate2 EndGetClientCertificate (IAsyncResult asyncResult) - { - // TODO: Not Implemented. - throw new NotImplementedException (); - } - - /// - /// Gets the client's X.509 v.3 certificate. - /// - /// - /// A that contains the client's X.509 v.3 certificate. - /// - /// - /// This method is not implemented. - /// - public X509Certificate2 GetClientCertificate () - { - // TODO: Not Implemented. - throw new NotImplementedException (); - } - - /// - /// Returns a that represents the current . - /// - /// - /// A that represents the current . - /// - public override string ToString () - { - var buffer = new StringBuilder (64); - buffer.AppendFormat ("{0} {1} HTTP/{2}\r\n", _method, _rawUrl, _version); - foreach (string key in _headers.AllKeys) - buffer.AppendFormat ("{0}: {1}\r\n", key, _headers [key]); - - buffer.Append ("\r\n"); - return buffer.ToString (); - } - - #endregion - } + return 0; + } + } + + /// + /// Gets the encoding used with the entity body data included in the request. + /// + /// + /// A that represents the encoding used with the entity + /// body data or if the request didn't include + /// the information about the encoding. + /// + public Encoding ContentEncoding { + get { + return _contentEncoding ?? (_contentEncoding = Encoding.Default); + } + } + + /// + /// Gets the size of the entity body data included in the request. + /// + /// + /// A that represents the value of the Content-Length + /// entity-header. The value is a number of bytes in the entity body data. + /// -1 if the size isn't known. + /// + public long ContentLength64 { + get { + return _contentLength; + } + } + + /// + /// Gets the media type of the entity body included in the request. + /// + /// + /// A that represents the value of the Content-Type + /// entity-header. + /// + public string ContentType { + get { + return _headers ["Content-Type"]; + } + } + + /// + /// Gets the cookies included in the request. + /// + /// + /// A that contains the cookies included in + /// the request. + /// + public CookieCollection Cookies { + get { + return _cookies ?? (_cookies = _headers.GetCookies (false)); + } + } + + /// + /// Gets a value indicating whether the request has the entity body. + /// + /// + /// true if the request has the entity body; otherwise, false. + /// + public bool HasEntityBody { + get { + return _contentLength > 0 || _chunked; + } + } + + /// + /// Gets the HTTP headers used in the request. + /// + /// + /// A that contains the HTTP headers used + /// in the request. + /// + public NameValueCollection Headers { + get { + return _headers; + } + } + + /// + /// Gets the HTTP method used in the request. + /// + /// + /// A that represents the HTTP method used in the request. + /// + public string HttpMethod { + get { + return _method; + } + } + + /// + /// Gets a that contains the entity body data included + /// in the request. + /// + /// + /// A that contains the entity body data included in the + /// request. + /// + public Stream InputStream { + get { + return _inputStream ?? + (_inputStream = HasEntityBody + ? _context.Connection.GetRequestStream ( + _chunked, _contentLength) + : Stream.Null); + } + } + + /// + /// Gets a value indicating whether the client that sent the request is + /// authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public bool IsAuthenticated { + get { + var user = _context.User; + return user != null && user.Identity.IsAuthenticated; + } + } + + /// + /// Gets a value indicating whether the request is sent from the local + /// computer. + /// + /// + /// true if the request is sent from the local computer; otherwise, + /// false. + /// + public bool IsLocal { + get { + return RemoteEndPoint.Address.IsLocal (); + } + } + + /// + /// Gets a value indicating whether the HTTP connection is secured using the + /// SSL protocol. + /// + /// + /// true if the HTTP connection is secured; otherwise, false. + /// + public bool IsSecureConnection { + get { + return _context.Connection.IsSecure; + } + } + + /// + /// Gets a value indicating whether the request is a WebSocket connection + /// request. + /// + /// + /// true if the request is a WebSocket connection request; otherwise, + /// false. + /// + public bool IsWebSocketRequest { + get { + return _method == "GET" && + _version >= HttpVersion.Version11 && + _headers.Contains ("Upgrade", "websocket") && + _headers.Contains ("Connection", "Upgrade"); + } + } + + /// + /// Gets a value indicating whether the client requests a persistent + /// connection. + /// + /// + /// true if the client requests a persistent connection; otherwise, + /// false. + /// + public bool KeepAlive { + get { + if (!_keepAliveWasSet) { + _keepAlive = _headers.Contains ("Connection", "keep-alive") || + _version == HttpVersion.Version11 + ? true + : _headers.Contains ("Keep-Alive") + ? !_headers.Contains ("Keep-Alive", "closed") + : false; + + _keepAliveWasSet = true; + } + + return _keepAlive; + } + } + + /// + /// Gets the server endpoint as an IP address and a port number. + /// + /// + /// A that represents the server endpoint. + /// + public System.Net.IPEndPoint LocalEndPoint { + get { + return _context.Connection.LocalEndPoint; + } + } + + /// + /// Gets the HTTP version used in the request. + /// + /// + /// A that represents the HTTP version used in the + /// request. + /// + public Version ProtocolVersion { + get { + return _version; + } + } + + /// + /// Gets the collection of query string variables used in the request. + /// + /// + /// A that contains the collection of query + /// string variables used in the request. + /// + public NameValueCollection QueryString { + get { + return _queryString; + } + } + + /// + /// Gets the raw URL (without the scheme, host and port) requested by the + /// client. + /// + /// + /// A that represents the raw URL requested by the + /// client. + /// + public string RawUrl { + get { + return _rawUrl; + } + } + + /// + /// Gets the client endpoint as an IP address and a port number. + /// + /// + /// A that represents the client endpoint. + /// + public System.Net.IPEndPoint RemoteEndPoint { + get { + return _context.Connection.RemoteEndPoint; + } + } + + /// + /// Gets the request identifier of a incoming HTTP request. + /// + /// + /// A that represents the identifier of a request. + /// + public Guid RequestTraceIdentifier { + get { + return _identifier; + } + } + + /// + /// Gets the URL requested by the client. + /// + /// + /// A that represents the URL requested by the client. + /// + public Uri Url { + get { + return _url; + } + } + + /// + /// Gets the URL of the resource from which the requested URL was obtained. + /// + /// + /// A that represents the value of the Referer + /// request-header or if the request didn't include + /// an Referer header. + /// + public Uri UrlReferrer { + get { + return _referer; + } + } + + /// + /// Gets the information about the user agent originating the request. + /// + /// + /// A that represents the value of the User-Agent + /// request-header. + /// + public string UserAgent { + get { + return _headers ["User-Agent"]; + } + } + + /// + /// Gets the server endpoint as an IP address and a port number. + /// + /// + /// A that represents the server endpoint. + /// + public string UserHostAddress { + get { + return LocalEndPoint.ToString (); + } + } + + /// + /// Gets the internet host name and port number (if present) specified by the + /// client. + /// + /// + /// A that represents the value of the Host + /// request-header. + /// + public string UserHostName { + get { + return _headers ["Host"]; + } + } + + /// + /// Gets the natural languages which are preferred for the response. + /// + /// + /// An array of that contains the natural language names + /// in the Accept-Language request-header or if the + /// request didn't include an Accept-Language header. + /// + public string [] UserLanguages { + get { + return _userLanguages; + } + } + + #endregion + + #region Private Methods + + private void createQueryString (string query) + { + if (query == null || query.Length == 0) { + _queryString = new NameValueCollection (1); + return; + } + + _queryString = new NameValueCollection (); + if (query [0] == '?') + query = query.Substring (1); + + var components = query.Split ('&'); + foreach (var component in components) { + var i = component.IndexOf ('='); + if (i == -1) { + _queryString.Add (null, HttpUtility.UrlDecode (component)); + } + else { + var name = HttpUtility.UrlDecode (component.Substring (0, i)); + var val = HttpUtility.UrlDecode (component.Substring (i + 1)); + _queryString.Add (name, val); + } + } + } + + #endregion + + #region Internal Methods + + internal void AddHeader (string header) + { + var colon = header.IndexOf (':'); + if (colon == -1) { + _context.ErrorMessage = "Invalid header"; + return; + } + + var name = header.Substring (0, colon).Trim (); + var val = header.Substring (colon + 1).Trim (); + var lower = name.ToLower (CultureInfo.InvariantCulture); + _headers.SetInternal (name, val, false); + + if (lower == "accept") { + _acceptTypes = val.SplitHeaderValue (',').ToArray (); + return; + } + + if (lower == "accept-language") { + _userLanguages = val.Split (','); + return; + } + + if (lower == "content-length") { + long length; + if (Int64.TryParse (val, out length) && length >= 0) { + _contentLength = length; + _contentLengthWasSet = true; + } + else { + _context.ErrorMessage = "Invalid Content-Length header"; + } + + return; + } + + if (lower == "content-type") { + var contents = val.Split (';'); + foreach (var content in contents) { + var tmp = content.Trim (); + if (tmp.StartsWith ("charset")) { + var charset = tmp.GetValue ("="); + if (charset != null && charset.Length > 0) { + try { + _contentEncoding = Encoding.GetEncoding (charset); + } + catch { + _context.ErrorMessage = "Invalid Content-Type header"; + } + } + + break; + } + } + + return; + } + + if (lower == "referer") + _referer = val.ToUri (); + } + + internal void FinishInitialization () + { + var host = _headers ["Host"]; + var noHost = host == null || host.Length == 0; + if (_version > HttpVersion.Version10 && noHost) { + _context.ErrorMessage = "Invalid Host header"; + return; + } + + if (noHost) + host = UserHostAddress; + + string path; + var rawUri = _rawUrl.ToUri (); + if (rawUri != null && rawUri.IsAbsoluteUri) { + host = rawUri.Host; + path = rawUri.PathAndQuery; + } + else + path = HttpUtility.UrlDecode (_rawUrl); + + var colon = host.IndexOf (':'); + if (colon != -1) + host = host.Substring (0, colon); + + var scheme = IsWebSocketRequest ? "ws" : "http"; + var url = String.Format ( + "{0}://{1}:{2}{3}", + IsSecureConnection ? scheme + "s" : scheme, + host, + LocalEndPoint.Port, + path); + + if (!Uri.TryCreate (url, UriKind.Absolute, out _url)) { + _context.ErrorMessage = "Invalid request url: " + url; + return; + } + + createQueryString (_url.Query); + + var encoding = Headers ["Transfer-Encoding"]; + if (_version >= HttpVersion.Version11 && + encoding != null && + encoding.Length > 0) { + _chunked = encoding.ToLower () == "chunked"; + if (!_chunked) { + _context.ErrorMessage = String.Empty; + _context.ErrorStatus = 501; + + return; + } + } + + if (!_chunked && !_contentLengthWasSet) { + var method = _method.ToLower (); + if (method == "post" || method == "put") { + _context.ErrorMessage = String.Empty; + _context.ErrorStatus = 411; + + return; + } + } + + var expect = Headers ["Expect"]; + if (expect != null && + expect.Length > 0 && + expect.ToLower () == "100-continue") { + var output = _context.Connection.GetResponseStream (); + output.InternalWrite (_100continue, 0, _100continue.Length); + } + } + + // Returns true is the stream could be reused. + internal bool FlushInput () + { + if (!HasEntityBody) + return true; + + var length = 2048; + if (_contentLength > 0) + length = (int) Math.Min (_contentLength, (long) length); + + var buffer = new byte [length]; + while (true) { + // TODO: Test if MS has a timeout when doing this. + try { + var ares = InputStream.BeginRead (buffer, 0, length, null, null); + if (!ares.IsCompleted && !ares.AsyncWaitHandle.WaitOne (100)) + return false; + + if (InputStream.EndRead (ares) <= 0) + return true; + } + catch { + return false; + } + } + } + + internal void SetRequestLine (string requestLine) + { + var parts = requestLine.Split (new char [] { ' ' }, 3); + if (parts.Length != 3) { + _context.ErrorMessage = "Invalid request line (parts)"; + return; + } + + _method = parts [0]; + if (!_method.IsToken ()) { + _context.ErrorMessage = "Invalid request line (method)"; + return; + } + + _rawUrl = parts [1]; + + if (parts [2].Length != 8 || !parts [2].StartsWith ("HTTP/")) { + _context.ErrorMessage = "Invalid request line (version)"; + return; + } + + try { + _version = new Version (parts [2].Substring (5)); + if (_version.Major < 1) + throw new Exception (); + } + catch { + _context.ErrorMessage = "Invalid request line (version)"; + } + } + + #endregion + + #region Public Methods + + /// + /// Begins getting the client's X.509 v.3 certificate asynchronously. + /// + /// + /// This asynchronous operation must be completed by calling the + /// method. Typically, that method is + /// invoked by the delegate. + /// + /// + /// An that contains the status of the + /// asynchronous operation. + /// + /// + /// An delegate that references the method(s) + /// called when the asynchronous operation completes. + /// + /// + /// An that contains a user defined object to pass to + /// the delegate. + /// + /// + /// This method is not implemented. + /// + public IAsyncResult BeginGetClientCertificate ( + AsyncCallback requestCallback, Object state) + { + // TODO: Not Implemented. + throw new NotImplementedException (); + } + + /// + /// Ends an asynchronous operation to get the client's X.509 v.3 certificate. + /// + /// + /// This method completes an asynchronous operation started by calling the + /// method. + /// + /// + /// A that contains the client's X.509 v.3 + /// certificate. + /// + /// + /// An obtained by calling the + /// method. + /// + /// + /// This method is not implemented. + /// + public X509Certificate2 EndGetClientCertificate (IAsyncResult asyncResult) + { + // TODO: Not Implemented. + throw new NotImplementedException (); + } + + /// + /// Gets the client's X.509 v.3 certificate. + /// + /// + /// A that contains the client's X.509 v.3 + /// certificate. + /// + /// + /// This method is not implemented. + /// + public X509Certificate2 GetClientCertificate () + { + // TODO: Not Implemented. + throw new NotImplementedException (); + } + + /// + /// Returns a that represents the current + /// . + /// + /// + /// A that represents the current + /// . + /// + public override string ToString () + { + var buffer = new StringBuilder (64); + buffer.AppendFormat ("{0} {1} HTTP/{2}\r\n", _method, _rawUrl, _version); + foreach (var key in _headers.AllKeys) + buffer.AppendFormat ("{0}: {1}\r\n", key, _headers [key]); + + buffer.Append ("\r\n"); + return buffer.ToString (); + } + + #endregion + } } diff --git a/websocket-sharp/Net/HttpListenerResponse.cs b/websocket-sharp/Net/HttpListenerResponse.cs index 2463b190..68ec1c33 100644 --- a/websocket-sharp/Net/HttpListenerResponse.cs +++ b/websocket-sharp/Net/HttpListenerResponse.cs @@ -1,772 +1,852 @@ -// -// HttpListenerResponse.cs -// Copied from System.Net.HttpListenerResponse.cs -// -// Author: -// Gonzalo Paniagua Javier (gonzalo@novell.com) -// -// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) -// Copyright (c) 2012-2013 sta.blockhead (sta.blockhead@gmail.com) -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// +#region License +/* + * HttpListenerResponse.cs + * + * This code is derived from System.Net.HttpListenerResponse.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2014 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * Gonzalo Paniagua Javier + */ +#endregion using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Net; using System.Text; namespace WebSocketSharp.Net { - /// - /// Provides access to a response to a request being processed by a instance. - /// - /// - /// The HttpListenerResponse class cannot be inherited. - /// - public sealed class HttpListenerResponse : IDisposable - { - #region Private Fields - - bool chunked; - bool cl_set; - Encoding content_encoding; - long content_length; - string content_type; - HttpListenerContext context; - CookieCollection cookies; - bool disposed; - bool force_close_chunked; - WebHeaderCollection headers; - bool keep_alive; - string location; - ResponseStream output_stream; - int status_code; - string status_description; - Version version; - - #endregion - - #region Internal Field - - internal bool HeadersSent; - - #endregion - - #region Constructor - - internal HttpListenerResponse (HttpListenerContext context) - { - this.context = context; - Init (); - } - - #endregion - - #region Internal Property - - internal bool ForceCloseChunked { - get { return force_close_chunked; } - } - - #endregion - - #region Public Properties - - /// - /// Gets or sets the encoding that can be used with the entity body data included in the response. - /// - /// - /// A that contains the encoding that can be used with the entity body data. - /// - /// - /// This object is closed. - /// - /// - /// The response has been sent already. - /// - public Encoding ContentEncoding { - get { - if (content_encoding == null) - content_encoding = Encoding.Default; - - return content_encoding; - } - set { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); - - if (HeadersSent) - throw new InvalidOperationException ("Cannot be changed after headers are sent."); - - content_encoding = value; - } - } - - /// - /// Gets or sets the size of the entity body data included in the response. - /// - /// - /// A that contains the value of the Content-Length entity-header field. - /// The value is a number of bytes in the entity body data. - /// - /// - /// This object is closed. - /// - /// - /// The response has been sent already. - /// - /// - /// The value specified for a set operation is less than zero. - /// - public long ContentLength64 { - get { return content_length; } - set { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); - - if (HeadersSent) - throw new InvalidOperationException ("Cannot be changed after headers are sent."); - - if (value < 0) - throw new ArgumentOutOfRangeException ("Must be greater than or equal zero.", "value"); - - cl_set = true; - content_length = value; - } - } - - /// - /// Gets or sets the media type of the entity body included in the response. - /// - /// - /// The type of the content. - /// A that contains the value of the Content-Type entity-header field. - /// - /// - /// This object is closed. - /// - /// - /// The response has been sent already. - /// - /// - /// The value specified for a set operation is . - /// - /// - /// The value specified for a set operation is a . - /// - public string ContentType { - get { return content_type; } - set { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); - - if (HeadersSent) - throw new InvalidOperationException ("Cannot be changed after headers are sent."); - - if (value == null) - throw new ArgumentNullException ("value"); - - if (value.Length == 0) - throw new ArgumentException ("Must not be empty.", "value"); - - content_type = value; - } - } - - /// - /// Gets or sets the cookies returned with the response. - /// - /// - /// A that contains the cookies returned with the response. - /// - public CookieCollection Cookies { - get { - if (cookies == null) - cookies = new CookieCollection (); - - return cookies; - } - set { cookies = value; } - } - - /// - /// Gets or sets the HTTP headers returned to the client. - /// - /// - /// A that contains the HTTP headers returned to the client. - /// - public WebHeaderCollection Headers { - get { return headers; } - set { - /* - * "If you attempt to set a Content-Length, Keep-Alive, Transfer-Encoding, or - * WWW-Authenticate header using the Headers property, an exception will be - * thrown. Use the KeepAlive or ContentLength64 properties to set these headers. - * You cannot set the Transfer-Encoding or WWW-Authenticate headers manually." - */ - // TODO: Support for InvalidOperationException. - - // TODO: check if this is marked readonly after headers are sent. - - headers = value; - } - } - - /// - /// Gets or sets a value indicating whether the server requests a persistent connection. - /// - /// - /// true if the server requests a persistent connection; otherwise, false. - /// The default is true. - /// - /// - /// This object is closed. - /// - /// - /// The response has been sent already. - /// - public bool KeepAlive { - get { return keep_alive; } - set { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); - - if (HeadersSent) - throw new InvalidOperationException ("Cannot be changed after headers are sent."); - - keep_alive = value; - } - } - - /// - /// Gets a to use to write the entity body data. - /// - /// - /// A to use to write the entity body data. - /// - /// - /// This object is closed. - /// - public Stream OutputStream { - get { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); - - if (output_stream == null) - output_stream = context.Connection.GetResponseStream (); - - return output_stream; - } - } - - /// - /// Gets or sets the HTTP version used in the response. - /// - /// - /// A that contains the HTTP version used in the response. - /// - /// - /// This object is closed. - /// - /// - /// The response has been sent already. - /// - /// - /// The value specified for a set operation is . - /// - /// - /// The value specified for a set operation does not have its Major property set to 1 or - /// does not have its Minor property set to either 0 or 1. - /// - public Version ProtocolVersion { - get { return version; } - set { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); - - if (HeadersSent) - throw new InvalidOperationException ("Cannot be changed after headers are sent."); - - if (value == null) - throw new ArgumentNullException ("value"); - - if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1)) - throw new ArgumentException ("Must be 1.0 or 1.1", "value"); - - version = value; - } - } - - /// - /// Gets or sets the URL to which the client is redirected to locate a requested resource. - /// - /// - /// A that contains the value of the Location response-header field. - /// - /// - /// This object is closed. - /// - /// - /// The response has been sent already. - /// - /// - /// The value specified for a set operation is a . - /// - public string RedirectLocation { - get { return location; } - set { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); - - if (HeadersSent) - throw new InvalidOperationException ("Cannot be changed after headers are sent."); - - if (value.Length == 0) - throw new ArgumentException ("Must not be empty.", "value"); - - location = value; - } - } - - /// - /// Gets or sets a value indicating whether the response uses the chunked transfer encoding. - /// - /// - /// true if the response uses the chunked transfer encoding; otherwise, false. - /// - /// - /// This object is closed. - /// - /// - /// The response has been sent already. - /// - public bool SendChunked { - get { return chunked; } - set { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); - - if (HeadersSent) - throw new InvalidOperationException ("Cannot be changed after headers are sent."); - - chunked = value; - } - } - - /// - /// Gets or sets the HTTP status code returned to the client. - /// - /// - /// An that indicates the HTTP status code for the response to the request. - /// The default is . - /// - /// - /// This object is closed. - /// - /// - /// The response has been sent already. - /// - /// - /// The value specified for a set operation is invalid. Valid values are between 100 and 999. - /// - public int StatusCode { - get { return status_code; } - set { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); - - if (HeadersSent) - throw new InvalidOperationException ("Cannot be changed after headers are sent."); - - if (value < 100 || value > 999) - throw new ProtocolViolationException ("StatusCode must be between 100 and 999."); - - status_code = value; - status_description = value.GetStatusDescription (); - } - } - - /// - /// Gets or sets a description of the HTTP status code returned to the client. - /// - /// - /// A that contains a description of the HTTP status code returned to the client. - /// - public string StatusDescription { - get { return status_description; } - set { - status_description = value.IsNullOrEmpty () - ? status_code.GetStatusDescription () - : value; - } - } - - #endregion - - #region Private Methods - - bool CanAddOrUpdate (Cookie cookie) - { - if (Cookies.Count == 0) - return true; - - var found = FindCookie (cookie); - if (found.Count() == 0) - return true; - - foreach (var c in found) - if (c.Version == cookie.Version) - return true; - - return false; - } - - void Close (bool force) - { - disposed = true; - context.Connection.Close (force); - } - - IEnumerable FindCookie (Cookie cookie) - { - var name = cookie.Name; - var domain = cookie.Domain; - var path = cookie.Path; - - return from Cookie c in Cookies - where String.Compare (name, c.Name, true, CultureInfo.InvariantCulture) == 0 && - String.Compare (domain, c.Domain, true, CultureInfo.InvariantCulture) == 0 && - String.Compare (path, c.Path, false, CultureInfo.InvariantCulture) == 0 - select c; - } - - void Init () - { - headers = new WebHeaderCollection (); - keep_alive = true; - status_code = 200; - status_description = "OK"; - version = HttpVersion.Version11; - } - - #endregion - - #region Internal Method - - internal void SendHeaders (bool closing, MemoryStream ms) - { - Encoding encoding = content_encoding; - if (encoding == null) - encoding = Encoding.Default; - - if (content_type != null) { - if (content_encoding != null && content_type.IndexOf ("charset=", StringComparison.Ordinal) == -1) { - string enc_name = content_encoding.WebName; - headers.SetInternal ("Content-Type", content_type + "; charset=" + enc_name, true); - } else { - headers.SetInternal ("Content-Type", content_type, true); - } - } - - if (headers ["Server"] == null) - headers.SetInternal ("Server", "WebSocketSharp-HTTPAPI/1.0", true); - - CultureInfo inv = CultureInfo.InvariantCulture; - if (headers ["Date"] == null) - headers.SetInternal ("Date", DateTime.UtcNow.ToString ("r", inv), true); - - if (!chunked) { - if (!cl_set && closing) { - cl_set = true; - content_length = 0; - } - - if (cl_set) - headers.SetInternal ("Content-Length", content_length.ToString (inv), true); - } - - Version v = context.Request.ProtocolVersion; - if (!cl_set && !chunked && v >= HttpVersion.Version11) - chunked = true; - - /* Apache forces closing the connection for these status codes: - * HttpStatusCode.BadRequest 400 - * HttpStatusCode.RequestTimeout 408 - * HttpStatusCode.LengthRequired 411 - * HttpStatusCode.RequestEntityTooLarge 413 - * HttpStatusCode.RequestUriTooLong 414 - * HttpStatusCode.InternalServerError 500 - * HttpStatusCode.ServiceUnavailable 503 - */ - bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 || - status_code == 413 || status_code == 414 || status_code == 500 || - status_code == 503); - - if (conn_close == false) - conn_close = !context.Request.KeepAlive; - - // They sent both KeepAlive: true and Connection: close!? - if (!keep_alive || conn_close) { - headers.SetInternal ("Connection", "close", true); - conn_close = true; - } - - if (chunked) - headers.SetInternal ("Transfer-Encoding", "chunked", true); - - int reuses = context.Connection.Reuses; - if (reuses >= 100) { - force_close_chunked = true; - if (!conn_close) { - headers.SetInternal ("Connection", "close", true); - conn_close = true; - } - } - - if (!conn_close) { - headers.SetInternal ("Keep-Alive", String.Format ("timeout=15,max={0}", 100 - reuses), true); - if (context.Request.ProtocolVersion <= HttpVersion.Version10) - headers.SetInternal ("Connection", "keep-alive", true); - } - - if (location != null) - headers.SetInternal ("Location", location, true); - - if (cookies != null) { - foreach (Cookie cookie in cookies) - headers.SetInternal ("Set-Cookie", cookie.ToResponseString (), true); - } - - StreamWriter writer = new StreamWriter (ms, encoding, 256); - writer.Write ("HTTP/{0} {1} {2}\r\n", version, status_code, status_description); - string headers_str = headers.ToStringMultiValue (true); - writer.Write (headers_str); - writer.Flush (); - int preamble = (encoding.CodePage == 65001) ? 3 : encoding.GetPreamble ().Length; - if (output_stream == null) - output_stream = context.Connection.GetResponseStream (); - - /* Assumes that the ms was at position 0 */ - ms.Position = preamble; - HeadersSent = true; - } - - #endregion - - #region Explicit Interface Implementation - - /// - /// Releases all resource used by the . - /// - void IDisposable.Dispose () - { - Close (true); // TODO: Abort or Close? - } - - #endregion - - #region Public Methods - - /// - /// Closes the connection to the client without sending a response. - /// - public void Abort () - { - if (disposed) - return; - - Close (true); - } - - /// - /// Adds the specified HTTP header and to - /// the headers for this response. - /// - /// - /// A that contains the name of the HTTP header to add. - /// - /// - /// A that contains the value of the HTTP header to add. - /// - /// - /// is or . - /// - /// - /// The length of is greater than 65,535 characters. - /// - public void AddHeader (string name, string value) - { - if (name.IsNullOrEmpty()) - throw new ArgumentNullException ("name"); - - // TODO: Check for forbidden headers and invalid characters. - - if (value.Length > 65535) - throw new ArgumentOutOfRangeException ("value"); - - headers.Set (name, value); - } - - /// - /// Adds the specified to the sent with the response. - /// - /// - /// A to add to the . - /// - /// - /// is . - /// - public void AppendCookie (Cookie cookie) - { - if (cookie == null) - throw new ArgumentNullException ("cookie"); - - Cookies.Add (cookie); - } - - /// - /// Appends a to the specified HTTP header sent with the response. - /// - /// - /// A that contains the name of the HTTP header to append to. - /// - /// - /// A that contains the value to append to the HTTP header. - /// - /// - /// is or . - /// - /// - /// The length of is greater than 65,535 characters. - /// - public void AppendHeader (string name, string value) - { - // TODO: Check for forbidden headers and invalid characters. - if (name.IsNullOrEmpty()) - throw new ArgumentException ("'name' cannot be null or empty", "name"); - - if (value.Length > 65535) - throw new ArgumentOutOfRangeException ("value"); - - headers.Add (name, value); - } - - /// - /// Sends the response to the client and releases the resources associated with - /// the instance. - /// - public void Close () - { - if (disposed) - return; - - Close (false); - } - - /// - /// Sends the response with the specified array of to the client and - /// releases the resources associated with the instance. - /// - /// - /// An array of that contains the response entity body data. - /// - /// - /// true if this method blocks execution while flushing the stream to the client; otherwise, false. - /// - /// - /// is . - /// - /// - /// This object is closed. - /// - public void Close (byte [] responseEntity, bool willBlock) - { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); - - if (responseEntity == null) - throw new ArgumentNullException ("responseEntity"); - - // TODO: If willBlock -> BeginWrite + Close? - ContentLength64 = responseEntity.Length; - OutputStream.Write (responseEntity, 0, (int) content_length); - Close (false); - } - - /// - /// Copies properties from the specified to this response. - /// - /// - /// A to copy. - /// - public void CopyFrom (HttpListenerResponse templateResponse) - { - headers.Clear (); - headers.Add (templateResponse.headers); - content_length = templateResponse.content_length; - status_code = templateResponse.status_code; - status_description = templateResponse.status_description; - keep_alive = templateResponse.keep_alive; - version = templateResponse.version; - } - - /// - /// Configures the response to redirect the client's request to the specified . - /// - /// - /// A that contains a URL to redirect the client's request to. - /// - public void Redirect (string url) - { - StatusCode = (int) HttpStatusCode.Redirect; - location = url; - } - - /// - /// Adds or updates a in the sent with the response. - /// - /// - /// A to set. - /// - /// - /// is . - /// - /// - /// already exists in the and - /// could not be replaced. - /// - public void SetCookie (Cookie cookie) - { - if (cookie == null) - throw new ArgumentNullException ("cookie"); - - if (!CanAddOrUpdate (cookie)) - throw new ArgumentException ("Cannot be replaced.", "cookie"); - - Cookies.Add (cookie); - } - - #endregion - } + /// + /// Provides access to a response to a request being processed by the + /// . + /// + /// + /// The HttpListenerResponse class cannot be inherited. + /// + public sealed class HttpListenerResponse : IDisposable + { + #region Private Fields + + private bool _chunked; + private Encoding _contentEncoding; + private long _contentLength; + private bool _contentLengthSet; + private string _contentType; + private HttpListenerContext _context; + private CookieCollection _cookies; + private bool _disposed; + private bool _forceCloseChunked; + private WebHeaderCollection _headers; + private bool _keepAlive; + private string _location; + private ResponseStream _outputStream; + private int _statusCode; + private string _statusDescription; + private Version _version; + + #endregion + + #region Internal Fields + + internal bool HeadersSent; + + #endregion + + #region Internal Constructors + + internal HttpListenerResponse (HttpListenerContext context) + { + _context = context; + _headers = new WebHeaderCollection (); + _keepAlive = true; + _statusCode = 200; + _statusDescription = "OK"; + _version = HttpVersion.Version11; + } + + #endregion + + #region Internal Properties + + internal bool ForceCloseChunked { + get { + return _forceCloseChunked; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets the encoding that can be used with the entity body data + /// included in the response. + /// + /// + /// A that represents the encoding that can be used + /// with the entity body data. + /// + /// + /// The response has been sent already. + /// + /// + /// This object is closed. + /// + public Encoding ContentEncoding { + get { + return _contentEncoding ?? (_contentEncoding = Encoding.Default); + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (HeadersSent) + throw new InvalidOperationException ( + "Cannot be changed after headers are sent."); + + _contentEncoding = value; + } + } + + /// + /// Gets or sets the size of the entity body data included in the response. + /// + /// + /// A that represents the value of the Content-Length + /// entity-header field. The value is a number of bytes in the entity body + /// data. + /// + /// + /// The value specified for a set operation is less than zero. + /// + /// + /// The response has been sent already. + /// + /// + /// This object is closed. + /// + public long ContentLength64 { + get { + return _contentLength; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (HeadersSent) + throw new InvalidOperationException ( + "Cannot be changed after headers are sent."); + + if (value < 0) + throw new ArgumentOutOfRangeException ( + "Must not be less than zero.", "value"); + + _contentLengthSet = true; + _contentLength = value; + } + } + + /// + /// Gets or sets the media type of the entity body included in the response. + /// + /// + /// The type of the content. A that represents the value + /// of the Content-Type entity-header field. + /// + /// + /// The value specified for a set operation is empty. + /// + /// + /// The value specified for a set operation is . + /// + /// + /// The response has been sent already. + /// + /// + /// This object is closed. + /// + public string ContentType { + get { + return _contentType; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (HeadersSent) + throw new InvalidOperationException ( + "Cannot be changed after headers are sent."); + + if (value == null) + throw new ArgumentNullException ("value"); + + if (value.Length == 0) + throw new ArgumentException ( + "Must not be empty.", "value"); + + _contentType = value; + } + } + + /// + /// Gets or sets the cookies returned with the response. + /// + /// + /// A that contains the cookies returned with + /// the response. + /// + public CookieCollection Cookies { + get { + return _cookies ?? (_cookies = new CookieCollection ()); + } + + set { + _cookies = value; + } + } + + /// + /// Gets or sets the HTTP headers returned to the client. + /// + /// + /// A that contains the HTTP headers + /// returned to the client. + /// + public WebHeaderCollection Headers { + get { + return _headers; + } + + set { + /* + * "If you attempt to set a Content-Length, Keep-Alive, Transfer-Encoding, + * or WWW-Authenticate header using the Headers property, an exception + * will be thrown. Use the KeepAlive or ContentLength64 properties to set + * these headers. You cannot set the Transfer-Encoding or WWW-Authenticate + * headers manually." + */ + + // TODO: Support for InvalidOperationException. + + // TODO: Check if this is marked readonly after headers are sent. + + _headers = value; + } + } + + /// + /// Gets or sets a value indicating whether the server requests a persistent + /// connection. + /// + /// + /// true if the server requests a persistent connection; otherwise, + /// false. The default is true. + /// + /// + /// The response has been sent already. + /// + /// + /// This object is closed. + /// + public bool KeepAlive { + get { + return _keepAlive; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (HeadersSent) + throw new InvalidOperationException ( + "Cannot be changed after headers are sent."); + + _keepAlive = value; + } + } + + /// + /// Gets a to use to write the entity body data. + /// + /// + /// A to use to write the entity body data. + /// + /// + /// This object is closed. + /// + public Stream OutputStream { + get { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + return _outputStream ?? + (_outputStream = _context.Connection.GetResponseStream ()); + } + } + + /// + /// Gets or sets the HTTP version used in the response. + /// + /// + /// A that represents the HTTP version used in the + /// response. + /// + /// + /// The value specified for a set operation doesn't have its Major + /// property set to 1 or doesn't have its Minor property set to + /// either 0 or 1. + /// + /// + /// The value specified for a set operation is . + /// + /// + /// The response has been sent already. + /// + /// + /// This object is closed. + /// + public Version ProtocolVersion { + get { + return _version; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (HeadersSent) + throw new InvalidOperationException ( + "Cannot be changed after headers are sent."); + + if (value == null) + throw new ArgumentNullException ("value"); + + if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1)) + throw new ArgumentException ("Must be 1.0 or 1.1.", "value"); + + _version = value; + } + } + + /// + /// Gets or sets the URL to which the client is redirected to locate + /// a requested resource. + /// + /// + /// A that represents the value of the Location + /// response-header field. + /// + /// + /// The value specified for a set operation is empty. + /// + /// + /// The response has been sent already. + /// + /// + /// This object is closed. + /// + public string RedirectLocation { + get { + return _location; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (HeadersSent) + throw new InvalidOperationException ( + "Cannot be changed after headers are sent."); + + if (value.Length == 0) + throw new ArgumentException ( + "Must not be empty.", "value"); + + _location = value; + } + } + + /// + /// Gets or sets a value indicating whether the response uses the chunked + /// transfer encoding. + /// + /// + /// true if the response uses the chunked transfer encoding; + /// otherwise, false. + /// + /// + /// The response has been sent already. + /// + /// + /// This object is closed. + /// + public bool SendChunked { + get { + return _chunked; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (HeadersSent) + throw new InvalidOperationException ( + "Cannot be changed after headers are sent."); + + _chunked = value; + } + } + + /// + /// Gets or sets the HTTP status code returned to the client. + /// + /// + /// An that represents the HTTP status code for the + /// response to the request. The default is . + /// + /// + /// The response has been sent already. + /// + /// + /// The value specified for a set operation is invalid. Valid values are + /// between 100 and 999. + /// + /// + /// This object is closed. + /// + public int StatusCode { + get { + return _statusCode; + } + + set { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (HeadersSent) + throw new InvalidOperationException ( + "Cannot be changed after headers are sent."); + + if (value < 100 || value > 999) + throw new System.Net.ProtocolViolationException ( + "StatusCode must be between 100 and 999."); + + _statusCode = value; + _statusDescription = value.GetStatusDescription (); + } + } + + /// + /// Gets or sets the description of the HTTP status code returned to the + /// client. + /// + /// + /// A that represents the description of the HTTP status + /// code returned to the client. + /// + public string StatusDescription { + get { + return _statusDescription; + } + + set { + _statusDescription = value == null || value.Length == 0 + ? _statusCode.GetStatusDescription () + : value; + } + } + + #endregion + + #region Private Methods + + private bool canAddOrUpdate (Cookie cookie) + { + if (Cookies.Count == 0) + return true; + + var found = findCookie (cookie); + if (found.Count () == 0) + return true; + + foreach (var c in found) + if (c.Version == cookie.Version) + return true; + + return false; + } + + private void close (bool force) + { + _disposed = true; + _context.Connection.Close (force); + } + + private IEnumerable findCookie (Cookie cookie) + { + var name = cookie.Name; + var domain = cookie.Domain; + var path = cookie.Path; + + return from Cookie c in Cookies + where c.Name.Equals (name, StringComparison.OrdinalIgnoreCase) && + c.Domain.Equals (domain, StringComparison.OrdinalIgnoreCase) && + c.Path.Equals (path, StringComparison.Ordinal) + select c; + } + + #endregion + + #region Internal Methods + + internal void SendHeaders (bool closing, MemoryStream stream) + { + if (_contentType != null) { + if (_contentEncoding != null && + _contentType.IndexOf ("charset=", StringComparison.Ordinal) == -1) { + var charset = _contentEncoding.WebName; + _headers.SetInternal ( + "Content-Type", _contentType + "; charset=" + charset, true); + } + else { + _headers.SetInternal ("Content-Type", _contentType, true); + } + } + + if (_headers ["Server"] == null) + _headers.SetInternal ("Server", "websocket-sharp/1.0", true); + + var provider = CultureInfo.InvariantCulture; + if (_headers ["Date"] == null) + _headers.SetInternal ( + "Date", DateTime.UtcNow.ToString ("r", provider), true); + + if (!_chunked) { + if (!_contentLengthSet && closing) { + _contentLengthSet = true; + _contentLength = 0; + } + + if (_contentLengthSet) + _headers.SetInternal ( + "Content-Length", _contentLength.ToString (provider), true); + } + + var version = _context.Request.ProtocolVersion; + if (!_contentLengthSet && !_chunked && version >= HttpVersion.Version11) + _chunked = true; + + /* Apache forces closing the connection for these status codes: + * HttpStatusCode.BadRequest 400 + * HttpStatusCode.RequestTimeout 408 + * HttpStatusCode.LengthRequired 411 + * HttpStatusCode.RequestEntityTooLarge 413 + * HttpStatusCode.RequestUriTooLong 414 + * HttpStatusCode.InternalServerError 500 + * HttpStatusCode.ServiceUnavailable 503 + */ + var connClose = _statusCode == 400 || + _statusCode == 408 || + _statusCode == 411 || + _statusCode == 413 || + _statusCode == 414 || + _statusCode == 500 || + _statusCode == 503; + + if (!connClose) + connClose = !_context.Request.KeepAlive; + + // They sent both KeepAlive: true and Connection: close!? + if (!_keepAlive || connClose) { + _headers.SetInternal ("Connection", "close", true); + connClose = true; + } + + if (_chunked) + _headers.SetInternal ("Transfer-Encoding", "chunked", true); + + int reuses = _context.Connection.Reuses; + if (reuses >= 100) { + _forceCloseChunked = true; + if (!connClose) { + _headers.SetInternal ("Connection", "close", true); + connClose = true; + } + } + + if (!connClose) { + _headers.SetInternal ( + "Keep-Alive", + String.Format ("timeout=15,max={0}", 100 - reuses), true); + if (_context.Request.ProtocolVersion <= HttpVersion.Version10) + _headers.SetInternal ("Connection", "keep-alive", true); + } + + if (_location != null) + _headers.SetInternal ("Location", _location, true); + + if (_cookies != null) { + foreach (Cookie cookie in _cookies) + _headers.SetInternal ("Set-Cookie", cookie.ToResponseString (), true); + } + + var encoding = _contentEncoding ?? Encoding.Default; + var writer = new StreamWriter (stream, encoding, 256); + writer.Write ( + "HTTP/{0} {1} {2}\r\n", _version, _statusCode, _statusDescription); + var headers = _headers.ToStringMultiValue (true); + writer.Write (headers); + writer.Flush (); + var preamble = encoding.CodePage == 65001 ? 3 : encoding.GetPreamble ().Length; + if (_outputStream == null) + _outputStream = _context.Connection.GetResponseStream (); + + // Assumes that the stream was at position 0. + stream.Position = preamble; + HeadersSent = true; + } + + #endregion + + #region Public Methods + + /// + /// Closes the connection to the client without sending a response. + /// + public void Abort () + { + if (_disposed) + return; + + close (true); + } + + /// + /// Adds the specified HTTP header and + /// to the headers for this response. + /// + /// + /// A that contains the name of the HTTP header to add. + /// + /// + /// A that contains the value of the HTTP header to add. + /// + /// + /// is or empty. + /// + /// + /// The length of is greater than 65,535 characters. + /// + public void AddHeader (string name, string value) + { + if (name == null || name.Length == 0) + throw new ArgumentNullException ("name"); + + // TODO: Check for forbidden headers and invalid characters. + if (value.Length > 65535) + throw new ArgumentOutOfRangeException ( + "value", "Greater than 65,535 characters."); + + _headers.Set (name, value); + } + + /// + /// Adds the specified to the sent + /// with the response. + /// + /// + /// A to add to the . + /// + /// + /// is . + /// + public void AppendCookie (Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + Cookies.Add (cookie); + } + + /// + /// Appends a to the specified HTTP header sent with + /// the response. + /// + /// + /// A that contains the name of the HTTP header to + /// append to. + /// + /// + /// A that contains the value to append to the HTTP + /// header. + /// + /// + /// is or empty. + /// + /// + /// The length of is greater than 65,535 characters. + /// + public void AppendHeader (string name, string value) + { + // TODO: Check for forbidden headers and invalid characters. + if (name == null || name.Length == 0) + throw new ArgumentException ("Must not be null or empty.", "name"); + + if (value.Length > 65535) + throw new ArgumentOutOfRangeException ( + "value", "Greater than 65,535 characters."); + + _headers.Add (name, value); + } + + /// + /// Sends the response to the client and releases the resources associated + /// with the instance. + /// + public void Close () + { + if (_disposed) + return; + + close (false); + } + + /// + /// Sends the response with the specified array of to the + /// client and releases the resources associated with the + /// instance. + /// + /// + /// An array of that contains the response entity body + /// data. + /// + /// + /// true if this method blocks execution while flushing the stream to + /// the client; otherwise, false. + /// + /// + /// is . + /// + /// + /// This object is closed. + /// + public void Close (byte [] responseEntity, bool willBlock) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (responseEntity == null) + throw new ArgumentNullException ("responseEntity"); + + // TODO: If willBlock -> BeginWrite + Close? + ContentLength64 = responseEntity.Length; + OutputStream.Write (responseEntity, 0, (int) _contentLength); + close (false); + } + + /// + /// Copies properties from the specified + /// to this response. + /// + /// + /// A to copy. + /// + public void CopyFrom (HttpListenerResponse templateResponse) + { + _headers.Clear (); + _headers.Add (templateResponse._headers); + _contentLength = templateResponse._contentLength; + _statusCode = templateResponse._statusCode; + _statusDescription = templateResponse._statusDescription; + _keepAlive = templateResponse._keepAlive; + _version = templateResponse._version; + } + + /// + /// Configures the response to redirect the client's request to the specified + /// . + /// + /// + /// A that represents the URL to redirect the client's + /// request to. + /// + public void Redirect (string url) + { + StatusCode = (int) HttpStatusCode.Redirect; + _location = url; + } + + /// + /// Adds or updates a in the sent + /// with the response. + /// + /// + /// A to set. + /// + /// + /// already exists in the and + /// could not be replaced. + /// + /// + /// is . + /// + public void SetCookie (Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + if (!canAddOrUpdate (cookie)) + throw new ArgumentException ("Cannot be replaced.", "cookie"); + + Cookies.Add (cookie); + } + + #endregion + + #region Explicit Interface Implementation + + /// + /// Releases all resource used by the . + /// + void IDisposable.Dispose () + { + // TODO: Abort or Close? + close (true); + } + + #endregion + } } diff --git a/websocket-sharp/Net/HttpUtility.cs b/websocket-sharp/Net/HttpUtility.cs index 871d32f7..5258bb48 100644 --- a/websocket-sharp/Net/HttpUtility.cs +++ b/websocket-sharp/Net/HttpUtility.cs @@ -1,36 +1,43 @@ #region License -// -// HttpUtility.cs -// Copied from System.Net.HttpUtility.cs -// -// Authors: -// Patrik Torstensson (Patrik.Torstensson@labs2.com) -// Wictor Wilén (decode/encode functions) (wictor@ibizkit.se) -// Tim Coleman (tim@timcoleman.com) -// Gonzalo Paniagua Javier (gonzalo@ximian.com) -// -// Copyright (C) 2005-2009 Novell, Inc (http://www.novell.com) -// Copyright (c) 2012-2013 sta.blockhead (sta.blockhead@gmail.com) -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// +/* + * HttpUtility.cs + * + * This code is derived from System.Net.HttpUtility.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005-2009 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2013 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * Patrik Torstensson + * Wictor Wilén (decode/encode functions) + * Tim Coleman + * Gonzalo Paniagua Javier + */ #endregion using System; @@ -40,1148 +47,1291 @@ using System.Collections.Specialized; using System.Globalization; using System.IO; using System.Text; - -namespace WebSocketSharp.Net { - - internal sealed class HttpUtility { - - sealed class HttpQSCollection : NameValueCollection - { - public override string ToString () - { - var count = Count; - if (count == 0) - return String.Empty; - - var output = new StringBuilder (); - var keys = AllKeys; - foreach (var key in keys) - output.AppendFormat ("{0}={1}&", key, this [key]); - - if (output.Length > 0) - output.Length--; - - return output.ToString (); - } - } - - #region Private Static Fields - - private static Dictionary _entities; - private static char [] _hexChars = "0123456789abcdef".ToCharArray (); - private static object _sync = new object (); - - #endregion - - #region Private Static Properties - - private static Dictionary Entities { - get { - lock (_sync) { - if (_entities == null) - InitEntities (); - - return _entities; - } - } - } - - #endregion - - #region Private Methods - - private static int GetChar (byte [] bytes, int offset, int length) - { - var value = 0; - var end = length + offset; - int current; - for (int i = offset; i < end; i++) { - current = GetInt (bytes [i]); - if (current == -1) - return -1; - - value = (value << 4) + current; - } - - return value; - } - - private static int GetChar (string s, int offset, int length) - { - var value = 0; - var end = length + offset; - char c; - int current; - for (int i = offset; i < end; i++) { - c = s [i]; - if (c > 127) - return -1; - - current = GetInt ((byte) c); - if (current == -1) - return -1; - - value = (value << 4) + current; - } - - return value; - } - - private static char [] GetChars (MemoryStream buffer, Encoding encoding) - { - return encoding.GetChars (buffer.GetBuffer (), 0, (int) buffer.Length); - } - - private static int GetInt (byte b) - { - var c = (char) b; - return c >= '0' && c <= '9' - ? c - '0' - : c >= 'a' && c <= 'f' - ? c - 'a' + 10 - : c >= 'A' && c <= 'F' - ? c - 'A' + 10 - : -1; - } - - private static void InitEntities () - { - // Build the dictionary of HTML entity references. - // This list comes from the HTML 4.01 W3C recommendation. - _entities = new Dictionary (); - _entities.Add ("nbsp", '\u00A0'); - _entities.Add ("iexcl", '\u00A1'); - _entities.Add ("cent", '\u00A2'); - _entities.Add ("pound", '\u00A3'); - _entities.Add ("curren", '\u00A4'); - _entities.Add ("yen", '\u00A5'); - _entities.Add ("brvbar", '\u00A6'); - _entities.Add ("sect", '\u00A7'); - _entities.Add ("uml", '\u00A8'); - _entities.Add ("copy", '\u00A9'); - _entities.Add ("ordf", '\u00AA'); - _entities.Add ("laquo", '\u00AB'); - _entities.Add ("not", '\u00AC'); - _entities.Add ("shy", '\u00AD'); - _entities.Add ("reg", '\u00AE'); - _entities.Add ("macr", '\u00AF'); - _entities.Add ("deg", '\u00B0'); - _entities.Add ("plusmn", '\u00B1'); - _entities.Add ("sup2", '\u00B2'); - _entities.Add ("sup3", '\u00B3'); - _entities.Add ("acute", '\u00B4'); - _entities.Add ("micro", '\u00B5'); - _entities.Add ("para", '\u00B6'); - _entities.Add ("middot", '\u00B7'); - _entities.Add ("cedil", '\u00B8'); - _entities.Add ("sup1", '\u00B9'); - _entities.Add ("ordm", '\u00BA'); - _entities.Add ("raquo", '\u00BB'); - _entities.Add ("frac14", '\u00BC'); - _entities.Add ("frac12", '\u00BD'); - _entities.Add ("frac34", '\u00BE'); - _entities.Add ("iquest", '\u00BF'); - _entities.Add ("Agrave", '\u00C0'); - _entities.Add ("Aacute", '\u00C1'); - _entities.Add ("Acirc", '\u00C2'); - _entities.Add ("Atilde", '\u00C3'); - _entities.Add ("Auml", '\u00C4'); - _entities.Add ("Aring", '\u00C5'); - _entities.Add ("AElig", '\u00C6'); - _entities.Add ("Ccedil", '\u00C7'); - _entities.Add ("Egrave", '\u00C8'); - _entities.Add ("Eacute", '\u00C9'); - _entities.Add ("Ecirc", '\u00CA'); - _entities.Add ("Euml", '\u00CB'); - _entities.Add ("Igrave", '\u00CC'); - _entities.Add ("Iacute", '\u00CD'); - _entities.Add ("Icirc", '\u00CE'); - _entities.Add ("Iuml", '\u00CF'); - _entities.Add ("ETH", '\u00D0'); - _entities.Add ("Ntilde", '\u00D1'); - _entities.Add ("Ograve", '\u00D2'); - _entities.Add ("Oacute", '\u00D3'); - _entities.Add ("Ocirc", '\u00D4'); - _entities.Add ("Otilde", '\u00D5'); - _entities.Add ("Ouml", '\u00D6'); - _entities.Add ("times", '\u00D7'); - _entities.Add ("Oslash", '\u00D8'); - _entities.Add ("Ugrave", '\u00D9'); - _entities.Add ("Uacute", '\u00DA'); - _entities.Add ("Ucirc", '\u00DB'); - _entities.Add ("Uuml", '\u00DC'); - _entities.Add ("Yacute", '\u00DD'); - _entities.Add ("THORN", '\u00DE'); - _entities.Add ("szlig", '\u00DF'); - _entities.Add ("agrave", '\u00E0'); - _entities.Add ("aacute", '\u00E1'); - _entities.Add ("acirc", '\u00E2'); - _entities.Add ("atilde", '\u00E3'); - _entities.Add ("auml", '\u00E4'); - _entities.Add ("aring", '\u00E5'); - _entities.Add ("aelig", '\u00E6'); - _entities.Add ("ccedil", '\u00E7'); - _entities.Add ("egrave", '\u00E8'); - _entities.Add ("eacute", '\u00E9'); - _entities.Add ("ecirc", '\u00EA'); - _entities.Add ("euml", '\u00EB'); - _entities.Add ("igrave", '\u00EC'); - _entities.Add ("iacute", '\u00ED'); - _entities.Add ("icirc", '\u00EE'); - _entities.Add ("iuml", '\u00EF'); - _entities.Add ("eth", '\u00F0'); - _entities.Add ("ntilde", '\u00F1'); - _entities.Add ("ograve", '\u00F2'); - _entities.Add ("oacute", '\u00F3'); - _entities.Add ("ocirc", '\u00F4'); - _entities.Add ("otilde", '\u00F5'); - _entities.Add ("ouml", '\u00F6'); - _entities.Add ("divide", '\u00F7'); - _entities.Add ("oslash", '\u00F8'); - _entities.Add ("ugrave", '\u00F9'); - _entities.Add ("uacute", '\u00FA'); - _entities.Add ("ucirc", '\u00FB'); - _entities.Add ("uuml", '\u00FC'); - _entities.Add ("yacute", '\u00FD'); - _entities.Add ("thorn", '\u00FE'); - _entities.Add ("yuml", '\u00FF'); - _entities.Add ("fnof", '\u0192'); - _entities.Add ("Alpha", '\u0391'); - _entities.Add ("Beta", '\u0392'); - _entities.Add ("Gamma", '\u0393'); - _entities.Add ("Delta", '\u0394'); - _entities.Add ("Epsilon", '\u0395'); - _entities.Add ("Zeta", '\u0396'); - _entities.Add ("Eta", '\u0397'); - _entities.Add ("Theta", '\u0398'); - _entities.Add ("Iota", '\u0399'); - _entities.Add ("Kappa", '\u039A'); - _entities.Add ("Lambda", '\u039B'); - _entities.Add ("Mu", '\u039C'); - _entities.Add ("Nu", '\u039D'); - _entities.Add ("Xi", '\u039E'); - _entities.Add ("Omicron", '\u039F'); - _entities.Add ("Pi", '\u03A0'); - _entities.Add ("Rho", '\u03A1'); - _entities.Add ("Sigma", '\u03A3'); - _entities.Add ("Tau", '\u03A4'); - _entities.Add ("Upsilon", '\u03A5'); - _entities.Add ("Phi", '\u03A6'); - _entities.Add ("Chi", '\u03A7'); - _entities.Add ("Psi", '\u03A8'); - _entities.Add ("Omega", '\u03A9'); - _entities.Add ("alpha", '\u03B1'); - _entities.Add ("beta", '\u03B2'); - _entities.Add ("gamma", '\u03B3'); - _entities.Add ("delta", '\u03B4'); - _entities.Add ("epsilon", '\u03B5'); - _entities.Add ("zeta", '\u03B6'); - _entities.Add ("eta", '\u03B7'); - _entities.Add ("theta", '\u03B8'); - _entities.Add ("iota", '\u03B9'); - _entities.Add ("kappa", '\u03BA'); - _entities.Add ("lambda", '\u03BB'); - _entities.Add ("mu", '\u03BC'); - _entities.Add ("nu", '\u03BD'); - _entities.Add ("xi", '\u03BE'); - _entities.Add ("omicron", '\u03BF'); - _entities.Add ("pi", '\u03C0'); - _entities.Add ("rho", '\u03C1'); - _entities.Add ("sigmaf", '\u03C2'); - _entities.Add ("sigma", '\u03C3'); - _entities.Add ("tau", '\u03C4'); - _entities.Add ("upsilon", '\u03C5'); - _entities.Add ("phi", '\u03C6'); - _entities.Add ("chi", '\u03C7'); - _entities.Add ("psi", '\u03C8'); - _entities.Add ("omega", '\u03C9'); - _entities.Add ("thetasym", '\u03D1'); - _entities.Add ("upsih", '\u03D2'); - _entities.Add ("piv", '\u03D6'); - _entities.Add ("bull", '\u2022'); - _entities.Add ("hellip", '\u2026'); - _entities.Add ("prime", '\u2032'); - _entities.Add ("Prime", '\u2033'); - _entities.Add ("oline", '\u203E'); - _entities.Add ("frasl", '\u2044'); - _entities.Add ("weierp", '\u2118'); - _entities.Add ("image", '\u2111'); - _entities.Add ("real", '\u211C'); - _entities.Add ("trade", '\u2122'); - _entities.Add ("alefsym", '\u2135'); - _entities.Add ("larr", '\u2190'); - _entities.Add ("uarr", '\u2191'); - _entities.Add ("rarr", '\u2192'); - _entities.Add ("darr", '\u2193'); - _entities.Add ("harr", '\u2194'); - _entities.Add ("crarr", '\u21B5'); - _entities.Add ("lArr", '\u21D0'); - _entities.Add ("uArr", '\u21D1'); - _entities.Add ("rArr", '\u21D2'); - _entities.Add ("dArr", '\u21D3'); - _entities.Add ("hArr", '\u21D4'); - _entities.Add ("forall", '\u2200'); - _entities.Add ("part", '\u2202'); - _entities.Add ("exist", '\u2203'); - _entities.Add ("empty", '\u2205'); - _entities.Add ("nabla", '\u2207'); - _entities.Add ("isin", '\u2208'); - _entities.Add ("notin", '\u2209'); - _entities.Add ("ni", '\u220B'); - _entities.Add ("prod", '\u220F'); - _entities.Add ("sum", '\u2211'); - _entities.Add ("minus", '\u2212'); - _entities.Add ("lowast", '\u2217'); - _entities.Add ("radic", '\u221A'); - _entities.Add ("prop", '\u221D'); - _entities.Add ("infin", '\u221E'); - _entities.Add ("ang", '\u2220'); - _entities.Add ("and", '\u2227'); - _entities.Add ("or", '\u2228'); - _entities.Add ("cap", '\u2229'); - _entities.Add ("cup", '\u222A'); - _entities.Add ("int", '\u222B'); - _entities.Add ("there4", '\u2234'); - _entities.Add ("sim", '\u223C'); - _entities.Add ("cong", '\u2245'); - _entities.Add ("asymp", '\u2248'); - _entities.Add ("ne", '\u2260'); - _entities.Add ("equiv", '\u2261'); - _entities.Add ("le", '\u2264'); - _entities.Add ("ge", '\u2265'); - _entities.Add ("sub", '\u2282'); - _entities.Add ("sup", '\u2283'); - _entities.Add ("nsub", '\u2284'); - _entities.Add ("sube", '\u2286'); - _entities.Add ("supe", '\u2287'); - _entities.Add ("oplus", '\u2295'); - _entities.Add ("otimes", '\u2297'); - _entities.Add ("perp", '\u22A5'); - _entities.Add ("sdot", '\u22C5'); - _entities.Add ("lceil", '\u2308'); - _entities.Add ("rceil", '\u2309'); - _entities.Add ("lfloor", '\u230A'); - _entities.Add ("rfloor", '\u230B'); - _entities.Add ("lang", '\u2329'); - _entities.Add ("rang", '\u232A'); - _entities.Add ("loz", '\u25CA'); - _entities.Add ("spades", '\u2660'); - _entities.Add ("clubs", '\u2663'); - _entities.Add ("hearts", '\u2665'); - _entities.Add ("diams", '\u2666'); - _entities.Add ("quot", '\u0022'); - _entities.Add ("amp", '\u0026'); - _entities.Add ("lt", '\u003C'); - _entities.Add ("gt", '\u003E'); - _entities.Add ("OElig", '\u0152'); - _entities.Add ("oelig", '\u0153'); - _entities.Add ("Scaron", '\u0160'); - _entities.Add ("scaron", '\u0161'); - _entities.Add ("Yuml", '\u0178'); - _entities.Add ("circ", '\u02C6'); - _entities.Add ("tilde", '\u02DC'); - _entities.Add ("ensp", '\u2002'); - _entities.Add ("emsp", '\u2003'); - _entities.Add ("thinsp", '\u2009'); - _entities.Add ("zwnj", '\u200C'); - _entities.Add ("zwj", '\u200D'); - _entities.Add ("lrm", '\u200E'); - _entities.Add ("rlm", '\u200F'); - _entities.Add ("ndash", '\u2013'); - _entities.Add ("mdash", '\u2014'); - _entities.Add ("lsquo", '\u2018'); - _entities.Add ("rsquo", '\u2019'); - _entities.Add ("sbquo", '\u201A'); - _entities.Add ("ldquo", '\u201C'); - _entities.Add ("rdquo", '\u201D'); - _entities.Add ("bdquo", '\u201E'); - _entities.Add ("dagger", '\u2020'); - _entities.Add ("Dagger", '\u2021'); - _entities.Add ("permil", '\u2030'); - _entities.Add ("lsaquo", '\u2039'); - _entities.Add ("rsaquo", '\u203A'); - _entities.Add ("euro", '\u20AC'); - } - - private static bool NotEncoded (char c) - { - return c == '!' || - c == '\'' || - c == '(' || - c == ')' || - c == '*' || - c == '-' || - c == '.' || - c == '_'; - } - - private static void UrlEncodeChar (char c, Stream result, bool isUnicode) - { - if (c > 255) { - // FIXME: What happens when there is an internal error ? - //if (!isUnicode) - // throw new ArgumentOutOfRangeException ("c", c, "c must be less than 256."); - result.WriteByte ((byte) '%'); - result.WriteByte ((byte) 'u'); - var i = (int) c; - var idx = i >> 12; - result.WriteByte ((byte) _hexChars [idx]); - idx = (i >> 8) & 0x0F; - result.WriteByte ((byte) _hexChars [idx]); - idx = (i >> 4) & 0x0F; - result.WriteByte ((byte) _hexChars [idx]); - idx = i & 0x0F; - result.WriteByte ((byte) _hexChars [idx]); - - return; - } - - if (c > ' ' && NotEncoded (c)) { - result.WriteByte ((byte) c); - return; - } - - if (c == ' ') { - result.WriteByte ((byte) '+'); - return; - } - - if ((c < '0') || - (c < 'A' && c > '9') || - (c > 'Z' && c < 'a') || - (c > 'z')) { - if (isUnicode && c > 127) { - result.WriteByte ((byte) '%'); - result.WriteByte ((byte) 'u'); - result.WriteByte ((byte) '0'); - result.WriteByte ((byte) '0'); - } - else - result.WriteByte ((byte) '%'); - - var idx = ((int) c) >> 4; - result.WriteByte ((byte) _hexChars [idx]); - idx = ((int) c) & 0x0F; - result.WriteByte ((byte) _hexChars [idx]); - } - else - result.WriteByte ((byte) c); - } - - private static void UrlPathEncodeChar (char c, Stream result) - { - if (c < 33 || c > 126) { - var bytes = Encoding.UTF8.GetBytes (c.ToString ()); - int i; - foreach (var b in bytes) { - result.WriteByte ((byte) '%'); - i = ((int) b) >> 4; - result.WriteByte ((byte) _hexChars [i]); - i = ((int) b) & 0x0F; - result.WriteByte ((byte) _hexChars [i]); - } - } - else if (c == ' ') { - result.WriteByte ((byte) '%'); - result.WriteByte ((byte) '2'); - result.WriteByte ((byte) '0'); - } - else - result.WriteByte ((byte) c); - } - - private static void WriteCharBytes (IList buffer, char c, Encoding encoding) - { - if (c > 255) - foreach (var b in encoding.GetBytes (new char[] { c })) - buffer.Add (b); - else - buffer.Add ((byte) c); - } - - #endregion - - #region Internal Methods - - internal static void ParseQueryString (string query, Encoding encoding, NameValueCollection result) - { - if (query.Length == 0) - return; - - var decoded = HtmlDecode (query); - var decodedLength = decoded.Length; - var namePos = 0; - var first = true; - while (namePos <= decodedLength) { - int valuePos = -1, valueEnd = -1; - for (int q = namePos; q < decodedLength; q++) { - if (valuePos == -1 && decoded [q] == '=') { - valuePos = q + 1; - } - else if (decoded [q] == '&') { - valueEnd = q; - break; - } - } - - if (first) { - first = false; - if (decoded [namePos] == '?') - namePos++; - } - - string name, value; - if (valuePos == -1) { - name = null; - valuePos = namePos; - } - else { - name = UrlDecode (decoded.Substring (namePos, valuePos - namePos - 1), encoding); - } - - if (valueEnd < 0) { - namePos = -1; - valueEnd = decoded.Length; - } - else { - namePos = valueEnd + 1; - } - - value = UrlDecode (decoded.Substring (valuePos, valueEnd - valuePos), encoding); - result.Add (name, value); - if (namePos == -1) - break; - } - } - - internal static string UrlDecodeInternal (byte [] bytes, int offset, int count, Encoding encoding) - { - var output = new StringBuilder (); - var acc = new MemoryStream (); - var end = count + offset; - int xchar; - for (int i = offset; i < end; i++) { - if (bytes [i] == '%' && i + 2 < count && bytes [i + 1] != '%') { - if (bytes [i + 1] == (byte) 'u' && i + 5 < end) { - if (acc.Length > 0) { - output.Append (GetChars (acc, encoding)); - acc.SetLength (0); - } - - xchar = GetChar (bytes, i + 2, 4); - if (xchar != -1) { - output.Append ((char) xchar); - i += 5; - - continue; - } - } - else if ((xchar = GetChar (bytes, i + 1, 2)) != -1) { - acc.WriteByte ((byte) xchar); - i += 2; - - continue; - } - } - - if (acc.Length > 0) { - output.Append (GetChars (acc, encoding)); - acc.SetLength (0); - } - - if (bytes [i] == '+') { - output.Append (' '); - } - else { - output.Append ((char) bytes [i]); - } - } - - if (acc.Length > 0) { - output.Append (GetChars (acc, encoding)); - } - - acc = null; - return output.ToString (); - } - - internal static byte [] UrlDecodeToBytesInternal (byte [] bytes, int offset, int count) - { - var result = new MemoryStream (); - var end = offset + count; - char c; - int xchar; - for (int i = offset; i < end; i++) { - c = (char) bytes [i]; - if (c == '+') { - c = ' '; - } - else if (c == '%' && i < end - 2) { - xchar = GetChar (bytes, i + 1, 2); - if (xchar != -1) { - c = (char) xchar; - i += 2; - } - } - - result.WriteByte ((byte) c); - } - - return result.ToArray (); - } - - internal static byte [] UrlEncodeToBytesInternal (byte [] bytes, int offset, int count) - { - var result = new MemoryStream (count); - var end = offset + count; - for (int i = offset; i < end; i++) - UrlEncodeChar ((char) bytes [i], result, false); - - return result.ToArray (); - } - - internal static byte [] UrlEncodeUnicodeToBytesInternal (string s) - { - var result = new MemoryStream (s.Length); - foreach (var c in s) - UrlEncodeChar (c, result, true); - - return result.ToArray (); - } - - #endregion - - #region Public Methods - - public static string HtmlAttributeEncode (string s) - { - if (s == null || s.Length == 0 || !s.Contains ('&', '"', '<', '>')) - return s; - - var output = new StringBuilder (); - foreach (var c in s) - output.Append ( - c == '&' - ? "&" - : c == '"' - ? """ - : c == '<' - ? "<" - : c == '>' - ? ">" - : c.ToString () - ); - - return output.ToString (); - } - - public static void HtmlAttributeEncode (string s, TextWriter output) - { - if (output == null) - throw new ArgumentNullException ("output"); - - output.Write (HtmlAttributeEncode (s)); - } - - /// - /// Decodes an HTML-encoded and returns the decoded . - /// - /// - /// A that contains the decoded string. - /// - /// - /// A to decode. - /// - public static string HtmlDecode (string s) - { - if (s == null || s.Length == 0 || !s.Contains ('&')) - return s; - - var entity = new StringBuilder (); - var output = new StringBuilder (); - // 0 -> nothing, - // 1 -> right after '&' - // 2 -> between '&' and ';' but no '#' - // 3 -> '#' found after '&' and getting numbers - var state = 0; - var number = 0; - var haveTrailingDigits = false; - foreach (var c in s) { - if (state == 0) { - if (c == '&') { - entity.Append (c); - state = 1; - } - else { - output.Append (c); - } - - continue; - } - - if (c == '&') { - state = 1; - if (haveTrailingDigits) { - entity.Append (number.ToString (CultureInfo.InvariantCulture)); - haveTrailingDigits = false; - } - - output.Append (entity.ToString ()); - entity.Length = 0; - entity.Append ('&'); - - continue; - } - - if (state == 1) { - if (c == ';') { - state = 0; - output.Append (entity.ToString ()); - output.Append (c); - entity.Length = 0; - } - else { - number = 0; - if (c != '#') { - state = 2; - } - else { - state = 3; - } - - entity.Append (c); - } - } - else if (state == 2) { - entity.Append (c); - if (c == ';') { - var key = entity.ToString (); - if (key.Length > 1 && Entities.ContainsKey (key.Substring (1, key.Length - 2))) - key = Entities [key.Substring (1, key.Length - 2)].ToString (); - - output.Append (key); - state = 0; - entity.Length = 0; - } - } - else if (state == 3) { - if (c == ';') { - if (number > 65535) { - output.Append ("&#"); - output.Append (number.ToString (CultureInfo.InvariantCulture)); - output.Append (";"); - } - else { - output.Append ((char) number); - } - - state = 0; - entity.Length = 0; - haveTrailingDigits = false; - } - else if (Char.IsDigit (c)) { - number = number * 10 + ((int) c - '0'); - haveTrailingDigits = true; - } - else { - state = 2; - if (haveTrailingDigits) { - entity.Append (number.ToString (CultureInfo.InvariantCulture)); - haveTrailingDigits = false; - } - - entity.Append (c); - } - } - } - - if (entity.Length > 0) { - output.Append (entity.ToString ()); - } - else if (haveTrailingDigits) { - output.Append (number.ToString (CultureInfo.InvariantCulture)); - } - - return output.ToString (); - } - - /// - /// Decodes an HTML-encoded and sends the decoded - /// to a . - /// - /// - /// A to decode. - /// - /// - /// A that receives the decoded . - /// - public static void HtmlDecode (string s, TextWriter output) - { - if (output == null) - throw new ArgumentNullException ("output"); - - output.Write (HtmlDecode (s)); - } - - /// - /// HTML-encodes a and returns the encoded . - /// - /// - /// A that contains the encoded string. - /// - /// - /// A to encode. - /// - public static string HtmlEncode (string s) - { - if (s == null || s.Length == 0) - return s; - - var needEncode = false; - foreach (var c in s) { - if (c == '&' || c == '"' || c == '<' || c == '>' || c > 159) { - needEncode = true; - break; - } - } - - if (!needEncode) - return s; - - var output = new StringBuilder (); - foreach (var c in s) { - if (c == '&') - output.Append ("&"); - else if (c == '"') - output.Append ("""); - else if (c == '<') - output.Append ("<"); - else if (c == '>') - output.Append (">"); - else { - // MS starts encoding with &# from 160 and stops at 255. - // We don't do that. One reason is the 65308/65310 unicode - // characters that look like '<' and '>'. - if (c > 159) { - output.Append ("&#"); - output.Append (((int) c).ToString (CultureInfo.InvariantCulture)); - output.Append (";"); - } - else { - output.Append (c); - } - } - } - - return output.ToString (); - } - - /// - /// HTML-encodes a and sends the encoded - /// to a . - /// - /// - /// A to encode. - /// - /// - /// A that receives the encoded . - /// - public static void HtmlEncode (string s, TextWriter output) - { - if (output == null) - throw new ArgumentNullException ("output"); - - output.Write (HtmlEncode (s)); - } - - public static NameValueCollection ParseQueryString (string query) - { - return ParseQueryString (query, Encoding.UTF8); - } - - public static NameValueCollection ParseQueryString (string query, Encoding encoding) - { - if (query == null) - throw new ArgumentNullException ("query"); - - var length = query.Length; - if (length == 0 || (length == 1 && query [0] == '?')) - return new NameValueCollection (); - - if (query [0] == '?') - query = query.Substring (1); - - if (encoding == null) - encoding = Encoding.UTF8; - - var result = new HttpQSCollection (); - ParseQueryString (query, encoding, result); - - return result; - } - - public static string UrlDecode (string s) - { - return UrlDecode (s, Encoding.UTF8); - } - - public static string UrlDecode (string s, Encoding encoding) - { - if (s == null || s.Length == 0 || !s.Contains ('%', '+')) - return s; - - if (encoding == null) - encoding = Encoding.UTF8; - - var length = s.Length; - var bytes = new List (); - char c; - int xchar; - for (int i = 0; i < length; i++) { - c = s [i]; - if (c == '%' && i + 2 < length && s [i + 1] != '%') { - if (s [i + 1] == 'u' && i + 5 < length) { - // Unicode hex sequence. - xchar = GetChar (s, i + 2, 4); - if (xchar != -1) { - WriteCharBytes (bytes, (char) xchar, encoding); - i += 5; - } - else - WriteCharBytes (bytes, '%', encoding); - } - else if ((xchar = GetChar (s, i + 1, 2)) != -1) { - WriteCharBytes (bytes, (char) xchar, encoding); - i += 2; - } - else { - WriteCharBytes (bytes, '%', encoding); - } - - continue; - } - - if (c == '+') - WriteCharBytes (bytes, ' ', encoding); - else - WriteCharBytes (bytes, c, encoding); - } - - var buffer = bytes.ToArray (); - return encoding.GetString (buffer); - } - - public static string UrlDecode (byte [] bytes, Encoding encoding) - { - if (encoding == null) - encoding = Encoding.UTF8; - - int length; - return bytes == null - ? null - : (length = bytes.Length) == 0 - ? String.Empty - : UrlDecodeInternal (bytes, 0, length, encoding); - } - - public static string UrlDecode (byte [] bytes, int offset, int count, Encoding encoding) - { - if (bytes == null) - return null; - - var length = bytes.Length; - if (length == 0 || count == 0) - return String.Empty; - - if (offset < 0 || offset >= length) - throw new ArgumentOutOfRangeException ("offset"); - - if (count < 0 || count > length - offset) - throw new ArgumentOutOfRangeException ("count"); - - if (encoding == null) - encoding = Encoding.UTF8; - - return UrlDecodeInternal (bytes, offset, count, encoding); - } - - public static byte [] UrlDecodeToBytes (byte [] bytes) - { - int length; - return bytes == null || (length = bytes.Length) == 0 - ? bytes - : UrlDecodeToBytesInternal (bytes, 0, length); - } - - public static byte [] UrlDecodeToBytes (string s) - { - return UrlDecodeToBytes (s, Encoding.UTF8); - } - - public static byte [] UrlDecodeToBytes (string s, Encoding encoding) - { - if (s == null) - return null; - - if (s.Length == 0) - return new byte [0]; - - if (encoding == null) - encoding = Encoding.UTF8; - - var bytes = encoding.GetBytes (s); - return UrlDecodeToBytesInternal (bytes, 0, bytes.Length); - } - - public static byte [] UrlDecodeToBytes (byte [] bytes, int offset, int count) - { - int length; - if (bytes == null || (length = bytes.Length) == 0) - return bytes; - - if (count == 0) - return new byte [0]; - - if (offset < 0 || offset >= length) - throw new ArgumentOutOfRangeException ("offset"); - - if (count < 0 || count > length - offset ) - throw new ArgumentOutOfRangeException ("count"); - - return UrlDecodeToBytesInternal (bytes, offset, count); - } - - public static string UrlEncode (byte [] bytes) - { - int length; - return bytes == null - ? null - : (length = bytes.Length) == 0 - ? String.Empty - : Encoding.ASCII.GetString (UrlEncodeToBytesInternal (bytes, 0, length)); - } - - public static string UrlEncode (string s) - { - return UrlEncode (s, Encoding.UTF8); - } - - public static string UrlEncode (string s, Encoding encoding) - { - int length; - if (s == null || (length = s.Length) == 0) - return s; - - var needEncode = false; - foreach (var c in s) { - if ((c < '0') || (c < 'A' && c > '9') || (c > 'Z' && c < 'a') || (c > 'z')) { - if (NotEncoded (c)) - continue; - - needEncode = true; - break; - } - } - - if (!needEncode) - return s; - - if (encoding == null) - encoding = Encoding.UTF8; - - // Avoided GetByteCount call. - var bytes = new byte [encoding.GetMaxByteCount (length)]; - var realLen = encoding.GetBytes (s, 0, length, bytes, 0); - - return Encoding.ASCII.GetString (UrlEncodeToBytesInternal (bytes, 0, realLen)); - } +using System.Security.Cryptography; + +namespace WebSocketSharp.Net +{ + internal sealed class HttpUtility + { + sealed class HttpQSCollection : NameValueCollection + { + public override string ToString () + { + var count = Count; + if (count == 0) + return String.Empty; + + var output = new StringBuilder (); + var keys = AllKeys; + foreach (var key in keys) + output.AppendFormat ("{0}={1}&", key, this [key]); + + if (output.Length > 0) + output.Length--; + + return output.ToString (); + } + } + + #region Private Static Fields + + private static Dictionary _entities; + private static char [] _hexChars = "0123456789abcdef".ToCharArray (); + private static object _sync = new object (); + + #endregion + + #region Private Static Properties + + private static Dictionary Entities { + get { + lock (_sync) { + if (_entities == null) + InitEntities (); + + return _entities; + } + } + } + + #endregion + + #region Private Methods + + private static string A1 (string username, string password, string realm) + { + return String.Format ("{0}:{1}:{2}", username, realm, password); + } + + private static string A1 ( + string username, + string password, + string realm, + string nonce, + string cnonce) + { + return String.Format ( + "{0}:{1}:{2}", Hash (A1 (username, password, realm)), nonce, cnonce); + } + + private static string A2 (string method, string uri) + { + return String.Format ("{0}:{1}", method, uri); + } + + private static string A2 (string method, string uri, string entity) + { + return String.Format ("{0}:{1}:{2}", method, uri, entity); + } + + private static int GetChar (byte [] bytes, int offset, int length) + { + var value = 0; + var end = length + offset; + int current; + for (int i = offset; i < end; i++) { + current = GetInt (bytes [i]); + if (current == -1) + return -1; + + value = (value << 4) + current; + } + + return value; + } + + private static int GetChar (string s, int offset, int length) + { + var value = 0; + var end = length + offset; + char c; + int current; + for (int i = offset; i < end; i++) { + c = s [i]; + if (c > 127) + return -1; + + current = GetInt ((byte) c); + if (current == -1) + return -1; + + value = (value << 4) + current; + } + + return value; + } + + private static char [] GetChars (MemoryStream buffer, Encoding encoding) + { + return encoding.GetChars (buffer.GetBuffer (), 0, (int) buffer.Length); + } + + private static int GetInt (byte b) + { + var c = (char) b; + return c >= '0' && c <= '9' + ? c - '0' + : c >= 'a' && c <= 'f' + ? c - 'a' + 10 + : c >= 'A' && c <= 'F' + ? c - 'A' + 10 + : -1; + } + + private static string Hash (string value) + { + var md5 = MD5.Create (); + var src = Encoding.UTF8.GetBytes (value); + var hashed = md5.ComputeHash (src); + var result = new StringBuilder (64); + foreach (var b in hashed) + result.Append (b.ToString ("x2")); + + return result.ToString (); + } + + private static void InitEntities () + { + // Build the dictionary of HTML entity references. + // This list comes from the HTML 4.01 W3C recommendation. + _entities = new Dictionary (); + _entities.Add ("nbsp", '\u00A0'); + _entities.Add ("iexcl", '\u00A1'); + _entities.Add ("cent", '\u00A2'); + _entities.Add ("pound", '\u00A3'); + _entities.Add ("curren", '\u00A4'); + _entities.Add ("yen", '\u00A5'); + _entities.Add ("brvbar", '\u00A6'); + _entities.Add ("sect", '\u00A7'); + _entities.Add ("uml", '\u00A8'); + _entities.Add ("copy", '\u00A9'); + _entities.Add ("ordf", '\u00AA'); + _entities.Add ("laquo", '\u00AB'); + _entities.Add ("not", '\u00AC'); + _entities.Add ("shy", '\u00AD'); + _entities.Add ("reg", '\u00AE'); + _entities.Add ("macr", '\u00AF'); + _entities.Add ("deg", '\u00B0'); + _entities.Add ("plusmn", '\u00B1'); + _entities.Add ("sup2", '\u00B2'); + _entities.Add ("sup3", '\u00B3'); + _entities.Add ("acute", '\u00B4'); + _entities.Add ("micro", '\u00B5'); + _entities.Add ("para", '\u00B6'); + _entities.Add ("middot", '\u00B7'); + _entities.Add ("cedil", '\u00B8'); + _entities.Add ("sup1", '\u00B9'); + _entities.Add ("ordm", '\u00BA'); + _entities.Add ("raquo", '\u00BB'); + _entities.Add ("frac14", '\u00BC'); + _entities.Add ("frac12", '\u00BD'); + _entities.Add ("frac34", '\u00BE'); + _entities.Add ("iquest", '\u00BF'); + _entities.Add ("Agrave", '\u00C0'); + _entities.Add ("Aacute", '\u00C1'); + _entities.Add ("Acirc", '\u00C2'); + _entities.Add ("Atilde", '\u00C3'); + _entities.Add ("Auml", '\u00C4'); + _entities.Add ("Aring", '\u00C5'); + _entities.Add ("AElig", '\u00C6'); + _entities.Add ("Ccedil", '\u00C7'); + _entities.Add ("Egrave", '\u00C8'); + _entities.Add ("Eacute", '\u00C9'); + _entities.Add ("Ecirc", '\u00CA'); + _entities.Add ("Euml", '\u00CB'); + _entities.Add ("Igrave", '\u00CC'); + _entities.Add ("Iacute", '\u00CD'); + _entities.Add ("Icirc", '\u00CE'); + _entities.Add ("Iuml", '\u00CF'); + _entities.Add ("ETH", '\u00D0'); + _entities.Add ("Ntilde", '\u00D1'); + _entities.Add ("Ograve", '\u00D2'); + _entities.Add ("Oacute", '\u00D3'); + _entities.Add ("Ocirc", '\u00D4'); + _entities.Add ("Otilde", '\u00D5'); + _entities.Add ("Ouml", '\u00D6'); + _entities.Add ("times", '\u00D7'); + _entities.Add ("Oslash", '\u00D8'); + _entities.Add ("Ugrave", '\u00D9'); + _entities.Add ("Uacute", '\u00DA'); + _entities.Add ("Ucirc", '\u00DB'); + _entities.Add ("Uuml", '\u00DC'); + _entities.Add ("Yacute", '\u00DD'); + _entities.Add ("THORN", '\u00DE'); + _entities.Add ("szlig", '\u00DF'); + _entities.Add ("agrave", '\u00E0'); + _entities.Add ("aacute", '\u00E1'); + _entities.Add ("acirc", '\u00E2'); + _entities.Add ("atilde", '\u00E3'); + _entities.Add ("auml", '\u00E4'); + _entities.Add ("aring", '\u00E5'); + _entities.Add ("aelig", '\u00E6'); + _entities.Add ("ccedil", '\u00E7'); + _entities.Add ("egrave", '\u00E8'); + _entities.Add ("eacute", '\u00E9'); + _entities.Add ("ecirc", '\u00EA'); + _entities.Add ("euml", '\u00EB'); + _entities.Add ("igrave", '\u00EC'); + _entities.Add ("iacute", '\u00ED'); + _entities.Add ("icirc", '\u00EE'); + _entities.Add ("iuml", '\u00EF'); + _entities.Add ("eth", '\u00F0'); + _entities.Add ("ntilde", '\u00F1'); + _entities.Add ("ograve", '\u00F2'); + _entities.Add ("oacute", '\u00F3'); + _entities.Add ("ocirc", '\u00F4'); + _entities.Add ("otilde", '\u00F5'); + _entities.Add ("ouml", '\u00F6'); + _entities.Add ("divide", '\u00F7'); + _entities.Add ("oslash", '\u00F8'); + _entities.Add ("ugrave", '\u00F9'); + _entities.Add ("uacute", '\u00FA'); + _entities.Add ("ucirc", '\u00FB'); + _entities.Add ("uuml", '\u00FC'); + _entities.Add ("yacute", '\u00FD'); + _entities.Add ("thorn", '\u00FE'); + _entities.Add ("yuml", '\u00FF'); + _entities.Add ("fnof", '\u0192'); + _entities.Add ("Alpha", '\u0391'); + _entities.Add ("Beta", '\u0392'); + _entities.Add ("Gamma", '\u0393'); + _entities.Add ("Delta", '\u0394'); + _entities.Add ("Epsilon", '\u0395'); + _entities.Add ("Zeta", '\u0396'); + _entities.Add ("Eta", '\u0397'); + _entities.Add ("Theta", '\u0398'); + _entities.Add ("Iota", '\u0399'); + _entities.Add ("Kappa", '\u039A'); + _entities.Add ("Lambda", '\u039B'); + _entities.Add ("Mu", '\u039C'); + _entities.Add ("Nu", '\u039D'); + _entities.Add ("Xi", '\u039E'); + _entities.Add ("Omicron", '\u039F'); + _entities.Add ("Pi", '\u03A0'); + _entities.Add ("Rho", '\u03A1'); + _entities.Add ("Sigma", '\u03A3'); + _entities.Add ("Tau", '\u03A4'); + _entities.Add ("Upsilon", '\u03A5'); + _entities.Add ("Phi", '\u03A6'); + _entities.Add ("Chi", '\u03A7'); + _entities.Add ("Psi", '\u03A8'); + _entities.Add ("Omega", '\u03A9'); + _entities.Add ("alpha", '\u03B1'); + _entities.Add ("beta", '\u03B2'); + _entities.Add ("gamma", '\u03B3'); + _entities.Add ("delta", '\u03B4'); + _entities.Add ("epsilon", '\u03B5'); + _entities.Add ("zeta", '\u03B6'); + _entities.Add ("eta", '\u03B7'); + _entities.Add ("theta", '\u03B8'); + _entities.Add ("iota", '\u03B9'); + _entities.Add ("kappa", '\u03BA'); + _entities.Add ("lambda", '\u03BB'); + _entities.Add ("mu", '\u03BC'); + _entities.Add ("nu", '\u03BD'); + _entities.Add ("xi", '\u03BE'); + _entities.Add ("omicron", '\u03BF'); + _entities.Add ("pi", '\u03C0'); + _entities.Add ("rho", '\u03C1'); + _entities.Add ("sigmaf", '\u03C2'); + _entities.Add ("sigma", '\u03C3'); + _entities.Add ("tau", '\u03C4'); + _entities.Add ("upsilon", '\u03C5'); + _entities.Add ("phi", '\u03C6'); + _entities.Add ("chi", '\u03C7'); + _entities.Add ("psi", '\u03C8'); + _entities.Add ("omega", '\u03C9'); + _entities.Add ("thetasym", '\u03D1'); + _entities.Add ("upsih", '\u03D2'); + _entities.Add ("piv", '\u03D6'); + _entities.Add ("bull", '\u2022'); + _entities.Add ("hellip", '\u2026'); + _entities.Add ("prime", '\u2032'); + _entities.Add ("Prime", '\u2033'); + _entities.Add ("oline", '\u203E'); + _entities.Add ("frasl", '\u2044'); + _entities.Add ("weierp", '\u2118'); + _entities.Add ("image", '\u2111'); + _entities.Add ("real", '\u211C'); + _entities.Add ("trade", '\u2122'); + _entities.Add ("alefsym", '\u2135'); + _entities.Add ("larr", '\u2190'); + _entities.Add ("uarr", '\u2191'); + _entities.Add ("rarr", '\u2192'); + _entities.Add ("darr", '\u2193'); + _entities.Add ("harr", '\u2194'); + _entities.Add ("crarr", '\u21B5'); + _entities.Add ("lArr", '\u21D0'); + _entities.Add ("uArr", '\u21D1'); + _entities.Add ("rArr", '\u21D2'); + _entities.Add ("dArr", '\u21D3'); + _entities.Add ("hArr", '\u21D4'); + _entities.Add ("forall", '\u2200'); + _entities.Add ("part", '\u2202'); + _entities.Add ("exist", '\u2203'); + _entities.Add ("empty", '\u2205'); + _entities.Add ("nabla", '\u2207'); + _entities.Add ("isin", '\u2208'); + _entities.Add ("notin", '\u2209'); + _entities.Add ("ni", '\u220B'); + _entities.Add ("prod", '\u220F'); + _entities.Add ("sum", '\u2211'); + _entities.Add ("minus", '\u2212'); + _entities.Add ("lowast", '\u2217'); + _entities.Add ("radic", '\u221A'); + _entities.Add ("prop", '\u221D'); + _entities.Add ("infin", '\u221E'); + _entities.Add ("ang", '\u2220'); + _entities.Add ("and", '\u2227'); + _entities.Add ("or", '\u2228'); + _entities.Add ("cap", '\u2229'); + _entities.Add ("cup", '\u222A'); + _entities.Add ("int", '\u222B'); + _entities.Add ("there4", '\u2234'); + _entities.Add ("sim", '\u223C'); + _entities.Add ("cong", '\u2245'); + _entities.Add ("asymp", '\u2248'); + _entities.Add ("ne", '\u2260'); + _entities.Add ("equiv", '\u2261'); + _entities.Add ("le", '\u2264'); + _entities.Add ("ge", '\u2265'); + _entities.Add ("sub", '\u2282'); + _entities.Add ("sup", '\u2283'); + _entities.Add ("nsub", '\u2284'); + _entities.Add ("sube", '\u2286'); + _entities.Add ("supe", '\u2287'); + _entities.Add ("oplus", '\u2295'); + _entities.Add ("otimes", '\u2297'); + _entities.Add ("perp", '\u22A5'); + _entities.Add ("sdot", '\u22C5'); + _entities.Add ("lceil", '\u2308'); + _entities.Add ("rceil", '\u2309'); + _entities.Add ("lfloor", '\u230A'); + _entities.Add ("rfloor", '\u230B'); + _entities.Add ("lang", '\u2329'); + _entities.Add ("rang", '\u232A'); + _entities.Add ("loz", '\u25CA'); + _entities.Add ("spades", '\u2660'); + _entities.Add ("clubs", '\u2663'); + _entities.Add ("hearts", '\u2665'); + _entities.Add ("diams", '\u2666'); + _entities.Add ("quot", '\u0022'); + _entities.Add ("amp", '\u0026'); + _entities.Add ("lt", '\u003C'); + _entities.Add ("gt", '\u003E'); + _entities.Add ("OElig", '\u0152'); + _entities.Add ("oelig", '\u0153'); + _entities.Add ("Scaron", '\u0160'); + _entities.Add ("scaron", '\u0161'); + _entities.Add ("Yuml", '\u0178'); + _entities.Add ("circ", '\u02C6'); + _entities.Add ("tilde", '\u02DC'); + _entities.Add ("ensp", '\u2002'); + _entities.Add ("emsp", '\u2003'); + _entities.Add ("thinsp", '\u2009'); + _entities.Add ("zwnj", '\u200C'); + _entities.Add ("zwj", '\u200D'); + _entities.Add ("lrm", '\u200E'); + _entities.Add ("rlm", '\u200F'); + _entities.Add ("ndash", '\u2013'); + _entities.Add ("mdash", '\u2014'); + _entities.Add ("lsquo", '\u2018'); + _entities.Add ("rsquo", '\u2019'); + _entities.Add ("sbquo", '\u201A'); + _entities.Add ("ldquo", '\u201C'); + _entities.Add ("rdquo", '\u201D'); + _entities.Add ("bdquo", '\u201E'); + _entities.Add ("dagger", '\u2020'); + _entities.Add ("Dagger", '\u2021'); + _entities.Add ("permil", '\u2030'); + _entities.Add ("lsaquo", '\u2039'); + _entities.Add ("rsaquo", '\u203A'); + _entities.Add ("euro", '\u20AC'); + } + + private static string KD (string secret, string data) + { + return Hash (String.Format ("{0}:{1}", secret, data)); + } + + private static bool NotEncoded (char c) + { + return c == '!' || + c == '\'' || + c == '(' || + c == ')' || + c == '*' || + c == '-' || + c == '.' || + c == '_'; + } + + private static void UrlEncodeChar (char c, Stream result, bool isUnicode) + { + if (c > 255) { + // FIXME: What happens when there is an internal error ? + //if (!isUnicode) + // throw new ArgumentOutOfRangeException ("c", c, "c must be less than 256."); + result.WriteByte ((byte) '%'); + result.WriteByte ((byte) 'u'); + var i = (int) c; + var idx = i >> 12; + result.WriteByte ((byte) _hexChars [idx]); + idx = (i >> 8) & 0x0F; + result.WriteByte ((byte) _hexChars [idx]); + idx = (i >> 4) & 0x0F; + result.WriteByte ((byte) _hexChars [idx]); + idx = i & 0x0F; + result.WriteByte ((byte) _hexChars [idx]); + + return; + } + + if (c > ' ' && NotEncoded (c)) { + result.WriteByte ((byte) c); + return; + } + + if (c == ' ') { + result.WriteByte ((byte) '+'); + return; + } + + if ((c < '0') || + (c < 'A' && c > '9') || + (c > 'Z' && c < 'a') || + (c > 'z')) { + if (isUnicode && c > 127) { + result.WriteByte ((byte) '%'); + result.WriteByte ((byte) 'u'); + result.WriteByte ((byte) '0'); + result.WriteByte ((byte) '0'); + } + else + result.WriteByte ((byte) '%'); + + var idx = ((int) c) >> 4; + result.WriteByte ((byte) _hexChars [idx]); + idx = ((int) c) & 0x0F; + result.WriteByte ((byte) _hexChars [idx]); + } + else + result.WriteByte ((byte) c); + } + + private static void UrlPathEncodeChar (char c, Stream result) + { + if (c < 33 || c > 126) { + var bytes = Encoding.UTF8.GetBytes (c.ToString ()); + int i; + foreach (var b in bytes) { + result.WriteByte ((byte) '%'); + i = ((int) b) >> 4; + result.WriteByte ((byte) _hexChars [i]); + i = ((int) b) & 0x0F; + result.WriteByte ((byte) _hexChars [i]); + } + } + else if (c == ' ') { + result.WriteByte ((byte) '%'); + result.WriteByte ((byte) '2'); + result.WriteByte ((byte) '0'); + } + else + result.WriteByte ((byte) c); + } + + private static void WriteCharBytes (IList buffer, char c, Encoding encoding) + { + if (c > 255) + foreach (var b in encoding.GetBytes (new char[] { c })) + buffer.Add (b); + else + buffer.Add ((byte) c); + } + + #endregion + + #region Internal Methods + + internal static string CreateBasicAuthChallenge (string realm) + { + return String.Format ("Basic realm=\"{0}\"", realm); + } + + internal static string CreateBasicAuthCredentials ( + string username, string password) + { + var userPass = String.Format ("{0}:{1}", username, password); + var base64UserPass = Convert.ToBase64String ( + Encoding.UTF8.GetBytes (userPass)); + + return "Basic " + base64UserPass; + } + + internal static string CreateDigestAuthChallenge (string realm) + { + var nonce = CreateNonceValue (); + var algorithm = "MD5"; + var qop = "auth"; + + return String.Format ( + "Digest realm=\"{0}\", nonce=\"{1}\", algorithm={2}, qop=\"{3}\"", + realm, + nonce, + algorithm, + qop); + } + + internal static string CreateDigestAuthCredentials ( + NameValueCollection authParams) + { + var digestRes = new StringBuilder (64); + digestRes.AppendFormat ("username=\"{0}\"", authParams ["username"]); + digestRes.AppendFormat (", realm=\"{0}\"", authParams ["realm"]); + digestRes.AppendFormat (", nonce=\"{0}\"", authParams ["nonce"]); + digestRes.AppendFormat (", uri=\"{0}\"", authParams ["uri"]); + + var algorithm = authParams ["algorithm"]; + if (algorithm != null) + digestRes.AppendFormat (", algorithm={0}", algorithm); + + digestRes.AppendFormat (", response=\"{0}\"", authParams ["response"]); + + var qop = authParams ["qop"]; + if (qop != null) { + digestRes.AppendFormat (", qop={0}", qop); + digestRes.AppendFormat (", nc={0}", authParams ["nc"]); + digestRes.AppendFormat (", cnonce=\"{0}\"", authParams ["cnonce"]); + } + + var opaque = authParams ["opaque"]; + if (opaque != null) + digestRes.AppendFormat (", opaque=\"{0}\"", opaque); + + return "Digest " + digestRes.ToString (); + } + + internal static string CreateNonceValue () + { + var src = new byte [16]; + var rand = new Random (); + rand.NextBytes (src); + var nonce = new StringBuilder (32); + foreach (var b in src) + nonce.Append (b.ToString ("x2")); + + return nonce.ToString (); + } + + internal static string CreateRequestDigest (NameValueCollection parameters) + { + var username = parameters ["username"]; + var password = parameters ["password"]; + var realm = parameters ["realm"]; + var nonce = parameters ["nonce"]; + var uri = parameters ["uri"]; + var algorithm = parameters ["algorithm"]; + var qop = parameters ["qop"]; + var nc = parameters ["nc"]; + var cnonce = parameters ["cnonce"]; + var method = parameters ["method"]; + + var a1 = algorithm != null && algorithm.ToLower () == "md5-sess" + ? A1 (username, password, realm, nonce, cnonce) + : A1 (username, password, realm); + + var a2 = qop != null && qop.ToLower () == "auth-int" + ? A2 (method, uri, parameters ["entity"]) + : A2 (method, uri); + + var secret = Hash (a1); + var data = qop != null + ? String.Format ("{0}:{1}:{2}:{3}:{4}", nonce, nc, cnonce, qop, Hash (a2)) + : String.Format ("{0}:{1}", nonce, Hash (a2)); + + return KD (secret, data); + } + + internal static void ParseQueryString (string query, Encoding encoding, NameValueCollection result) + { + if (query.Length == 0) + return; + + var decoded = HtmlDecode (query); + var decodedLength = decoded.Length; + var namePos = 0; + var first = true; + while (namePos <= decodedLength) { + int valuePos = -1, valueEnd = -1; + for (int q = namePos; q < decodedLength; q++) { + if (valuePos == -1 && decoded [q] == '=') { + valuePos = q + 1; + } + else if (decoded [q] == '&') { + valueEnd = q; + break; + } + } + + if (first) { + first = false; + if (decoded [namePos] == '?') + namePos++; + } + + string name, value; + if (valuePos == -1) { + name = null; + valuePos = namePos; + } + else { + name = UrlDecode (decoded.Substring (namePos, valuePos - namePos - 1), encoding); + } + + if (valueEnd < 0) { + namePos = -1; + valueEnd = decoded.Length; + } + else { + namePos = valueEnd + 1; + } + + value = UrlDecode (decoded.Substring (valuePos, valueEnd - valuePos), encoding); + result.Add (name, value); + if (namePos == -1) + break; + } + } + + internal static string UrlDecodeInternal (byte [] bytes, int offset, int count, Encoding encoding) + { + var output = new StringBuilder (); + var acc = new MemoryStream (); + var end = count + offset; + int xchar; + for (int i = offset; i < end; i++) { + if (bytes [i] == '%' && i + 2 < count && bytes [i + 1] != '%') { + if (bytes [i + 1] == (byte) 'u' && i + 5 < end) { + if (acc.Length > 0) { + output.Append (GetChars (acc, encoding)); + acc.SetLength (0); + } + + xchar = GetChar (bytes, i + 2, 4); + if (xchar != -1) { + output.Append ((char) xchar); + i += 5; + + continue; + } + } + else if ((xchar = GetChar (bytes, i + 1, 2)) != -1) { + acc.WriteByte ((byte) xchar); + i += 2; + + continue; + } + } + + if (acc.Length > 0) { + output.Append (GetChars (acc, encoding)); + acc.SetLength (0); + } + + if (bytes [i] == '+') { + output.Append (' '); + } + else { + output.Append ((char) bytes [i]); + } + } + + if (acc.Length > 0) { + output.Append (GetChars (acc, encoding)); + } + + acc = null; + return output.ToString (); + } + + internal static byte [] UrlDecodeToBytesInternal (byte [] bytes, int offset, int count) + { + var result = new MemoryStream (); + var end = offset + count; + char c; + int xchar; + for (int i = offset; i < end; i++) { + c = (char) bytes [i]; + if (c == '+') { + c = ' '; + } + else if (c == '%' && i < end - 2) { + xchar = GetChar (bytes, i + 1, 2); + if (xchar != -1) { + c = (char) xchar; + i += 2; + } + } + + result.WriteByte ((byte) c); + } + + return result.ToArray (); + } + + internal static byte [] UrlEncodeToBytesInternal (byte [] bytes, int offset, int count) + { + var result = new MemoryStream (count); + var end = offset + count; + for (int i = offset; i < end; i++) + UrlEncodeChar ((char) bytes [i], result, false); + + return result.ToArray (); + } + + internal static byte [] UrlEncodeUnicodeToBytesInternal (string s) + { + var result = new MemoryStream (s.Length); + foreach (var c in s) + UrlEncodeChar (c, result, true); + + return result.ToArray (); + } + + #endregion + + #region Public Methods + + public static string HtmlAttributeEncode (string s) + { + if (s == null || s.Length == 0 || !s.Contains ('&', '"', '<', '>')) + return s; + + var output = new StringBuilder (); + foreach (var c in s) + output.Append ( + c == '&' + ? "&" + : c == '"' + ? """ + : c == '<' + ? "<" + : c == '>' + ? ">" + : c.ToString () + ); + + return output.ToString (); + } + + public static void HtmlAttributeEncode (string s, TextWriter output) + { + if (output == null) + throw new ArgumentNullException ("output"); + + output.Write (HtmlAttributeEncode (s)); + } + + /// + /// Decodes an HTML-encoded and returns the decoded . + /// + /// + /// A that contains the decoded string. + /// + /// + /// A to decode. + /// + public static string HtmlDecode (string s) + { + if (s == null || s.Length == 0 || !s.Contains ('&')) + return s; + + var entity = new StringBuilder (); + var output = new StringBuilder (); + // 0 -> nothing, + // 1 -> right after '&' + // 2 -> between '&' and ';' but no '#' + // 3 -> '#' found after '&' and getting numbers + var state = 0; + var number = 0; + var haveTrailingDigits = false; + foreach (var c in s) { + if (state == 0) { + if (c == '&') { + entity.Append (c); + state = 1; + } + else { + output.Append (c); + } + + continue; + } + + if (c == '&') { + state = 1; + if (haveTrailingDigits) { + entity.Append (number.ToString (CultureInfo.InvariantCulture)); + haveTrailingDigits = false; + } + + output.Append (entity.ToString ()); + entity.Length = 0; + entity.Append ('&'); + + continue; + } + + if (state == 1) { + if (c == ';') { + state = 0; + output.Append (entity.ToString ()); + output.Append (c); + entity.Length = 0; + } + else { + number = 0; + if (c != '#') { + state = 2; + } + else { + state = 3; + } + + entity.Append (c); + } + } + else if (state == 2) { + entity.Append (c); + if (c == ';') { + var key = entity.ToString (); + if (key.Length > 1 && Entities.ContainsKey (key.Substring (1, key.Length - 2))) + key = Entities [key.Substring (1, key.Length - 2)].ToString (); + + output.Append (key); + state = 0; + entity.Length = 0; + } + } + else if (state == 3) { + if (c == ';') { + if (number > 65535) { + output.Append ("&#"); + output.Append (number.ToString (CultureInfo.InvariantCulture)); + output.Append (";"); + } + else { + output.Append ((char) number); + } + + state = 0; + entity.Length = 0; + haveTrailingDigits = false; + } + else if (Char.IsDigit (c)) { + number = number * 10 + ((int) c - '0'); + haveTrailingDigits = true; + } + else { + state = 2; + if (haveTrailingDigits) { + entity.Append (number.ToString (CultureInfo.InvariantCulture)); + haveTrailingDigits = false; + } + + entity.Append (c); + } + } + } + + if (entity.Length > 0) { + output.Append (entity.ToString ()); + } + else if (haveTrailingDigits) { + output.Append (number.ToString (CultureInfo.InvariantCulture)); + } + + return output.ToString (); + } + + /// + /// Decodes an HTML-encoded and sends the decoded + /// to a . + /// + /// + /// A to decode. + /// + /// + /// A that receives the decoded . + /// + public static void HtmlDecode (string s, TextWriter output) + { + if (output == null) + throw new ArgumentNullException ("output"); + + output.Write (HtmlDecode (s)); + } + + /// + /// HTML-encodes a and returns the encoded . + /// + /// + /// A that contains the encoded string. + /// + /// + /// A to encode. + /// + public static string HtmlEncode (string s) + { + if (s == null || s.Length == 0) + return s; + + var needEncode = false; + foreach (var c in s) { + if (c == '&' || c == '"' || c == '<' || c == '>' || c > 159) { + needEncode = true; + break; + } + } + + if (!needEncode) + return s; + + var output = new StringBuilder (); + foreach (var c in s) { + if (c == '&') + output.Append ("&"); + else if (c == '"') + output.Append ("""); + else if (c == '<') + output.Append ("<"); + else if (c == '>') + output.Append (">"); + else { + // MS starts encoding with &# from 160 and stops at 255. + // We don't do that. One reason is the 65308/65310 unicode + // characters that look like '<' and '>'. + if (c > 159) { + output.Append ("&#"); + output.Append (((int) c).ToString (CultureInfo.InvariantCulture)); + output.Append (";"); + } + else { + output.Append (c); + } + } + } + + return output.ToString (); + } + + /// + /// HTML-encodes a and sends the encoded + /// to a . + /// + /// + /// A to encode. + /// + /// + /// A that receives the encoded . + /// + public static void HtmlEncode (string s, TextWriter output) + { + if (output == null) + throw new ArgumentNullException ("output"); + + output.Write (HtmlEncode (s)); + } + + public static NameValueCollection ParseQueryString (string query) + { + return ParseQueryString (query, Encoding.UTF8); + } + + public static NameValueCollection ParseQueryString (string query, Encoding encoding) + { + if (query == null) + throw new ArgumentNullException ("query"); + + var length = query.Length; + if (length == 0 || (length == 1 && query [0] == '?')) + return new NameValueCollection (); + + if (query [0] == '?') + query = query.Substring (1); + + if (encoding == null) + encoding = Encoding.UTF8; + + var result = new HttpQSCollection (); + ParseQueryString (query, encoding, result); + + return result; + } + + public static string UrlDecode (string s) + { + return UrlDecode (s, Encoding.UTF8); + } + + public static string UrlDecode (string s, Encoding encoding) + { + if (s == null || s.Length == 0 || !s.Contains ('%', '+')) + return s; + + if (encoding == null) + encoding = Encoding.UTF8; + + var length = s.Length; + var bytes = new List (); + char c; + int xchar; + for (int i = 0; i < length; i++) { + c = s [i]; + if (c == '%' && i + 2 < length && s [i + 1] != '%') { + if (s [i + 1] == 'u' && i + 5 < length) { + // Unicode hex sequence. + xchar = GetChar (s, i + 2, 4); + if (xchar != -1) { + WriteCharBytes (bytes, (char) xchar, encoding); + i += 5; + } + else + WriteCharBytes (bytes, '%', encoding); + } + else if ((xchar = GetChar (s, i + 1, 2)) != -1) { + WriteCharBytes (bytes, (char) xchar, encoding); + i += 2; + } + else { + WriteCharBytes (bytes, '%', encoding); + } + + continue; + } + + if (c == '+') + WriteCharBytes (bytes, ' ', encoding); + else + WriteCharBytes (bytes, c, encoding); + } + + var buffer = bytes.ToArray (); + return encoding.GetString (buffer); + } + + public static string UrlDecode (byte [] bytes, Encoding encoding) + { + if (encoding == null) + encoding = Encoding.UTF8; + + int length; + return bytes == null + ? null + : (length = bytes.Length) == 0 + ? String.Empty + : UrlDecodeInternal (bytes, 0, length, encoding); + } + + public static string UrlDecode (byte [] bytes, int offset, int count, Encoding encoding) + { + if (bytes == null) + return null; + + var length = bytes.Length; + if (length == 0 || count == 0) + return String.Empty; + + if (offset < 0 || offset >= length) + throw new ArgumentOutOfRangeException ("offset"); + + if (count < 0 || count > length - offset) + throw new ArgumentOutOfRangeException ("count"); + + if (encoding == null) + encoding = Encoding.UTF8; + + return UrlDecodeInternal (bytes, offset, count, encoding); + } + + public static byte [] UrlDecodeToBytes (byte [] bytes) + { + int length; + return bytes == null || (length = bytes.Length) == 0 + ? bytes + : UrlDecodeToBytesInternal (bytes, 0, length); + } + + public static byte [] UrlDecodeToBytes (string s) + { + return UrlDecodeToBytes (s, Encoding.UTF8); + } + + public static byte [] UrlDecodeToBytes (string s, Encoding encoding) + { + if (s == null) + return null; + + if (s.Length == 0) + return new byte [0]; + + if (encoding == null) + encoding = Encoding.UTF8; + + var bytes = encoding.GetBytes (s); + return UrlDecodeToBytesInternal (bytes, 0, bytes.Length); + } + + public static byte [] UrlDecodeToBytes (byte [] bytes, int offset, int count) + { + int length; + if (bytes == null || (length = bytes.Length) == 0) + return bytes; + + if (count == 0) + return new byte [0]; + + if (offset < 0 || offset >= length) + throw new ArgumentOutOfRangeException ("offset"); + + if (count < 0 || count > length - offset ) + throw new ArgumentOutOfRangeException ("count"); + + return UrlDecodeToBytesInternal (bytes, offset, count); + } + + public static string UrlEncode (byte [] bytes) + { + int length; + return bytes == null + ? null + : (length = bytes.Length) == 0 + ? String.Empty + : Encoding.ASCII.GetString (UrlEncodeToBytesInternal (bytes, 0, length)); + } + + public static string UrlEncode (string s) + { + return UrlEncode (s, Encoding.UTF8); + } - public static string UrlEncode (byte [] bytes, int offset, int count) - { - var encoded = UrlEncodeToBytes (bytes, offset, count); - return encoded == null - ? null - : encoded.Length == 0 - ? String.Empty - : Encoding.ASCII.GetString (encoded); - } + public static string UrlEncode (string s, Encoding encoding) + { + int length; + if (s == null || (length = s.Length) == 0) + return s; - public static byte [] UrlEncodeToBytes (byte [] bytes) - { - int length; - return bytes == null || (length = bytes.Length) == 0 - ? bytes - : UrlEncodeToBytesInternal (bytes, 0, length); - } + var needEncode = false; + foreach (var c in s) { + if ((c < '0') || (c < 'A' && c > '9') || (c > 'Z' && c < 'a') || (c > 'z')) { + if (NotEncoded (c)) + continue; - public static byte [] UrlEncodeToBytes (string s) - { - return UrlEncodeToBytes (s, Encoding.UTF8); - } + needEncode = true; + break; + } + } - public static byte [] UrlEncodeToBytes (string s, Encoding encoding) - { - if (s == null) - return null; + if (!needEncode) + return s; - if (s.Length == 0) - return new byte [0]; + if (encoding == null) + encoding = Encoding.UTF8; - if (encoding == null) - encoding = Encoding.UTF8; + // Avoided GetByteCount call. + var bytes = new byte [encoding.GetMaxByteCount (length)]; + var realLen = encoding.GetBytes (s, 0, length, bytes, 0); - var bytes = encoding.GetBytes (s); - return UrlEncodeToBytesInternal (bytes, 0, bytes.Length); - } + return Encoding.ASCII.GetString (UrlEncodeToBytesInternal (bytes, 0, realLen)); + } + + public static string UrlEncode (byte [] bytes, int offset, int count) + { + var encoded = UrlEncodeToBytes (bytes, offset, count); + return encoded == null + ? null + : encoded.Length == 0 + ? String.Empty + : Encoding.ASCII.GetString (encoded); + } - public static byte [] UrlEncodeToBytes (byte [] bytes, int offset, int count) - { - int length; - if (bytes == null || (length = bytes.Length) == 0) - return bytes; + public static byte [] UrlEncodeToBytes (byte [] bytes) + { + int length; + return bytes == null || (length = bytes.Length) == 0 + ? bytes + : UrlEncodeToBytesInternal (bytes, 0, length); + } - if (count == 0) - return new byte [0]; + public static byte [] UrlEncodeToBytes (string s) + { + return UrlEncodeToBytes (s, Encoding.UTF8); + } - if (offset < 0 || offset >= length) - throw new ArgumentOutOfRangeException ("offset"); + public static byte [] UrlEncodeToBytes (string s, Encoding encoding) + { + if (s == null) + return null; - if (count < 0 || count > length - offset) - throw new ArgumentOutOfRangeException ("count"); + if (s.Length == 0) + return new byte [0]; - return UrlEncodeToBytesInternal (bytes, offset, count); - } + if (encoding == null) + encoding = Encoding.UTF8; - public static string UrlEncodeUnicode (string s) - { - return s == null || s.Length == 0 - ? s - : Encoding.ASCII.GetString (UrlEncodeUnicodeToBytesInternal (s)); - } + var bytes = encoding.GetBytes (s); + return UrlEncodeToBytesInternal (bytes, 0, bytes.Length); + } - public static byte [] UrlEncodeUnicodeToBytes (string s) - { - if (s == null) - return null; + public static byte [] UrlEncodeToBytes (byte [] bytes, int offset, int count) + { + int length; + if (bytes == null || (length = bytes.Length) == 0) + return bytes; - if (s.Length == 0) - return new byte [0]; + if (count == 0) + return new byte [0]; - return UrlEncodeUnicodeToBytesInternal (s); - } + if (offset < 0 || offset >= length) + throw new ArgumentOutOfRangeException ("offset"); - public static string UrlPathEncode (string s) - { - if (s == null || s.Length == 0) - return s; + if (count < 0 || count > length - offset) + throw new ArgumentOutOfRangeException ("count"); - var result = new MemoryStream (); - foreach (var c in s) - UrlPathEncodeChar (c, result); + return UrlEncodeToBytesInternal (bytes, offset, count); + } - return Encoding.ASCII.GetString (result.ToArray ()); - } + public static string UrlEncodeUnicode (string s) + { + return s == null || s.Length == 0 + ? s + : Encoding.ASCII.GetString (UrlEncodeUnicodeToBytesInternal (s)); + } - #endregion - } + public static byte [] UrlEncodeUnicodeToBytes (string s) + { + if (s == null) + return null; + + if (s.Length == 0) + return new byte [0]; + + return UrlEncodeUnicodeToBytesInternal (s); + } + + public static string UrlPathEncode (string s) + { + if (s == null || s.Length == 0) + return s; + + var result = new MemoryStream (); + foreach (var c in s) + UrlPathEncodeChar (c, result); + + return Encoding.ASCII.GetString (result.ToArray ()); + } + + #endregion + } } diff --git a/websocket-sharp/Net/ListenerAsyncResult.cs b/websocket-sharp/Net/ListenerAsyncResult.cs index fee4a4be..81c1145d 100644 --- a/websocket-sharp/Net/ListenerAsyncResult.cs +++ b/websocket-sharp/Net/ListenerAsyncResult.cs @@ -1,222 +1,200 @@ -// -// ListenerAsyncResult.cs -// Copied from System.Net.ListenerAsyncResult.cs -// -// Authors: -// Gonzalo Paniagua Javier (gonzalo@ximian.com) -// -// Copyright (c) 2005 Ximian, Inc (http://www.ximian.com) -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// +#region License +/* + * ListenerAsyncResult.cs + * + * This code is derived from System.Net.ListenerAsyncResult.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Ximian, Inc. (http://www.ximian.com) + * Copyright (c) 2012-2013 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * Gonzalo Paniagua Javier + */ +#endregion using System; -using System.Net; using System.Threading; -namespace WebSocketSharp.Net { +namespace WebSocketSharp.Net +{ + internal class ListenerAsyncResult : IAsyncResult + { + #region Private Fields - class ListenerAsyncResult : IAsyncResult - { - #region Private Static Field + private AsyncCallback _callback; + private bool _completed; + private HttpListenerContext _context; + private Exception _exception; + private ManualResetEvent _waitHandle; + private object _state; + private object _sync; + private bool _syncCompleted; - static WaitCallback InvokeCB = new WaitCallback (InvokeCallback); + #endregion - #endregion + #region Internal Fields - #region Private Fields + internal bool EndCalled; + internal bool InGet; - AsyncCallback cb; - bool completed; - HttpListenerContext context; - Exception exception; - ListenerAsyncResult forward; - ManualResetEvent handle; - object locker; - object state; - bool synch; + #endregion - #endregion + #region Public Constructors - #region Internal Fields + public ListenerAsyncResult (AsyncCallback callback, object state) + { + _callback = callback; + _state = state; + _sync = new object (); + } - internal bool EndCalled; - internal bool InGet; + #endregion - #endregion + #region Public Properties - #region Constructor + public object AsyncState { + get { + return _state; + } + } - public ListenerAsyncResult (AsyncCallback cb, object state) - { - this.cb = cb; - this.state = state; - this.locker = new object(); - } + public WaitHandle AsyncWaitHandle { + get { + lock (_sync) + return _waitHandle ?? (_waitHandle = new ManualResetEvent (_completed)); + } + } - #endregion + public bool CompletedSynchronously { + get { + return _syncCompleted; + } + } - #region Properties + public bool IsCompleted { + get { + lock (_sync) + return _completed; + } + } - public object AsyncState { - get { - if (forward != null) - return forward.AsyncState; + #endregion - return state; - } - } + #region Private Methods - public WaitHandle AsyncWaitHandle { - get { - if (forward != null) - return forward.AsyncWaitHandle; + private static void invokeCallback (object state) + { + try { + var ares = (ListenerAsyncResult) state; + ares._callback (ares); + } + catch { + } + } - lock (locker) { - if (handle == null) - handle = new ManualResetEvent (completed); - } + #endregion - return handle; - } - } + #region Internal Methods - public bool CompletedSynchronously { - get { - if (forward != null) - return forward.CompletedSynchronously; + internal void Complete (Exception exception) + { + _exception = InGet && (exception is ObjectDisposedException) + ? new HttpListenerException (500, "Listener closed") + : exception; - return synch; - } - } + lock (_sync) { + _completed = true; + if (_waitHandle != null) + _waitHandle.Set (); - public bool IsCompleted { - get { - if (forward != null) - return forward.IsCompleted; + if (_callback != null) + ThreadPool.UnsafeQueueUserWorkItem (invokeCallback, this); + } + } - lock (locker) { - return completed; - } - } - } + internal void Complete (HttpListenerContext context) + { + Complete (context, false); + } - #endregion + internal void Complete (HttpListenerContext context, bool syncCompleted) + { + var listener = context.Listener; + var scheme = listener.SelectAuthenticationScheme (context); + if (scheme == AuthenticationSchemes.None) { + context.Response.Close (HttpStatusCode.Forbidden); + listener.BeginGetContext (this); - #region Private Method + return; + } - static void InvokeCallback (object o) - { - ListenerAsyncResult ares = (ListenerAsyncResult) o; - if (ares.forward != null) { - InvokeCallback (ares.forward); - return; - } + var header = context.Request.Headers ["Authorization"]; + if (scheme == AuthenticationSchemes.Basic && + (header == null || + !header.StartsWith ("basic", StringComparison.OrdinalIgnoreCase))) { + context.Response.CloseWithAuthChallenge ( + HttpUtility.CreateBasicAuthChallenge (listener.Realm)); + listener.BeginGetContext (this); - try { - ares.cb (ares); - } catch { - } - } + return; + } - #endregion + if (scheme == AuthenticationSchemes.Digest && + (header == null || + !header.StartsWith ("digest", StringComparison.OrdinalIgnoreCase))) { + context.Response.CloseWithAuthChallenge ( + HttpUtility.CreateDigestAuthChallenge (listener.Realm)); + listener.BeginGetContext (this); - #region Internal Methods + return; + } - internal void Complete (Exception exc) - { - if (forward != null) { - forward.Complete (exc); - return; - } + _context = context; + _syncCompleted = syncCompleted; - exception = exc; - if (InGet && (exc is ObjectDisposedException)) - exception = new HttpListenerException (500, "Listener closed"); + lock (_sync) { + _completed = true; + if (_waitHandle != null) + _waitHandle.Set (); - lock (locker) { - completed = true; - if (handle != null) - handle.Set (); + if (_callback != null) + ThreadPool.UnsafeQueueUserWorkItem (invokeCallback, this); + } + } - if (cb != null) - ThreadPool.UnsafeQueueUserWorkItem (InvokeCB, this); - } - } + internal HttpListenerContext GetContext () + { + if (_exception != null) + throw _exception; - internal void Complete (HttpListenerContext context) - { - Complete (context, false); - } + return _context; + } - internal void Complete (HttpListenerContext context, bool synch) - { - if (forward != null) { - forward.Complete (context, synch); - return; - } - - this.synch = synch; - this.context = context; - lock (locker) { - AuthenticationSchemes schemes = context.Listener.SelectAuthenticationScheme (context); - if ((schemes == AuthenticationSchemes.Basic || context.Listener.AuthenticationSchemes == AuthenticationSchemes.Negotiate) && context.Request.Headers ["Authorization"] == null) { - context.Response.StatusCode = 401; - context.Response.Headers ["WWW-Authenticate"] = schemes + " realm=\"" + context.Listener.Realm + "\""; - context.Response.OutputStream.Close (); - IAsyncResult ares = context.Listener.BeginGetContext (cb, state); - this.forward = (ListenerAsyncResult) ares; - lock (forward.locker) { - if (handle != null) - forward.handle = handle; - } - - ListenerAsyncResult next = forward; - for (int i = 0; next.forward != null; i++) { - if (i > 20) - Complete (new HttpListenerException (400, "Too many authentication errors")); - - next = next.forward; - } - } else { - completed = true; - if (handle != null) - handle.Set (); - - if (cb != null) - ThreadPool.UnsafeQueueUserWorkItem (InvokeCB, this); - } - } - } - - internal HttpListenerContext GetContext () - { - if (forward != null) - return forward.GetContext (); - - if (exception != null) - throw exception; - - return context; - } - - #endregion - } + #endregion + } } diff --git a/websocket-sharp/Net/NetworkCredential.cs b/websocket-sharp/Net/NetworkCredential.cs new file mode 100644 index 00000000..e66394a9 --- /dev/null +++ b/websocket-sharp/Net/NetworkCredential.cs @@ -0,0 +1,179 @@ +#region License +/* + * NetworkCredential.cs + * + * The MIT License + * + * Copyright (c) 2014 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + /// + /// Provides the credentials for HTTP authentication (Basic/Digest). + /// + public class NetworkCredential + { + #region Private Fields + + private string _domain; + private string _password; + private string [] _roles; + private string _username; + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class + /// with the specified user name and password. + /// + /// + /// A that represents the user name associated with the + /// credentials. + /// + /// + /// A that represents the password for the user name + /// associated with the credentials. + /// + /// + /// is or empty. + /// + public NetworkCredential (string username, string password) + : this (username, password, null, new string [0]) + { + } + + /// + /// Initializes a new instance of the class + /// with the specified user name, password, domain, and roles. + /// + /// + /// A that represents the user name associated with the + /// credentials. + /// + /// + /// A that represents the password for the user name + /// associated with the credentials. + /// + /// + /// A that represents the name of the user domain + /// associated with the credentials. + /// + /// + /// An array of that contains the role names to which + /// the user associated with the credentials belongs if any. + /// + /// + /// is or empty. + /// + public NetworkCredential ( + string username, string password, string domain, params string [] roles) + { + if (username == null || username.Length == 0) + throw new ArgumentException ("Must not be null or empty.", "username"); + + _username = username; + _password = password; + _domain = domain; + _roles = roles; + } + + #endregion + + #region Public Properties + + /// + /// Gets the name of the user domain associated with the credentials. + /// + /// + /// A that represents the name of the user domain + /// associated with the credentials. + /// + public string Domain { + get { + return _domain ?? String.Empty; + } + + internal set { + _domain = value; + } + } + + /// + /// Gets the password for the user name associated with the credentials. + /// + /// + /// A that represents the password for the user name + /// associated with the credentials. + /// + public string Password { + get { + return _password ?? String.Empty; + } + + internal set { + _password = value; + } + } + + /// + /// Gets the role names to which the user associated with the credentials + /// belongs. + /// + /// + /// An array of that contains the role names to which + /// the user associated with the credentials belongs. + /// + public string [] Roles { + get { + return _roles; + } + + internal set { + _roles = value; + } + } + + /// + /// Gets the user name associated with the credentials. + /// + /// + /// A that represents the user name associated with the + /// credentials. + /// + public string UserName { + get { + return _username; + } + + internal set { + _username = value; + } + } + + #endregion + } +} diff --git a/websocket-sharp/Net/RequestStream.cs b/websocket-sharp/Net/RequestStream.cs index 2af57f6f..64eb615a 100644 --- a/websocket-sharp/Net/RequestStream.cs +++ b/websocket-sharp/Net/RequestStream.cs @@ -1,253 +1,281 @@ -// -// RequestStream.cs -// Copied from System.Net.RequestStream.cs -// -// Author: -// Gonzalo Paniagua Javier (gonzalo@novell.com) -// -// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// +#region License +/* + * RequestStream.cs + * + * This code is derived from System.Net.RequestStream.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2013 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * Gonzalo Paniagua Javier + */ +#endregion using System; using System.IO; -using System.Runtime.InteropServices; -namespace WebSocketSharp.Net { +namespace WebSocketSharp.Net +{ + internal class RequestStream : Stream + { + #region Private Fields - class RequestStream : Stream { + private byte [] _buffer; + private bool _disposed; + private int _length; + private int _offset; + private long _remainingBody; + private Stream _stream; - #region Fields + #endregion - byte [] buffer; - bool disposed; - int length; - int offset; - long remaining_body; - Stream stream; + #region Internal Constructors - #endregion + internal RequestStream ( + Stream stream, byte [] buffer, int offset, int length) + : this (stream, buffer, offset, length, -1) + { + } - #region Constructors + internal RequestStream ( + Stream stream, byte [] buffer, int offset, int length, long contentlength) + { + _stream = stream; + _buffer = buffer; + _offset = offset; + _length = length; + _remainingBody = contentlength; + } - internal RequestStream (Stream stream, byte [] buffer, int offset, int length) - : this (stream, buffer, offset, length, -1) - { - } + #endregion - internal RequestStream (Stream stream, byte [] buffer, int offset, int length, long contentlength) - { - this.stream = stream; - this.buffer = buffer; - this.offset = offset; - this.length = length; - this.remaining_body = contentlength; - } + #region Public Properties - #endregion + public override bool CanRead { + get { + return true; + } + } - #region Properties + public override bool CanSeek { + get { + return false; + } + } - public override bool CanRead { - get { return true; } - } + public override bool CanWrite { + get { + return false; + } + } - public override bool CanSeek { - get { return false; } - } + public override long Length { + get { + throw new NotSupportedException (); + } + } - public override bool CanWrite { - get { return false; } - } + public override long Position { + get { + throw new NotSupportedException (); + } - public override long Length { - get { throw new NotSupportedException (); } - } + set { + throw new NotSupportedException (); + } + } - public override long Position { - get { throw new NotSupportedException (); } - set { throw new NotSupportedException (); } - } + #endregion - #endregion + #region Private Methods - #region Private Method + // Returns 0 if we can keep reading from the base stream, + // > 0 if we read something from the buffer. + // -1 if we had a content length set and we finished reading that many bytes. + private int fillFromBuffer (byte [] buffer, int offset, int count) + { + if (buffer == null) + throw new ArgumentNullException ("buffer"); - // Returns 0 if we can keep reading from the base stream, - // > 0 if we read something from the buffer. - // -1 if we had a content length set and we finished reading that many bytes. - int FillFromBuffer (byte [] buffer, int offset, int count) - { - if (buffer == null) - throw new ArgumentNullException ("buffer"); + if (offset < 0) + throw new ArgumentOutOfRangeException ("offset", "Less than zero."); - if (offset < 0) - throw new ArgumentOutOfRangeException ("offset", "< 0"); + if (count < 0) + throw new ArgumentOutOfRangeException ("count", "Less than zero."); - if (count < 0) - throw new ArgumentOutOfRangeException ("count", "< 0"); + var len = buffer.Length; + if (offset > len) + throw new ArgumentException ("'offset' is greater than 'buffer' size."); - int len = buffer.Length; - if (offset > len) - throw new ArgumentException ("Destination offset is beyond array size."); + if (offset > len - count) + throw new ArgumentException ("Reading would overrun 'buffer'."); - if (offset > len - count) - throw new ArgumentException ("Reading would overrun buffer."); + if (_remainingBody == 0) + return -1; - if (this.remaining_body == 0) - return -1; + if (_length == 0) + return 0; - if (this.length == 0) - return 0; + var size = _length < count ? _length : count; + if (_remainingBody > 0 && _remainingBody < size) + size = (int) _remainingBody; - int size = Math.Min (this.length, count); - if (this.remaining_body > 0) - size = (int) Math.Min (size, this.remaining_body); + var remainingBuffer = _buffer.Length - _offset; + if (remainingBuffer < size) + size = remainingBuffer; - if (this.offset > this.buffer.Length - size) { - size = Math.Min (size, this.buffer.Length - this.offset); - } + if (size == 0) + return 0; - if (size == 0) - return 0; + Buffer.BlockCopy (_buffer, _offset, buffer, offset, size); + _offset += size; + _length -= size; + if (_remainingBody > 0) + _remainingBody -= size; - Buffer.BlockCopy (this.buffer, this.offset, buffer, offset, size); - this.offset += size; - this.length -= size; - if (this.remaining_body > 0) - remaining_body -= size; + return size; + } - return size; - } + #endregion - #endregion + #region Public Methods - #region Public Methods + public override IAsyncResult BeginRead ( + byte [] buffer, + int offset, + int count, + AsyncCallback callback, + object state) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); - public override IAsyncResult BeginRead ( - byte [] buffer, int offset, int count, AsyncCallback cback, object state) - { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + var read = fillFromBuffer (buffer, offset, count); + if (read > 0 || read == -1) { + var ares = new HttpStreamAsyncResult (); + ares.Buffer = buffer; + ares.Offset = offset; + ares.Count = count; + ares.Callback = callback; + ares.State = state; + ares.SyncRead = read; + ares.Complete (); - int nread = FillFromBuffer (buffer, offset, count); - if (nread > 0 || nread == -1) { - var ares = new HttpStreamAsyncResult (); - ares.Buffer = buffer; - ares.Offset = offset; - ares.Count = count; - ares.Callback = cback; - ares.State = state; - ares.SyncRead = nread; - ares.Complete (); - return ares; - } + return ares; + } - // Avoid reading past the end of the request to allow - // for HTTP pipelining - if (remaining_body >= 0 && count > remaining_body) - count = (int) Math.Min (Int32.MaxValue, remaining_body); + // Avoid reading past the end of the request to allow for HTTP pipelining. + if (_remainingBody >= 0 && _remainingBody < count) + count = (int) _remainingBody; - return stream.BeginRead (buffer, offset, count, cback, state); - } + return _stream.BeginRead (buffer, offset, count, callback, state); + } - public override IAsyncResult BeginWrite ( - byte [] buffer, int offset, int count, AsyncCallback cback, object state) - { - throw new NotSupportedException (); - } + public override IAsyncResult BeginWrite ( + byte [] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotSupportedException (); + } - public override void Close () - { - disposed = true; - } + public override void Close () + { + _disposed = true; + } - public override int EndRead (IAsyncResult ares) - { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + public override int EndRead (IAsyncResult asyncResult) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); - if (ares == null) - throw new ArgumentNullException ("ares"); + if (asyncResult == null) + throw new ArgumentNullException ("asyncResult"); - if (ares is HttpStreamAsyncResult) { - var ares_ = (HttpStreamAsyncResult) ares; - if (!ares.IsCompleted) - ares.AsyncWaitHandle.WaitOne (); + if (asyncResult is HttpStreamAsyncResult) { + var ares = (HttpStreamAsyncResult) asyncResult; + if (!ares.IsCompleted) + ares.AsyncWaitHandle.WaitOne (); - return ares_.SyncRead; - } + return ares.SyncRead; + } - // Close on exception? - int nread = stream.EndRead (ares); - if (remaining_body > 0 && nread > 0) - remaining_body -= nread; + // Close on exception? + var read = _stream.EndRead (asyncResult); + if (read > 0 && _remainingBody > 0) + _remainingBody -= read; - return nread; - } + return read; + } - public override void EndWrite (IAsyncResult async_result) - { - throw new NotSupportedException (); - } + public override void EndWrite (IAsyncResult asyncResult) + { + throw new NotSupportedException (); + } - public override void Flush () - { - } + public override void Flush () + { + } - public override int Read ([In,Out] byte[] buffer, int offset, int count) - { - if (disposed) - throw new ObjectDisposedException (GetType () .ToString ()); + public override int Read (byte [] buffer, int offset, int count) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); - // Call FillFromBuffer to check for buffer boundaries even when remaining_body is 0 - int nread = FillFromBuffer (buffer, offset, count); - if (nread == -1) { // No more bytes available (Content-Length) - return 0; - } else if (nread > 0) { - return nread; - } + // Call fillFromBuffer to check for buffer boundaries even when + // _remainingBody is 0. + var read = fillFromBuffer (buffer, offset, count); + if (read == -1) // No more bytes available (Content-Length). + return 0; + else if (read > 0) + return read; - nread = stream.Read (buffer, offset, count); - if (nread > 0 && remaining_body > 0) - remaining_body -= nread; + read = _stream.Read (buffer, offset, count); + if (read > 0 && _remainingBody > 0) + _remainingBody -= read; - return nread; - } + return read; + } - public override long Seek (long offset, SeekOrigin origin) - { - throw new NotSupportedException (); - } + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException (); + } - public override void SetLength (long value) - { - throw new NotSupportedException (); - } + public override void SetLength (long value) + { + throw new NotSupportedException (); + } - public override void Write (byte[] buffer, int offset, int count) - { - throw new NotSupportedException (); - } + public override void Write (byte [] buffer, int offset, int count) + { + throw new NotSupportedException (); + } - #endregion - } + #endregion + } } diff --git a/websocket-sharp/Net/ResponseStream.cs b/websocket-sharp/Net/ResponseStream.cs index bb66339f..e69d87b4 100644 --- a/websocket-sharp/Net/ResponseStream.cs +++ b/websocket-sharp/Net/ResponseStream.cs @@ -1,284 +1,322 @@ -// -// ResponseStream.cs -// Copied from System.Net.ResponseStream.cs -// -// Author: -// Gonzalo Paniagua Javier (gonzalo@novell.com) -// -// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// +#region License +/* + * ResponseStream.cs + * + * This code is derived from System.Net.ResponseStream.cs of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2014 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * Gonzalo Paniagua Javier + */ +#endregion using System; using System.IO; -using System.Net; -using System.Net.Sockets; using System.Text; -using System.Runtime.InteropServices; -namespace WebSocketSharp.Net { +namespace WebSocketSharp.Net +{ + // FIXME: Does this buffer the response until Close? + // Update: we send a single packet for the first non-chunked Write + // What happens when we set content-length to X and write X-1 bytes then close? + // what if we don't set content-length at all? + internal class ResponseStream : Stream + { + #region Private Static Fields - // FIXME: Does this buffer the response until Close? - // Update: we send a single packet for the first non-chunked Write - // What happens when we set content-length to X and write X-1 bytes then close? - // what if we don't set content-length at all? - class ResponseStream : Stream { + private static byte [] _crlf = new byte [] { 13, 10 }; - #region Private Static Field + #endregion - static byte [] crlf = new byte [] { 13, 10 }; + #region Private Fields - #endregion + private bool _disposed; + private bool _ignoreErrors; + private HttpListenerResponse _response; + private Stream _stream; + private bool _trailerSent; - #region Private Fields + #endregion - bool disposed; - bool ignore_errors; - HttpListenerResponse response; - Stream stream; - bool trailer_sent; + #region Internal Constructors - #endregion + internal ResponseStream ( + Stream stream, HttpListenerResponse response, bool ignoreErrors) + { + _stream = stream; + _response = response; + _ignoreErrors = ignoreErrors; + } - #region Constructor + #endregion - internal ResponseStream (System.IO.Stream stream, HttpListenerResponse response, bool ignore_errors) - { - this.stream = stream; - this.response = response; - this.ignore_errors = ignore_errors; - } + #region Public Properties - #endregion + public override bool CanRead { + get { + return false; + } + } - #region Properties + public override bool CanSeek { + get { + return false; + } + } - public override bool CanRead { - get { return false; } - } + public override bool CanWrite { + get { + return true; + } + } - public override bool CanSeek { - get { return false; } - } + public override long Length { + get { + throw new NotSupportedException (); + } + } - public override bool CanWrite { - get { return true; } - } + public override long Position { + get { + throw new NotSupportedException (); + } - public override long Length { - get { throw new NotSupportedException (); } - } + set { + throw new NotSupportedException (); + } + } - public override long Position { - get { throw new NotSupportedException (); } - set { throw new NotSupportedException (); } - } + #endregion - #endregion + #region Private Methods - #region Private Methods + private static byte [] getChunkSizeBytes (int size, bool final) + { + return Encoding.ASCII.GetBytes ( + String.Format ("{0:x}\r\n{1}", size, final ? "\r\n" : "")); + } - static byte [] GetChunkSizeBytes (int size, bool final) - { - string str = String.Format ("{0:x}\r\n{1}", size, final ? "\r\n" : ""); - return Encoding.ASCII.GetBytes (str); - } + private MemoryStream getHeaders (bool closing) + { + if (_response.HeadersSent) + return null; - MemoryStream GetHeaders (bool closing) - { - if (response.HeadersSent) - return null; + var stream = new MemoryStream (); + _response.SendHeaders (closing, stream); - MemoryStream ms = new MemoryStream (); - response.SendHeaders (closing, ms); + return stream; + } - return ms; - } + #endregion - #endregion + #region Internal Methods - #region Internal Method + internal void InternalWrite (byte [] buffer, int offset, int count) + { + if (_ignoreErrors) { + try { + _stream.Write (buffer, offset, count); + } + catch { + } + } + else { + _stream.Write (buffer, offset, count); + } + } - internal void InternalWrite (byte [] buffer, int offset, int count) - { - if (ignore_errors) { - try { - stream.Write (buffer, offset, count); - } catch { - } - } else { - stream.Write (buffer, offset, count); - } - } + #endregion - #endregion + #region Public Methods - #region Public Methods + public override IAsyncResult BeginRead ( + byte [] buffer, + int offset, + int count, + AsyncCallback callback, + object state) + { + throw new NotSupportedException (); + } - public override IAsyncResult BeginRead ( - byte [] buffer, - int offset, - int count, - AsyncCallback cback, - object state) - { - throw new NotSupportedException (); - } + public override IAsyncResult BeginWrite ( + byte [] buffer, + int offset, + int count, + AsyncCallback callback, + object state) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); - public override IAsyncResult BeginWrite ( - byte [] buffer, - int offset, - int count, - AsyncCallback cback, - object state) - { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + var stream = getHeaders (false); + var chunked = _response.SendChunked; + byte [] bytes = null; + if (stream != null) { + var start = stream.Position; + stream.Position = stream.Length; + if (chunked) { + bytes = getChunkSizeBytes (count, false); + stream.Write (bytes, 0, bytes.Length); + } - byte [] bytes = null; - MemoryStream ms = GetHeaders (false); - bool chunked = response.SendChunked; - if (ms != null) { - long start = ms.Position; - ms.Position = ms.Length; - if (chunked) { - bytes = GetChunkSizeBytes (count, false); - ms.Write (bytes, 0, bytes.Length); - } - ms.Write (buffer, offset, count); - buffer = ms.GetBuffer (); - offset = (int) start; - count = (int) (ms.Position - start); - } else if (chunked) { - bytes = GetChunkSizeBytes (count, false); - InternalWrite (bytes, 0, bytes.Length); - } + stream.Write (buffer, offset, count); + buffer = stream.GetBuffer (); + offset = (int) start; + count = (int) (stream.Position - start); + } + else if (chunked) { + bytes = getChunkSizeBytes (count, false); + InternalWrite (bytes, 0, bytes.Length); + } - return stream.BeginWrite (buffer, offset, count, cback, state); - } + return _stream.BeginWrite (buffer, offset, count, callback, state); + } - public override void Close () - { - if (disposed == false) { - disposed = true; - byte [] bytes = null; - MemoryStream ms = GetHeaders (true); - bool chunked = response.SendChunked; - if (ms != null) { - long start = ms.Position; - if (chunked && !trailer_sent) { - bytes = GetChunkSizeBytes (0, true); - ms.Position = ms.Length; - ms.Write (bytes, 0, bytes.Length); - } - InternalWrite (ms.GetBuffer (), (int) start, (int) (ms.Length - start)); - trailer_sent = true; - } else if (chunked && !trailer_sent) { - bytes = GetChunkSizeBytes (0, true); - InternalWrite (bytes, 0, bytes.Length); - trailer_sent = true; - } + public override void Close () + { + if (_disposed) + return; - response.Close (); - } - } + _disposed = true; - public override int EndRead (IAsyncResult ares) - { - throw new NotSupportedException (); - } + var stream = getHeaders (true); + var chunked = _response.SendChunked; + byte [] bytes = null; + if (stream != null) { + var start = stream.Position; + if (chunked && !_trailerSent) { + bytes = getChunkSizeBytes (0, true); + stream.Position = stream.Length; + stream.Write (bytes, 0, bytes.Length); + } - public override void EndWrite (IAsyncResult ares) - { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + InternalWrite ( + stream.GetBuffer (), (int) start, (int) (stream.Length - start)); + _trailerSent = true; + } + else if (chunked && !_trailerSent) { + bytes = getChunkSizeBytes (0, true); + InternalWrite (bytes, 0, bytes.Length); + _trailerSent = true; + } - if (ignore_errors) { - try { - stream.EndWrite (ares); - if (response.SendChunked) - stream.Write (crlf, 0, 2); - } catch { - } - } else { - stream.EndWrite (ares); - if (response.SendChunked) - stream.Write (crlf, 0, 2); - } - } + _response.Close (); + } - public override void Flush () - { - } + public override int EndRead (IAsyncResult asyncResult) + { + throw new NotSupportedException (); + } - public override int Read ([In,Out] byte[] buffer, int offset, int count) - { - throw new NotSupportedException (); - } + public override void EndWrite (IAsyncResult asyncResult) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); - public override long Seek (long offset, SeekOrigin origin) - { - throw new NotSupportedException (); - } + Action endWrite = ares => { + _stream.EndWrite (ares); + if (_response.SendChunked) + _stream.Write (_crlf, 0, 2); + }; - public override void SetLength (long value) - { - throw new NotSupportedException (); - } + if (_ignoreErrors) { + try { + endWrite (asyncResult); + } + catch { + } + } + else { + endWrite (asyncResult); + } + } - public override void Write (byte [] buffer, int offset, int count) - { - if (disposed) - throw new ObjectDisposedException (GetType ().ToString ()); + public override void Flush () + { + } - byte [] bytes = null; - MemoryStream ms = GetHeaders (false); - bool chunked = response.SendChunked; - if (ms != null) { - long start = ms.Position; // After the possible preamble for the encoding - ms.Position = ms.Length; - if (chunked) { - bytes = GetChunkSizeBytes (count, false); - ms.Write (bytes, 0, bytes.Length); - } + public override int Read (byte [] buffer, int offset, int count) + { + throw new NotSupportedException (); + } - int new_count = Math.Min (count, 16384 - (int) ms.Position + (int) start); - ms.Write (buffer, offset, new_count); - count -= new_count; - offset += new_count; - InternalWrite (ms.GetBuffer (), (int) start, (int) (ms.Length - start)); - ms.SetLength (0); - ms.Capacity = 0; // 'dispose' the buffer in ms. - } else if (chunked) { - bytes = GetChunkSizeBytes (count, false); - InternalWrite (bytes, 0, bytes.Length); - } + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException (); + } - if (count > 0) - InternalWrite (buffer, offset, count); + public override void SetLength (long value) + { + throw new NotSupportedException (); + } - if (chunked) - InternalWrite (crlf, 0, 2); - } + public override void Write (byte [] buffer, int offset, int count) + { + if (_disposed) + throw new ObjectDisposedException (GetType ().ToString ()); - #endregion - } + var stream = getHeaders (false); + var chunked = _response.SendChunked; + byte [] bytes = null; + if (stream != null) { + // After the possible preamble for the encoding. + var start = stream.Position; + stream.Position = stream.Length; + if (chunked) { + bytes = getChunkSizeBytes (count, false); + stream.Write (bytes, 0, bytes.Length); + } + + var newCount = Math.Min ( + count, 16384 - (int) stream.Position + (int) start); + stream.Write (buffer, offset, newCount); + count -= newCount; + offset += newCount; + InternalWrite ( + stream.GetBuffer (), (int) start, (int) (stream.Length - start)); + stream.SetLength (0); + stream.Capacity = 0; // 'dispose' the buffer in stream. + } + else if (chunked) { + bytes = getChunkSizeBytes (count, false); + InternalWrite (bytes, 0, bytes.Length); + } + + if (count > 0) + InternalWrite (buffer, offset, count); + + if (chunked) + InternalWrite (_crlf, 0, 2); + } + + #endregion + } } diff --git a/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs b/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs index 096d464e..09a19458 100644 --- a/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs +++ b/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs @@ -5,7 +5,7 @@ * The MIT License * * Copyright (c) 2012-2013 sta.blockhead - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -15,7 +15,7 @@ * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -34,7 +34,8 @@ using System.Security.Principal; namespace WebSocketSharp.Net.WebSockets { /// - /// Provides access to the WebSocket connection request objects received by the . + /// Provides access to the WebSocket connection request information received by + /// the . /// /// /// @@ -50,11 +51,12 @@ namespace WebSocketSharp.Net.WebSockets #region Internal Constructors - internal HttpListenerWebSocketContext (HttpListenerContext context) + internal HttpListenerWebSocketContext ( + HttpListenerContext context, Logger logger) { _context = context; _stream = WsStream.CreateServerStream (context); - _websocket = new WebSocket (this); + _websocket = new WebSocket (this, logger ?? new Logger ()); } #endregion @@ -72,10 +74,11 @@ namespace WebSocketSharp.Net.WebSockets #region Public Properties /// - /// Gets the cookies used in the WebSocket opening handshake. + /// Gets the cookies used in the WebSocket connection request. /// /// - /// A that contains the cookies. + /// A that contains the + /// cookies. /// public override CookieCollection CookieCollection { get { @@ -84,10 +87,10 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets the HTTP headers used in the WebSocket opening handshake. + /// Gets the HTTP headers used in the WebSocket connection request. /// /// - /// A that contains the HTTP headers. + /// A that contains the HTTP headers. /// public override NameValueCollection Headers { get { @@ -96,10 +99,11 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets the value of the Host header field used in the WebSocket opening handshake. + /// Gets the value of the Host header field used in the WebSocket connection + /// request. /// /// - /// A that contains the value of the Host header field. + /// A that represents the value of the Host header field. /// public override string Host { get { @@ -120,10 +124,12 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets a value indicating whether the client connected from the local computer. + /// Gets a value indicating whether the client connected from the local + /// computer. /// /// - /// true if the client connected from the local computer; otherwise, false. + /// true if the client connected from the local computer; otherwise, + /// false. /// public override bool IsLocal { get { @@ -135,7 +141,8 @@ namespace WebSocketSharp.Net.WebSockets /// Gets a value indicating whether the WebSocket connection is secured. /// /// - /// true if the WebSocket connection is secured; otherwise, false. + /// true if the WebSocket connection is secured; otherwise, + /// false. /// public override bool IsSecureConnection { get { @@ -144,10 +151,12 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets a value indicating whether the request is a WebSocket connection request. + /// Gets a value indicating whether the request is a WebSocket connection + /// request. /// /// - /// true if the request is a WebSocket connection request; otherwise, false. + /// true if the request is a WebSocket connection request; otherwise, + /// false. /// public override bool IsWebSocketRequest { get { @@ -156,10 +165,12 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets the value of the Origin header field used in the WebSocket opening handshake. + /// Gets the value of the Origin header field used in the WebSocket + /// connection request. /// /// - /// A that contains the value of the Origin header field. + /// A that represents the value of the Origin header + /// field. /// public override string Origin { get { @@ -171,19 +182,22 @@ namespace WebSocketSharp.Net.WebSockets /// Gets the absolute path of the requested WebSocket URI. /// /// - /// A that contains the absolute path of the requested WebSocket URI. + /// A that represents the absolute path of the requested + /// WebSocket URI. /// public override string Path { get { - return RequestUri.GetAbsolutePath (); + return _context.Request.Url.GetAbsolutePath (); } } /// - /// Gets the collection of query string variables used in the WebSocket opening handshake. + /// Gets the collection of query string variables used in the WebSocket + /// connection request. /// /// - /// A that contains the collection of query string variables. + /// A that contains the collection of query + /// string variables. /// public override NameValueCollection QueryString { get { @@ -195,22 +209,26 @@ namespace WebSocketSharp.Net.WebSockets /// Gets the WebSocket URI requested by the client. /// /// - /// A that contains the WebSocket URI. + /// A that represents the WebSocket URI requested by the + /// client. /// public override Uri RequestUri { get { - return _context.Request.RawUrl.ToUri (); + return _context.Request.Url; } } /// - /// Gets the value of the Sec-WebSocket-Key header field used in the WebSocket opening handshake. + /// Gets the value of the Sec-WebSocket-Key header field used in the + /// WebSocket connection request. /// /// - /// The SecWebSocketKey property provides a part of the information used by the server to prove that it received a valid WebSocket opening handshake. + /// This property provides a part of the information used by the server to + /// prove that it received a valid WebSocket connection request. /// /// - /// A that contains the value of the Sec-WebSocket-Key header field. + /// A that represents the value of the Sec-WebSocket-Key + /// header field. /// public override string SecWebSocketKey { get { @@ -219,13 +237,15 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets the values of the Sec-WebSocket-Protocol header field used in the WebSocket opening handshake. + /// Gets the values of the Sec-WebSocket-Protocol header field used in the + /// WebSocket connection request. /// /// - /// The SecWebSocketProtocols property indicates the subprotocols of the WebSocket connection. + /// This property represents the subprotocols of the WebSocket connection. /// /// - /// An IEnumerable<string> that contains the values of the Sec-WebSocket-Protocol header field. + /// An IEnumerable<string> that contains the values of the + /// Sec-WebSocket-Protocol header field. /// public override IEnumerable SecWebSocketProtocols { get { @@ -234,13 +254,15 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets the value of the Sec-WebSocket-Version header field used in the WebSocket opening handshake. + /// Gets the value of the Sec-WebSocket-Version header field used in the + /// WebSocket connection request. /// /// - /// The SecWebSocketVersion property indicates the WebSocket protocol version of the connection. + /// This property represents the WebSocket protocol version of the connection. /// /// - /// A that contains the value of the Sec-WebSocket-Version header field. + /// A that represents the value of the + /// Sec-WebSocket-Version header field. /// public override string SecWebSocketVersion { get { @@ -252,7 +274,7 @@ namespace WebSocketSharp.Net.WebSockets /// Gets the server endpoint as an IP address and a port number. /// /// - /// A that contains the server endpoint. + /// A that represents the server endpoint. /// public override System.Net.IPEndPoint ServerEndPoint { get { @@ -261,10 +283,11 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets the client information (identity, authentication information and security roles). + /// Gets the client information (identity, authentication information and + /// security roles). /// /// - /// A that contains the client information. + /// A that represents the client information. /// public override IPrincipal User { get { @@ -276,7 +299,7 @@ namespace WebSocketSharp.Net.WebSockets /// Gets the client endpoint as an IP address and a port number. /// /// - /// A that contains the client endpoint. + /// A that represents the client endpoint. /// public override System.Net.IPEndPoint UserEndPoint { get { @@ -285,7 +308,8 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets the WebSocket instance used for two-way communication between client and server. + /// Gets the WebSocket instance used for two-way communication between client + /// and server. /// /// /// A . @@ -305,15 +329,22 @@ namespace WebSocketSharp.Net.WebSockets _context.Connection.Close (true); } + internal void Close (HttpStatusCode code) + { + _context.Response.Close (code); + } + #endregion #region Public Methods /// - /// Returns a that represents the current . + /// Returns a that represents the current + /// . /// /// - /// A that represents the current . + /// A that represents the current + /// . /// public override string ToString () { diff --git a/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs b/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs index db94cbaa..a84f7c56 100644 --- a/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs +++ b/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs @@ -5,7 +5,7 @@ * The MIT License * * Copyright (c) 2012-2013 sta.blockhead - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -15,7 +15,7 @@ * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -36,7 +36,8 @@ using System.Security.Principal; namespace WebSocketSharp.Net.WebSockets { /// - /// Provides access to the WebSocket connection request objects received by the . + /// Provides access to the WebSocket connection request information received by + /// the . /// /// /// @@ -49,19 +50,22 @@ namespace WebSocketSharp.Net.WebSockets private HandshakeRequest _request; private bool _secure; private WsStream _stream; + private Uri _uri; + private IPrincipal _user; private WebSocket _websocket; #endregion #region Internal Constructors - internal TcpListenerWebSocketContext (TcpClient client, bool secure, X509Certificate cert) + internal TcpListenerWebSocketContext ( + TcpClient client, X509Certificate cert, bool secure, Logger logger) { _client = client; _secure = secure; - _stream = WsStream.CreateServerStream (client, secure, cert); + _stream = WsStream.CreateServerStream (client, cert, secure); _request = HandshakeRequest.Parse (_stream.ReadHandshake ()); - _websocket = new WebSocket (this); + _websocket = new WebSocket (this, logger); } #endregion @@ -79,25 +83,23 @@ namespace WebSocketSharp.Net.WebSockets #region Public Properties /// - /// Gets the cookies used in the WebSocket opening handshake. + /// Gets the cookies used in the WebSocket connection request. /// /// - /// A that contains the cookies. + /// A that contains the + /// cookies. /// public override CookieCollection CookieCollection { get { - if (_cookies == null) - _cookies = _request.Cookies; - - return _cookies; + return _cookies ?? (_cookies = _request.Cookies); } } /// - /// Gets the HTTP headers used in the WebSocket opening handshake. + /// Gets the HTTP headers used in the WebSocket connection request. /// /// - /// A that contains the HTTP headers. + /// A that contains the HTTP headers. /// public override NameValueCollection Headers { get { @@ -106,10 +108,11 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets the value of the Host header field used in the WebSocket opening handshake. + /// Gets the value of the Host header field used in the WebSocket connection + /// request. /// /// - /// A that contains the value of the Host header field. + /// A that represents the value of the Host header field. /// public override string Host { get { @@ -123,20 +126,19 @@ namespace WebSocketSharp.Net.WebSockets /// /// true if the client is authenticated; otherwise, false. /// - /// - /// This property is not implemented. - /// public override bool IsAuthenticated { get { - throw new NotImplementedException (); + return _user != null && _user.Identity.IsAuthenticated; } } /// - /// Gets a value indicating whether the client connected from the local computer. + /// Gets a value indicating whether the client connected from the local + /// computer. /// /// - /// true if the client connected from the local computer; otherwise, false. + /// true if the client connected from the local computer; otherwise, + /// false. /// public override bool IsLocal { get { @@ -148,7 +150,8 @@ namespace WebSocketSharp.Net.WebSockets /// Gets a value indicating whether the WebSocket connection is secured. /// /// - /// true if the WebSocket connection is secured; otherwise, false. + /// true if the WebSocket connection is secured; otherwise, + /// false. /// public override bool IsSecureConnection { get { @@ -157,10 +160,12 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets a value indicating whether the request is a WebSocket connection request. + /// Gets a value indicating whether the request is a WebSocket connection + /// request. /// /// - /// true if the request is a WebSocket connection request; otherwise, false. + /// true if the request is a WebSocket connection request; otherwise, + /// false. /// public override bool IsWebSocketRequest { get { @@ -169,10 +174,12 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets the value of the Origin header field used in the WebSocket opening handshake. + /// Gets the value of the Origin header field used in the WebSocket + /// connection request. /// /// - /// A that contains the value of the Origin header field. + /// A that represents the value of the Origin header + /// field. /// public override string Origin { get { @@ -184,7 +191,8 @@ namespace WebSocketSharp.Net.WebSockets /// Gets the absolute path of the requested WebSocket URI. /// /// - /// A that contains the absolute path of the requested WebSocket URI. + /// A that represents the absolute path of the requested + /// WebSocket URI. /// public override string Path { get { @@ -193,10 +201,12 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets the collection of query string variables used in the WebSocket opening handshake. + /// Gets the collection of query string variables used in the WebSocket + /// connection request. /// /// - /// A that contains the collection of query string variables. + /// A that contains the collection of query + /// string variables. /// public override NameValueCollection QueryString { get { @@ -208,22 +218,26 @@ namespace WebSocketSharp.Net.WebSockets /// Gets the WebSocket URI requested by the client. /// /// - /// A that contains the WebSocket URI. + /// A that represents the WebSocket URI requested by the + /// client. /// public override Uri RequestUri { get { - return _request.RequestUri; + return _uri ?? (_uri = createRequestUri ()); } } /// - /// Gets the value of the Sec-WebSocket-Key header field used in the WebSocket opening handshake. + /// Gets the value of the Sec-WebSocket-Key header field used in the + /// WebSocket connection request. /// /// - /// The SecWebSocketKey property provides a part of the information used by the server to prove that it received a valid WebSocket opening handshake. + /// This property provides a part of the information used by the server to + /// prove that it received a valid WebSocket connection request. /// /// - /// A that contains the value of the Sec-WebSocket-Key header field. + /// A that represents the value of the Sec-WebSocket-Key + /// header field. /// public override string SecWebSocketKey { get { @@ -232,13 +246,15 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets the values of the Sec-WebSocket-Protocol header field used in the WebSocket opening handshake. + /// Gets the values of the Sec-WebSocket-Protocol header field used in the + /// WebSocket connection request. /// /// - /// This property indicates the subprotocols of the WebSocket connection. + /// This property represents the subprotocols of the WebSocket connection. /// /// - /// An IEnumerable<string> that contains the values of the Sec-WebSocket-Protocol header field. + /// An IEnumerable<string> that contains the values of the + /// Sec-WebSocket-Protocol header field. /// public override IEnumerable SecWebSocketProtocols { get { @@ -247,13 +263,15 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets the value of the Sec-WebSocket-Version header field used in the WebSocket opening handshake. + /// Gets the value of the Sec-WebSocket-Version header field used in the + /// WebSocket connection request. /// /// - /// The SecWebSocketVersion property indicates the WebSocket protocol version of the connection. + /// This property represents the WebSocket protocol version of the connection. /// /// - /// A that contains the value of the Sec-WebSocket-Version header field. + /// A that represents the value of the + /// Sec-WebSocket-Version header field. /// public override string SecWebSocketVersion { get { @@ -265,7 +283,7 @@ namespace WebSocketSharp.Net.WebSockets /// Gets the server endpoint as an IP address and a port number. /// /// - /// A that contains the server endpoint. + /// A that represents the server endpoint. /// public override System.Net.IPEndPoint ServerEndPoint { get { @@ -274,17 +292,15 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets the client information (identity, authentication information and security roles). + /// Gets the client information (identity, authentication information and + /// security roles). /// /// - /// A that contains the client information. + /// A that represents the client information. /// - /// - /// This property is not implemented. - /// public override IPrincipal User { get { - throw new NotImplementedException (); + return _user; } } @@ -292,7 +308,7 @@ namespace WebSocketSharp.Net.WebSockets /// Gets the client endpoint as an IP address and a port number. /// /// - /// A that contains the client endpoint. + /// A that represents the client endpoint. /// public override System.Net.IPEndPoint UserEndPoint { get { @@ -301,7 +317,8 @@ namespace WebSocketSharp.Net.WebSockets } /// - /// Gets the WebSocket instance used for two-way communication between client and server. + /// Gets the WebSocket instance used for two-way communication between client + /// and server. /// /// /// A . @@ -314,6 +331,22 @@ namespace WebSocketSharp.Net.WebSockets #endregion + #region Private Methods + + private Uri createRequestUri () + { + var scheme = _secure ? "wss" : "ws"; + var host = _request.Headers ["Host"]; + var rawUri = _request.RequestUri; + var path = rawUri.IsAbsoluteUri + ? rawUri.PathAndQuery + : HttpUtility.UrlDecode (_request.RawUrl); + + return String.Format ("{0}://{1}{2}", scheme, host, path).ToUri (); + } + + #endregion + #region Internal Methods internal void Close () @@ -322,15 +355,64 @@ namespace WebSocketSharp.Net.WebSockets _client.Close (); } + internal void Close (HttpStatusCode code) + { + _websocket.Close (HandshakeResponse.CreateCloseResponse (code)); + } + + internal void SendAuthChallenge (string challenge) + { + var res = new HandshakeResponse (HttpStatusCode.Unauthorized); + res.Headers ["WWW-Authenticate"] = challenge; + _stream.WriteHandshake (res); + _request = HandshakeRequest.Parse (_stream.ReadHandshake ()); + } + + internal void SetUser ( + AuthenticationSchemes expectedScheme, + string realm, + Func credentialsFinder) + { + var authRes = _request.AuthResponse; + if (authRes == null) + return; + + var identity = authRes.ToIdentity (); + if (identity == null) + return; + + NetworkCredential credentials = null; + try { + credentials = credentialsFinder (identity); + } + catch { + } + + if (credentials == null) + return; + + var valid = expectedScheme == AuthenticationSchemes.Basic + ? ((HttpBasicIdentity) identity).Password == credentials.Password + : expectedScheme == AuthenticationSchemes.Digest + ? ((HttpDigestIdentity) identity).IsValid ( + credentials.Password, realm, _request.HttpMethod, null) + : false; + + if (valid) + _user = new GenericPrincipal (identity, credentials.Roles); + } + #endregion #region Public Methods /// - /// Returns a that represents the current . + /// Returns a that represents the current + /// . /// /// - /// A that represents the current . + /// A that represents the current + /// . /// public override string ToString () { diff --git a/websocket-sharp/Net/WebSockets/WebSocketContext.cs b/websocket-sharp/Net/WebSockets/WebSocketContext.cs index 5dbe98ec..0067429d 100644 --- a/websocket-sharp/Net/WebSockets/WebSocketContext.cs +++ b/websocket-sharp/Net/WebSockets/WebSocketContext.cs @@ -5,7 +5,7 @@ * The MIT License * * Copyright (c) 2012-2013 sta.blockhead - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -15,7 +15,7 @@ * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -34,7 +34,7 @@ using System.Security.Principal; namespace WebSocketSharp.Net.WebSockets { /// - /// Provides access to the WebSocket connection request objects. + /// Provides access to the WebSocket connection request information. /// /// /// The WebSocketContext class is an abstract class. @@ -55,26 +55,28 @@ namespace WebSocketSharp.Net.WebSockets #region Public Properties /// - /// Gets the cookies used in the WebSocket opening handshake. + /// Gets the cookies used in the WebSocket connection request. /// /// - /// A that contains the cookies. + /// A that contains the + /// cookies. /// public abstract CookieCollection CookieCollection { get; } /// - /// Gets the HTTP headers used in the WebSocket opening handshake. + /// Gets the HTTP headers used in the WebSocket connection request. /// /// - /// A that contains the HTTP headers. + /// A that contains the HTTP headers. /// public abstract NameValueCollection Headers { get; } /// - /// Gets the value of the Host header field used in the WebSocket opening handshake. + /// Gets the value of the Host header field used in the WebSocket connection + /// request. /// /// - /// A that contains the value of the Host header field. + /// A that represents the value of the Host header field. /// public abstract string Host { get; } @@ -87,10 +89,12 @@ namespace WebSocketSharp.Net.WebSockets public abstract bool IsAuthenticated { get; } /// - /// Gets a value indicating whether the client connected from the local computer. + /// Gets a value indicating whether the client connected from the local + /// computer. /// /// - /// true if the client connected from the local computer; otherwise, false. + /// true if the client connected from the local computer; otherwise, + /// false. /// public abstract bool IsLocal { get; } @@ -98,23 +102,28 @@ namespace WebSocketSharp.Net.WebSockets /// Gets a value indicating whether the WebSocket connection is secured. /// /// - /// true if the WebSocket connection is secured; otherwise, false. + /// true if the WebSocket connection is secured; otherwise, + /// false. /// public abstract bool IsSecureConnection { get; } /// - /// Gets a value indicating whether the request is a WebSocket connection request. + /// Gets a value indicating whether the request is a WebSocket connection + /// request. /// /// - /// true if the request is a WebSocket connection request; otherwise, false. + /// true if the request is a WebSocket connection request; otherwise, + /// false. /// public abstract bool IsWebSocketRequest { get; } /// - /// Gets the value of the Origin header field used in the WebSocket opening handshake. + /// Gets the value of the Origin header field used in the WebSocket + /// connection request. /// /// - /// A that contains the value of the Origin header field. + /// A that represents the value of the Origin header + /// field. /// public abstract string Origin { get; } @@ -122,15 +131,18 @@ namespace WebSocketSharp.Net.WebSockets /// Gets the absolute path of the requested WebSocket URI. /// /// - /// A that contains the absolute path of the requested WebSocket URI. + /// A that represents the absolute path of the requested + /// WebSocket URI. /// public abstract string Path { get; } /// - /// Gets the collection of query string variables used in the WebSocket opening handshake. + /// Gets the collection of query string variables used in the WebSocket + /// connection request. /// /// - /// A that contains the collection of query string variables. + /// A that contains the collection of query + /// string variables. /// public abstract NameValueCollection QueryString { get; } @@ -138,40 +150,47 @@ namespace WebSocketSharp.Net.WebSockets /// Gets the WebSocket URI requested by the client. /// /// - /// A that contains the WebSocket URI. + /// A that represents the WebSocket URI. /// public abstract Uri RequestUri { get; } /// - /// Gets the value of the Sec-WebSocket-Key header field used in the WebSocket opening handshake. + /// Gets the value of the Sec-WebSocket-Key header field used in the + /// WebSocket connection request. /// /// - /// The SecWebSocketKey property provides a part of the information used by the server to prove that it received a valid WebSocket opening handshake. + /// This property provides a part of the information used by the server to + /// prove that it received a valid WebSocket connection request. /// /// - /// A that contains the value of the Sec-WebSocket-Key header field. + /// A that represents the value of the Sec-WebSocket-Key + /// header field. /// public abstract string SecWebSocketKey { get; } /// - /// Gets the values of the Sec-WebSocket-Protocol header field used in the WebSocket opening handshake. + /// Gets the values of the Sec-WebSocket-Protocol header field used in the + /// WebSocket connection request. /// /// - /// The SecWebSocketProtocols property indicates the subprotocols of the WebSocket connection. + /// This property represents the subprotocols of the WebSocket connection. /// /// - /// An IEnumerable<string> that contains the values of the Sec-WebSocket-Protocol header field. + /// An IEnumerable<string> that contains the values of the + /// Sec-WebSocket-Protocol header field. /// public abstract IEnumerable SecWebSocketProtocols { get; } /// - /// Gets the value of the Sec-WebSocket-Version header field used in the WebSocket opening handshake. + /// Gets the value of the Sec-WebSocket-Version header field used in the + /// WebSocket connection request. /// /// - /// The SecWebSocketVersion property indicates the WebSocket protocol version of the connection. + /// This property represents the WebSocket protocol version of the connection. /// /// - /// A that contains the value of the Sec-WebSocket-Version header field. + /// A that represents the value of the + /// Sec-WebSocket-Version header field. /// public abstract string SecWebSocketVersion { get; } @@ -179,15 +198,16 @@ namespace WebSocketSharp.Net.WebSockets /// Gets the server endpoint as an IP address and a port number. /// /// - /// A that contains the server endpoint. + /// A that represents the server endpoint. /// public abstract System.Net.IPEndPoint ServerEndPoint { get; } /// - /// Gets the client information (identity, authentication information and security roles). + /// Gets the client information (identity, authentication information and + /// security roles). /// /// - /// A that contains the client information. + /// A that represents the client information. /// public abstract IPrincipal User { get; } @@ -195,12 +215,13 @@ namespace WebSocketSharp.Net.WebSockets /// Gets the client endpoint as an IP address and a port number. /// /// - /// A that contains the client endpoint. + /// A that represents the client endpoint. /// public abstract System.Net.IPEndPoint UserEndPoint { get; } /// - /// Gets the WebSocket instance used for two-way communication between client and server. + /// Gets the WebSocket instance used for two-way communication between client + /// and server. /// /// /// A . diff --git a/websocket-sharp/Server/HttpServer.cs b/websocket-sharp/Server/HttpServer.cs index d0bcdf2d..ca28cc8d 100644 --- a/websocket-sharp/Server/HttpServer.cs +++ b/websocket-sharp/Server/HttpServer.cs @@ -7,7 +7,7 @@ * The MIT License * * Copyright (c) 2012-2013 sta.blockhead - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -17,7 +17,7 @@ * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -40,16 +40,19 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; using System.Threading; using WebSocketSharp.Net; +using WebSocketSharp.Net.WebSockets; namespace WebSocketSharp.Server { /// - /// Provides a simple HTTP server that allows to accept the WebSocket connection requests. + /// Provides a simple HTTP server that allows to accept the WebSocket + /// connection requests. /// /// - /// The HttpServer instance can provide the multi WebSocket services. + /// The HttpServer class can provide the multi WebSocket services. /// public class HttpServer { @@ -71,8 +74,8 @@ namespace WebSocketSharp.Server #region Public Constructors /// - /// Initializes a new instance of the class that listens for - /// incoming requests on port 80. + /// Initializes a new instance of the class that + /// listens for incoming requests on port 80. /// public HttpServer () : this (80) @@ -80,8 +83,8 @@ namespace WebSocketSharp.Server } /// - /// Initializes a new instance of the class that listens for - /// incoming requests on the specified . + /// Initializes a new instance of the class that + /// listens for incoming requests on the specified . /// /// /// An that contains a port number. @@ -95,8 +98,9 @@ namespace WebSocketSharp.Server } /// - /// Initializes a new instance of the class that listens for - /// incoming requests on the specified and . + /// Initializes a new instance of the class that + /// listens for incoming requests on the specified + /// and . /// /// /// An that contains a port number. @@ -114,16 +118,28 @@ namespace WebSocketSharp.Server public HttpServer (int port, bool secure) { if (!port.IsPortNumber ()) - throw new ArgumentOutOfRangeException ("port", "Must be between 1 and 65535: " + port); + throw new ArgumentOutOfRangeException ( + "port", "Must be between 1 and 65535: " + port); if ((port == 80 && secure) || (port == 443 && !secure)) - throw new ArgumentException (String.Format ( - "Invalid pair of 'port' and 'secure': {0}, {1}", port, secure)); + throw new ArgumentException ( + String.Format ( + "Invalid pair of 'port' and 'secure': {0}, {1}", port, secure)); _port = port; _secure = secure; + _listener = new HttpListener (); + _logger = new Logger (); + _serviceHosts = new WebSocketServiceHostManager (_logger); + _state = ServerState.READY; + _sync = new object (); - init (); + var os = Environment.OSVersion; + if (os.Platform != PlatformID.Unix && os.Platform != PlatformID.MacOSX) + _windows = true; + + var prefix = String.Format ("http{0}://*:{1}/", _secure ? "s" : "", _port); + _listener.Prefixes.Add (prefix); } #endregion @@ -131,7 +147,29 @@ namespace WebSocketSharp.Server #region Public Properties /// - /// Gets or sets the certificate used to authenticate the server on the secure connection. + /// Gets or sets the scheme used to authenticate the clients. + /// + /// + /// One of the values + /// that indicates the scheme used to authenticate the clients. The default + /// value is . + /// + public AuthenticationSchemes AuthenticationSchemes { + get { + return _listener.AuthenticationSchemes; + } + + set { + if (!canSet ("AuthenticationSchemes")) + return; + + _listener.AuthenticationSchemes = value; + } + } + + /// + /// Gets or sets the certificate used to authenticate the server on the + /// secure connection. /// /// /// A used to authenticate the server. @@ -142,16 +180,12 @@ namespace WebSocketSharp.Server } set { - if (_state == ServerState.START || _state == ServerState.SHUTDOWN) - { - _logger.Error ( - "The value of Certificate property cannot be changed because the server has already been started."); - + if (!canSet ("Certificate")) return; - } if (EndPointListener.CertificateExists (_port, _listener.CertificateFolderPath)) - _logger.Warn ("The server certificate associated with the port number already exists."); + _logger.Warn ( + "The server certificate associated with the port number already exists."); _listener.DefaultCertificate = value; } @@ -173,7 +207,8 @@ namespace WebSocketSharp.Server /// Gets a value indicating whether the server provides secure connection. /// /// - /// true if the server provides secure connection; otherwise, false. + /// true if the server provides secure connection; otherwise, + /// false. /// public bool IsSecure { get { @@ -182,12 +217,12 @@ namespace WebSocketSharp.Server } /// - /// Gets or sets a value indicating whether the server cleans up the inactive WebSocket sessions - /// periodically. + /// Gets or sets a value indicating whether the server cleans up the inactive + /// WebSocket sessions periodically. /// /// - /// true if the server cleans up the inactive WebSocket sessions every 60 seconds; - /// otherwise, false. The default value is true. + /// true if the server cleans up the inactive WebSocket sessions every + /// 60 seconds; otherwise, false. The default value is true. /// public bool KeepClean { get { @@ -203,9 +238,9 @@ namespace WebSocketSharp.Server /// Gets the logging functions. /// /// - /// The default logging level is the . - /// If you want to change the current logging level, you set the Log.Level property - /// to one of the values which you want. + /// The default logging level is the . If you + /// change the current logging level, you set the Log.Level property + /// to any of the values. /// /// /// A that provides the logging functions. @@ -228,6 +263,27 @@ namespace WebSocketSharp.Server } } + /// + /// Gets or sets the name of the realm associated with the + /// . + /// + /// + /// A that contains the name of the realm. + /// The default value is SECRET AREA. + /// + public string Realm { + get { + return _listener.Realm; + } + + set { + if (!canSet ("Realm")) + return; + + _listener.Realm = value; + } + } + /// /// Gets or sets the document root path of server. /// @@ -243,23 +299,42 @@ namespace WebSocketSharp.Server } set { - if (_state == ServerState.START || _state == ServerState.SHUTDOWN) - { - _logger.Error ( - "The value of RootPath property cannot be changed because the server has already been started."); - + if (!canSet ("RootPath")) return; - } _rootPath = value; } } /// - /// Gets the functions for the WebSocket services that the server provides. + /// Gets or sets the delegate called to find the credentials for an identity + /// used to authenticate a client. /// /// - /// A that manages the WebSocket services. + /// A Func<, > + /// delegate that references the method(s) used to find the credentials. The + /// default value is a function that only returns . + /// + public Func UserCredentialsFinder { + get { + return _listener.UserCredentialsFinder; + } + + set { + if (!canSet ("UserCredentialsFinder")) + return; + + _listener.UserCredentialsFinder = value; + } + } + + /// + /// Gets the functions for the WebSocket services provided by the + /// . + /// + /// + /// A that manages the WebSocket + /// services. /// public WebSocketServiceHostManager WebSocketServices { get { @@ -322,8 +397,7 @@ namespace WebSocketSharp.Server private void abort () { - lock (_sync) - { + lock (_sync) { if (!IsListening) return; @@ -331,12 +405,136 @@ namespace WebSocketSharp.Server } _serviceHosts.Stop ( - ((ushort) CloseStatusCode.SERVER_ERROR).ToByteArrayInternally (ByteOrder.BIG), true); + ((ushort) CloseStatusCode.SERVER_ERROR).ToByteArrayInternally (ByteOrder.BIG), + true); _listener.Abort (); _state = ServerState.STOP; } + private void acceptHttpRequest (HttpListenerContext context) + { + var args = new HttpRequestEventArgs (context); + var method = context.Request.HttpMethod; + if (method == "GET" && OnGet != null) { + OnGet (this, args); + return; + } + + if (method == "HEAD" && OnHead != null) { + OnHead (this, args); + return; + } + + if (method == "POST" && OnPost != null) { + OnPost (this, args); + return; + } + + if (method == "PUT" && OnPut != null) { + OnPut (this, args); + return; + } + + if (method == "DELETE" && OnDelete != null) { + OnDelete (this, args); + return; + } + + if (method == "OPTIONS" && OnOptions != null) { + OnOptions (this, args); + return; + } + + if (method == "TRACE" && OnTrace != null) { + OnTrace (this, args); + return; + } + + if (method == "CONNECT" && OnConnect != null) { + OnConnect (this, args); + return; + } + + if (method == "PATCH" && OnPatch != null) { + OnPatch (this, args); + return; + } + + context.Response.Close (HttpStatusCode.NotImplemented); + } + + private void acceptRequestAsync (HttpListenerContext context) + { + ThreadPool.QueueUserWorkItem ( + state => { + try { + var authScheme = _listener.SelectAuthenticationScheme (context); + if (authScheme != AuthenticationSchemes.Anonymous && + !authenticateRequest (authScheme, context)) + return; + + if (context.Request.IsUpgradeTo ("websocket")) { + acceptWebSocketRequest (context.AcceptWebSocket (_logger)); + return; + } + + acceptHttpRequest (context); + context.Response.Close (); + } + catch (Exception ex) { + _logger.Fatal (ex.ToString ()); + context.Connection.Close (true); + } + }); + } + + private void acceptWebSocketRequest (HttpListenerWebSocketContext context) + { + var path = context.Path; + + WebSocketServiceHost host; + if (path == null || + !_serviceHosts.TryGetServiceHostInternally (path, out host)) { + context.Close (HttpStatusCode.NotImplemented); + return; + } + + host.StartSession (context); + } + + private bool authenticateRequest ( + AuthenticationSchemes scheme, HttpListenerContext context) + { + if (context.Request.IsAuthenticated) + return true; + + if (scheme == AuthenticationSchemes.Basic) + context.Response.CloseWithAuthChallenge ( + HttpUtility.CreateBasicAuthChallenge (_listener.Realm)); + else if (scheme == AuthenticationSchemes.Digest) + context.Response.CloseWithAuthChallenge ( + HttpUtility.CreateDigestAuthChallenge (_listener.Realm)); + else + context.Response.Close (HttpStatusCode.Forbidden); + + return false; + } + + private bool canSet (string property) + { + if (_state == ServerState.START || _state == ServerState.SHUTDOWN) { + _logger.Error ( + String.Format ( + "The '{0}' property cannot set a value because the server has already been started.", + property)); + + return false; + } + + return true; + } + private string checkIfCertExists () { return _secure && @@ -346,137 +544,17 @@ namespace WebSocketSharp.Server : null; } - private void init () - { - _listener = new HttpListener (); - _logger = new Logger (); - _serviceHosts = new WebSocketServiceHostManager (_logger); - _state = ServerState.READY; - _sync = new object (); - - _windows = false; - var os = Environment.OSVersion; - if (os.Platform != PlatformID.Unix && os.Platform != PlatformID.MacOSX) - _windows = true; - - var prefix = String.Format ("http{0}://*:{1}/", _secure ? "s" : "", _port); - _listener.Prefixes.Add (prefix); - } - - private void processHttpRequest (HttpListenerContext context) - { - var args = new HttpRequestEventArgs (context); - var method = context.Request.HttpMethod; - if (method == "GET" && OnGet != null) - { - OnGet (this, args); - return; - } - - if (method == "HEAD" && OnHead != null) - { - OnHead (this, args); - return; - } - - if (method == "POST" && OnPost != null) - { - OnPost (this, args); - return; - } - - if (method == "PUT" && OnPut != null) - { - OnPut (this, args); - return; - } - - if (method == "DELETE" && OnDelete != null) - { - OnDelete (this, args); - return; - } - - if (method == "OPTIONS" && OnOptions != null) - { - OnOptions (this, args); - return; - } - - if (method == "TRACE" && OnTrace != null) - { - OnTrace (this, args); - return; - } - - if (method == "CONNECT" && OnConnect != null) - { - OnConnect (this, args); - return; - } - - if (method == "PATCH" && OnPatch != null) - { - OnPatch (this, args); - return; - } - - context.Response.StatusCode = (int) HttpStatusCode.NotImplemented; - } - - private bool processWebSocketRequest (HttpListenerContext context) - { - var wsContext = context.AcceptWebSocket (); - - var path = wsContext.Path; - WebSocketServiceHost host; - if (path == null || !_serviceHosts.TryGetServiceHostInternally (path, out host)) - { - context.Response.StatusCode = (int) HttpStatusCode.NotImplemented; - return false; - } - - wsContext.WebSocket.Log = _logger; - host.StartSession (wsContext); - - return true; - } - - private void processRequestAsync (HttpListenerContext context) - { - WaitCallback callback = state => - { - try { - if (context.Request.IsUpgradeTo ("websocket")) - { - if (processWebSocketRequest (context)) - return; - } - else - { - processHttpRequest (context); - } - - context.Response.Close (); - } - catch (Exception ex) { - _logger.Fatal (ex.ToString ()); - context.Connection.Close (true); - } - }; - - ThreadPool.QueueUserWorkItem (callback); - } - private void receiveRequest () { - while (true) - { + while (true) { try { - processRequestAsync (_listener.GetContext ()); + acceptRequestAsync (_listener.GetContext ()); } catch (HttpListenerException ex) { - _logger.Warn (String.Format ("Receiving has been stopped.\nreason: {0}.", ex.Message)); + _logger.Warn ( + String.Format ( + "Receiving has been stopped.\nreason: {0}.", ex.Message)); + break; } catch (Exception ex) { @@ -489,9 +567,9 @@ namespace WebSocketSharp.Server abort (); } - private void startReceiveRequestThread () + private void startReceiving () { - _receiveRequestThread = new Thread (new ThreadStart (receiveRequest)); + _receiveRequestThread = new Thread (new ThreadStart (receiveRequest)); _receiveRequestThread.IsBackground = true; _receiveRequestThread.Start (); } @@ -507,18 +585,21 @@ namespace WebSocketSharp.Server #region Public Methods /// - /// Adds the specified typed WebSocket service with the specified . + /// Adds the specified typed WebSocket service with the specified + /// . /// /// - /// This method converts to URL-decoded string and - /// removes '/' from tail end of . + /// This method converts to URL-decoded string + /// and removes '/' from tail end of . /// /// - /// A that contains an absolute path to the WebSocket service. + /// A that contains an absolute path to the WebSocket + /// service. /// /// - /// The type of the WebSocket service. The TWithNew must inherit the class and - /// must have a public parameterless constructor. + /// The type of the WebSocket service. The TWithNew must inherit the + /// class and must have a public parameterless + /// constructor. /// public void AddWebSocketService (string servicePath) where TWithNew : WebSocketService, new () @@ -527,42 +608,50 @@ namespace WebSocketSharp.Server } /// - /// Adds the specified typed WebSocket service with the specified and - /// . + /// Adds the specified typed WebSocket service with the specified + /// and . /// /// /// - /// This method converts to URL-decoded string and - /// removes '/' from tail end of . + /// This method converts to URL-decoded + /// string and removes '/' from tail end of + /// . /// /// - /// returns a initialized specified typed WebSocket service - /// instance. + /// returns a initialized specified + /// typed WebSocket service instance. /// /// /// - /// A that contains an absolute path to the WebSocket service. + /// A that contains an absolute path to the WebSocket + /// service. /// /// - /// A Func<T> delegate that references the method used to initialize a new WebSocket service - /// instance (a new WebSocket session). + /// A Func<T> delegate that references the method used to initialize + /// a new WebSocket service instance (a new WebSocket session). /// /// - /// The type of the WebSocket service. The T must inherit the class. + /// The type of the WebSocket service. The T must inherit the + /// class. /// public void AddWebSocketService (string servicePath, Func serviceConstructor) where T : WebSocketService { var msg = servicePath.CheckIfValidServicePath () ?? - (serviceConstructor == null ? "'serviceConstructor' must not be null." : null); + (serviceConstructor == null + ? "'serviceConstructor' must not be null." + : null); + + if (msg != null) { + _logger.Error ( + String.Format ("{0}\nservice path: {1}", msg, servicePath ?? "")); - if (msg != null) - { - _logger.Error (String.Format ("{0}\nservice path: {1}", msg, servicePath ?? "")); return; } - var host = new WebSocketServiceHost (servicePath, serviceConstructor, _logger); + var host = new WebSocketServiceHost ( + servicePath, serviceConstructor, _logger); + if (!KeepClean) host.KeepClean = false; @@ -573,8 +662,8 @@ namespace WebSocketSharp.Server /// Gets the contents of the file with the specified . /// /// - /// An array of that contains the contents of the file if exists; - /// otherwise, . + /// An array of that contains the contents of the file if + /// it exists; otherwise, . /// /// /// A that contains a virtual path to the file to get. @@ -591,24 +680,28 @@ namespace WebSocketSharp.Server } /// - /// Removes the WebSocket service with the specified . + /// Removes the WebSocket service with the specified + /// . /// /// - /// This method converts to URL-decoded string and - /// removes '/' from tail end of . + /// This method converts to URL-decoded string + /// and removes '/' from tail end of . /// /// - /// true if the WebSocket service is successfully found and removed; otherwise, false. + /// true if the WebSocket service is successfully found and removed; + /// otherwise, false. /// /// - /// A that contains an absolute path to the WebSocket service to find. + /// A that contains an absolute path to the WebSocket + /// service to find. /// public bool RemoveWebSocketService (string servicePath) { var msg = servicePath.CheckIfValidServicePath (); - if (msg != null) - { - _logger.Error (String.Format ("{0}\nservice path: {1}", msg, servicePath ?? "")); + if (msg != null) { + _logger.Error ( + String.Format ("{0}\nservice path: {1}", msg, servicePath)); + return false; } @@ -616,22 +709,23 @@ namespace WebSocketSharp.Server } /// - /// Starts to receive the HTTP requests. + /// Starts receiving the HTTP requests. /// public void Start () { - lock (_sync) - { + lock (_sync) { var msg = _state.CheckIfStopped () ?? checkIfCertExists (); - if (msg != null) - { - _logger.Error (String.Format ("{0}\nstate: {1}\nsecure: {2}", msg, _state, _secure)); + if (msg != null) { + _logger.Error ( + String.Format ( + "{0}\nstate: {1}\nsecure: {2}", msg, _state, _secure)); + return; } _serviceHosts.Start (); _listener.Start (); - startReceiveRequestThread (); + startReceiving (); _state = ServerState.START; } @@ -642,11 +736,9 @@ namespace WebSocketSharp.Server /// public void Stop () { - lock (_sync) - { + lock (_sync) { var msg = _state.CheckIfStarted (); - if (msg != null) - { + if (msg != null) { _logger.Error (String.Format ("{0}\nstate: {1}", msg, _state)); return; } @@ -654,18 +746,19 @@ namespace WebSocketSharp.Server _state = ServerState.SHUTDOWN; } - _serviceHosts.Stop (new byte []{}, true); + _serviceHosts.Stop (new byte [0], true); stopListener (5000); _state = ServerState.STOP; } /// - /// Stops receiving the HTTP requests with the specified and - /// used to stop the WebSocket services. + /// Stops receiving the HTTP requests with the specified + /// and used to stop the WebSocket services. /// /// - /// A that contains a status code indicating the reason for stop. + /// A that contains a status code indicating the reason + /// for stop. /// /// /// A that contains the reason for stop. @@ -673,16 +766,15 @@ namespace WebSocketSharp.Server public void Stop (ushort code, string reason) { byte [] data = null; - lock (_sync) - { + lock (_sync) { var msg = _state.CheckIfStarted () ?? code.CheckIfValidCloseStatusCode () ?? (data = code.Append (reason)).CheckIfValidCloseData (); - if (msg != null) - { - _logger.Error (String.Format ( - "{0}\nstate: {1}\ncode: {2}\nreason: {3}", msg, _state, code, reason)); + if (msg != null) { + _logger.Error ( + String.Format ( + "{0}\nstate: {1}\ncode: {2}\nreason: {3}", msg, _state, code, reason)); return; } @@ -697,12 +789,13 @@ namespace WebSocketSharp.Server } /// - /// Stops receiving the HTTP requests with the specified - /// and used to stop the WebSocket services. + /// Stops receiving the HTTP requests with the specified + /// and used to stop the + /// WebSocket services. /// /// - /// One of the values that represent the status codes indicating - /// the reasons for stop. + /// One of the values that represent the status + /// codes indicating the reasons for stop. /// /// /// A that contains the reason for stop. @@ -710,14 +803,14 @@ namespace WebSocketSharp.Server public void Stop (CloseStatusCode code, string reason) { byte [] data = null; - lock (_sync) - { + lock (_sync) { var msg = _state.CheckIfStarted () ?? (data = ((ushort) code).Append (reason)).CheckIfValidCloseData (); - if (msg != null) - { - _logger.Error (String.Format ("{0}\nstate: {1}\nreason: {2}", msg, _state, reason)); + if (msg != null) { + _logger.Error ( + String.Format ("{0}\nstate: {1}\nreason: {2}", msg, _state, reason)); + return; } diff --git a/websocket-sharp/Server/WebSocketServer.cs b/websocket-sharp/Server/WebSocketServer.cs index dd5c0c11..6d19a2ff 100644 --- a/websocket-sharp/Server/WebSocketServer.cs +++ b/websocket-sharp/Server/WebSocketServer.cs @@ -7,7 +7,7 @@ * The MIT License * * Copyright (c) 2012-2013 sta.blockhead - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -17,7 +17,7 @@ * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -39,6 +39,7 @@ using System; using System.Collections.Generic; using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; using System.Text; using System.Threading; using WebSocketSharp.Net; @@ -47,7 +48,8 @@ using WebSocketSharp.Net.WebSockets; namespace WebSocketSharp.Server { /// - /// Provides the functions of the server that receives the WebSocket connection requests. + /// Provides the functions of the server that receives the WebSocket connection + /// requests. /// /// /// The WebSocketServer class provides the multi WebSocket service. @@ -56,25 +58,28 @@ namespace WebSocketSharp.Server { #region Private Fields - private System.Net.IPAddress _address; - private X509Certificate2 _cert; - private TcpListener _listener; - private Logger _logger; - private int _port; - private Thread _receiveRequestThread; - private bool _secure; - private WebSocketServiceHostManager _serviceHosts; - private volatile ServerState _state; - private object _sync; - private Uri _uri; + private System.Net.IPAddress _address; + private AuthenticationSchemes _authSchemes; + private X509Certificate2 _cert; + private Func _credentialsFinder; + private TcpListener _listener; + private Logger _logger; + private int _port; + private string _realm; + private Thread _receiveRequestThread; + private bool _secure; + private WebSocketServiceHostManager _serviceHosts; + private volatile ServerState _state; + private object _sync; + private Uri _uri; #endregion #region Public Constructors /// - /// Initializes a new instance of the class that listens for - /// incoming requests on port 80. + /// Initializes a new instance of the class + /// that listens for incoming requests on port 80. /// public WebSocketServer () : this (80) @@ -82,8 +87,9 @@ namespace WebSocketSharp.Server } /// - /// Initializes a new instance of the class that listens for - /// incoming connection attempts on the specified . + /// Initializes a new instance of the class + /// that listens for incoming connection attempts on the specified + /// . /// /// /// An that contains a port number. @@ -97,8 +103,9 @@ namespace WebSocketSharp.Server } /// - /// Initializes a new instance of the class that listens for - /// incoming connection attempts on the specified WebSocket URL. + /// Initializes a new instance of the class + /// that listens for incoming connection attempts on the specified WebSocket + /// URL. /// /// /// A that contains a WebSocket URL. @@ -121,8 +128,9 @@ namespace WebSocketSharp.Server var host = _uri.DnsSafeHost; _address = host.ToIPAddress (); if (_address == null || !_address.IsLocal ()) - throw new ArgumentException (String.Format ( - "The host part must be the local host name: {0}", host), "url"); + throw new ArgumentException ( + String.Format ( + "The host part must be the local host name: {0}", host), "url"); _port = _uri.Port; _secure = _uri.Scheme == "wss" ? true : false; @@ -131,8 +139,9 @@ namespace WebSocketSharp.Server } /// - /// Initializes a new instance of the class that listens for - /// incoming connection attempts on the specified and . + /// Initializes a new instance of the class + /// that listens for incoming connection attempts on the specified + /// and . /// /// /// An that contains a port number. @@ -153,11 +162,13 @@ namespace WebSocketSharp.Server } /// - /// Initializes a new instance of the class that listens for - /// incoming connection attempts on the specified and . + /// Initializes a new instance of the class + /// that listens for incoming connection attempts on the specified + /// and . /// /// - /// A that represents the local IP address. + /// A that represents the local IP + /// address. /// /// /// An that contains a port number. @@ -177,12 +188,14 @@ namespace WebSocketSharp.Server } /// - /// Initializes a new instance of the class that listens for - /// incoming connection attempts on the specified , - /// and . + /// Initializes a new instance of the class + /// that listens for incoming connection attempts on the specified + /// , and + /// . /// /// - /// A that represents the local IP address. + /// A that represents the local IP + /// address. /// /// /// An that contains a port number. @@ -205,21 +218,25 @@ namespace WebSocketSharp.Server /// -or- /// /// - /// Pair of and is invalid. + /// Pair of and is + /// invalid. /// /// public WebSocketServer (System.Net.IPAddress address, int port, bool secure) { if (!address.IsLocal ()) - throw new ArgumentException (String.Format ( - "Must be the local IP address: {0}", address), "address"); + throw new ArgumentException ( + String.Format ( + "Must be the local IP address: {0}", address), "address"); if (!port.IsPortNumber ()) - throw new ArgumentOutOfRangeException ("port", "Must be between 1 and 65535: " + port); + throw new ArgumentOutOfRangeException ( + "port", "Must be between 1 and 65535: " + port); if ((port == 80 && secure) || (port == 443 && !secure)) - throw new ArgumentException (String.Format ( - "Invalid pair of 'port' and 'secure': {0}, {1}", port, secure)); + throw new ArgumentException ( + String.Format ( + "Invalid pair of 'port' and 'secure': {0}, {1}", port, secure)); _address = address; _port = port; @@ -234,10 +251,12 @@ namespace WebSocketSharp.Server #region Public Properties /// - /// Gets the local IP address on which to listen for incoming connection attempts. + /// Gets the local IP address on which to listen for incoming connection + /// attempts. /// /// - /// A that represents the local IP address. + /// A that represents the local IP + /// address. /// public System.Net.IPAddress Address { get { @@ -246,7 +265,29 @@ namespace WebSocketSharp.Server } /// - /// Gets or sets the certificate used to authenticate the server on the secure connection. + /// Gets or sets the scheme used to authenticate the clients. + /// + /// + /// One of the values + /// that indicates the scheme used to authenticate the clients. The default + /// value is . + /// + public AuthenticationSchemes AuthenticationSchemes { + get { + return _authSchemes; + } + + set { + if (!canSet ("AuthenticationSchemes")) + return; + + _authSchemes = value; + } + } + + /// + /// Gets or sets the certificate used to authenticate the server on the + /// secure connection. /// /// /// A used to authenticate the server. @@ -257,13 +298,8 @@ namespace WebSocketSharp.Server } set { - if (_state == ServerState.START || _state == ServerState.SHUTDOWN) - { - _logger.Error ( - "The value of Certificate property cannot be changed because the server has already been started."); - + if (!canSet ("Certificate")) return; - } _cert = value; } @@ -285,7 +321,8 @@ namespace WebSocketSharp.Server /// Gets a value indicating whether the server provides secure connection. /// /// - /// true if the server provides secure connection; otherwise, false. + /// true if the server provides secure connection; otherwise, + /// false. /// public bool IsSecure { get { @@ -294,11 +331,12 @@ namespace WebSocketSharp.Server } /// - /// Gets or sets a value indicating whether the server cleans up the inactive sessions periodically. + /// Gets or sets a value indicating whether the server cleans up the inactive + /// sessions periodically. /// /// - /// true if the server cleans up the inactive sessions every 60 seconds; - /// otherwise, false. The default value is true. + /// true if the server cleans up the inactive sessions every 60 + /// seconds; otherwise, false. The default value is true. /// public bool KeepClean { get { @@ -314,9 +352,9 @@ namespace WebSocketSharp.Server /// Gets the logging functions. /// /// - /// The default logging level is the . - /// If you want to change the current logging level, you set the Log.Level property - /// to one of the values which you want. + /// The default logging level is the . If you + /// change the current logging level, you set the Log.Level property + /// to any of the values. /// /// /// A that provides the logging functions. @@ -340,10 +378,55 @@ namespace WebSocketSharp.Server } /// - /// Gets the functions for the WebSocket services provided by the server. + /// Gets or sets the name of the realm associated with the + /// . /// /// - /// A that manages the WebSocket services. + /// A that contains the name of the realm. + /// The default value is SECRET AREA. + /// + public string Realm { + get { + return _realm ?? (_realm = "SECRET AREA"); + } + + set { + if (!canSet ("Realm")) + return; + + _realm = value; + } + } + + /// + /// Gets or sets the delegate called to find the credentials for an identity + /// used to authenticate a client. + /// + /// + /// A Func<, > + /// delegate that references the method(s) used to find the credentials. The + /// default value is a function that only returns . + /// + public Func UserCredentialsFinder { + get { + return _credentialsFinder ?? (_credentialsFinder = identity => null); + } + + set { + if (!canSet ("UserCredentialsFinder")) + return; + + _credentialsFinder = value; + } + } + + /// + /// Gets the functions for the WebSocket services provided by the + /// . + /// + /// + /// A that manages the WebSocket + /// services. /// public WebSocketServiceHostManager WebSocketServices { get { @@ -357,8 +440,7 @@ namespace WebSocketSharp.Server private void abort () { - lock (_sync) - { + lock (_sync) { if (!IsListening) return; @@ -367,30 +449,103 @@ namespace WebSocketSharp.Server _listener.Stop (); _serviceHosts.Stop ( - ((ushort) CloseStatusCode.SERVER_ERROR).ToByteArrayInternally (ByteOrder.BIG), true); + ((ushort) CloseStatusCode.SERVER_ERROR).ToByteArrayInternally (ByteOrder.BIG), + true); _state = ServerState.STOP; } + private void acceptRequestAsync (TcpClient client) + { + ThreadPool.QueueUserWorkItem ( + state => { + try { + var context = client.GetWebSocketContext (_cert, _secure, _logger); + if (_authSchemes != AuthenticationSchemes.Anonymous && + !authenticateRequest (_authSchemes, context)) + return; + + acceptWebSocket (context); + } + catch (Exception ex) { + _logger.Fatal (ex.ToString ()); + client.Close (); + } + }); + } + private void acceptWebSocket (TcpListenerWebSocketContext context) { - var websocket = context.WebSocket; - websocket.Log = _logger; - var path = context.Path; + WebSocketServiceHost host; - if (path == null || !_serviceHosts.TryGetServiceHostInternally (path, out host)) - { - websocket.Close (HttpStatusCode.NotImplemented); + if (path == null || + !_serviceHosts.TryGetServiceHostInternally (path, out host)) { + context.Close (HttpStatusCode.NotImplemented); return; } - if (_uri.IsAbsoluteUri) - websocket.Url = new Uri (_uri, path); - host.StartSession (context); } + private bool authenticateRequest ( + AuthenticationSchemes authScheme, TcpListenerWebSocketContext context) + { + var challenge = authScheme == AuthenticationSchemes.Basic + ? HttpUtility.CreateBasicAuthChallenge (Realm) + : authScheme == AuthenticationSchemes.Digest + ? HttpUtility.CreateDigestAuthChallenge (Realm) + : null; + + if (challenge == null) { + context.Close (HttpStatusCode.Forbidden); + return false; + } + + var retry = -1; + var expected = authScheme.ToString (); + var realm = Realm; + var credentialsFinder = UserCredentialsFinder; + Func auth = null; + auth = () => { + retry++; + if (retry > 99) { + context.Close (HttpStatusCode.Forbidden); + return false; + } + + var header = context.Headers ["Authorization"]; + if (header == null || + !header.StartsWith (expected, StringComparison.OrdinalIgnoreCase)) { + context.SendAuthChallenge (challenge); + return auth (); + } + + context.SetUser (authScheme, realm, credentialsFinder); + if (context.IsAuthenticated) + return true; + + context.SendAuthChallenge (challenge); + return auth (); + }; + + return auth (); + } + + private bool canSet (string property) + { + if (_state == ServerState.START || _state == ServerState.SHUTDOWN) { + _logger.Error ( + String.Format ( + "The '{0}' property cannot set a value because the server has already been started.", + property)); + + return false; + } + + return true; + } + private string checkIfCertExists () { return _secure && _cert == null @@ -400,6 +555,7 @@ namespace WebSocketSharp.Server private void init () { + _authSchemes = AuthenticationSchemes.Anonymous; _listener = new TcpListener (_address, _port); _logger = new Logger (); _serviceHosts = new WebSocketServiceHostManager (_logger); @@ -407,32 +563,17 @@ namespace WebSocketSharp.Server _sync = new object (); } - private void processRequestAsync (TcpClient client) - { - WaitCallback callback = state => - { - try { - acceptWebSocket (client.GetWebSocketContext (_secure, _cert)); - } - catch (Exception ex) - { - _logger.Fatal (ex.ToString ()); - client.Close (); - } - }; - - ThreadPool.QueueUserWorkItem (callback); - } - private void receiveRequest () { - while (true) - { + while (true) { try { - processRequestAsync (_listener.AcceptTcpClient ()); + acceptRequestAsync (_listener.AcceptTcpClient ()); } catch (SocketException ex) { - _logger.Warn (String.Format ("Receiving has been stopped.\nreason: {0}.", ex.Message)); + _logger.Warn ( + String.Format ( + "Receiving has been stopped.\nreason: {0}.", ex.Message)); + break; } catch (Exception ex) { @@ -445,7 +586,7 @@ namespace WebSocketSharp.Server abort (); } - private void startReceiveRequestThread () + private void startReceiving () { _receiveRequestThread = new Thread (new ThreadStart (receiveRequest)); _receiveRequestThread.IsBackground = true; @@ -463,8 +604,7 @@ namespace WebSocketSharp.Server if (!uriString.TryCreateWebSocketUri (out result, out message)) return false; - if (result.PathAndQuery != "/") - { + if (result.PathAndQuery != "/") { result = null; message = "Must not contain the path or query component: " + uriString; @@ -479,18 +619,21 @@ namespace WebSocketSharp.Server #region Public Methods /// - /// Adds the specified typed WebSocket service with the specified . + /// Adds the specified typed WebSocket service with the specified + /// . /// /// - /// This method converts to URL-decoded string and - /// removes '/' from tail end of . + /// This method converts to URL-decoded string + /// and removes '/' from tail end of . /// /// - /// A that contains an absolute path to the WebSocket service. + /// A that contains an absolute path to the WebSocket + /// service. /// /// - /// The type of the WebSocket service. The TWithNew must inherit the - /// class and must have a public parameterless constructor. + /// The type of the WebSocket service. The TWithNew must inherit the + /// class and must have a public parameterless + /// constructor. /// public void AddWebSocketService (string servicePath) where TWithNew : WebSocketService, new () @@ -499,42 +642,50 @@ namespace WebSocketSharp.Server } /// - /// Adds the specified typed WebSocket service with the specified and - /// . + /// Adds the specified typed WebSocket service with the specified + /// and . /// /// /// - /// This method converts to URL-decoded string and - /// removes '/' from tail end of . + /// This method converts to URL-decoded + /// string and removes '/' from tail end of + /// . /// /// - /// returns a initialized specified typed WebSocket service - /// instance. + /// returns a initialized specified + /// typed WebSocket service instance. /// /// /// - /// A that contains an absolute path to the WebSocket service. + /// A that contains an absolute path to the WebSocket + /// service. /// /// - /// A Func<T> delegate that references the method used to initialize a new WebSocket service - /// instance (a new WebSocket session). + /// A Func<T> delegate that references the method used to initialize + /// a new WebSocket service instance (a new WebSocket session). /// /// - /// The type of the WebSocket service. The T must inherit the class. + /// The type of the WebSocket service. The T must inherit the + /// class. /// public void AddWebSocketService (string servicePath, Func serviceConstructor) where T : WebSocketService { var msg = servicePath.CheckIfValidServicePath () ?? - (serviceConstructor == null ? "'serviceConstructor' must not be null." : null); + (serviceConstructor == null + ? "'serviceConstructor' must not be null." + : null); + + if (msg != null) { + _logger.Error ( + String.Format ("{0}\nservice path: {1}", msg, servicePath ?? "")); - if (msg != null) - { - _logger.Error (String.Format ("{0}\nservice path: {1}", msg, servicePath ?? "")); return; } - var host = new WebSocketServiceHost (servicePath, serviceConstructor, _logger); + var host = new WebSocketServiceHost ( + servicePath, serviceConstructor, _logger); + if (!KeepClean) host.KeepClean = false; @@ -542,24 +693,28 @@ namespace WebSocketSharp.Server } /// - /// Removes the WebSocket service with the specified . + /// Removes the WebSocket service with the specified + /// . /// /// - /// This method converts to URL-decoded string and - /// removes '/' from tail end of . + /// This method converts to URL-decoded string + /// and removes '/' from tail end of . /// /// - /// true if the WebSocket service is successfully found and removed; otherwise, false. + /// true if the WebSocket service is successfully found and removed; + /// otherwise, false. /// /// - /// A that contains an absolute path to the WebSocket service to find. + /// A that contains an absolute path to the WebSocket + /// service to find. /// public bool RemoveWebSocketService (string servicePath) { var msg = servicePath.CheckIfValidServicePath (); - if (msg != null) - { - _logger.Error (String.Format ("{0}\nservice path: {1}", msg, servicePath ?? "")); + if (msg != null) { + _logger.Error ( + String.Format ("{0}\nservice path: {1}", msg, servicePath)); + return false; } @@ -567,22 +722,23 @@ namespace WebSocketSharp.Server } /// - /// Starts to receive the WebSocket connection requests. + /// Starts receiving the WebSocket connection requests. /// public void Start () { - lock (_sync) - { + lock (_sync) { var msg = _state.CheckIfStopped () ?? checkIfCertExists (); - if (msg != null) - { - _logger.Error (String.Format ("{0}\nstate: {1}\nsecure: {2}", msg, _state, _secure)); + if (msg != null) { + _logger.Error ( + String.Format ( + "{0}\nstate: {1}\nsecure: {2}", msg, _state, _secure)); + return; } _serviceHosts.Start (); _listener.Start (); - startReceiveRequestThread (); + startReceiving (); _state = ServerState.START; } @@ -593,11 +749,9 @@ namespace WebSocketSharp.Server /// public void Stop () { - lock (_sync) - { + lock (_sync) { var msg = _state.CheckIfStarted (); - if (msg != null) - { + if (msg != null) { _logger.Error (String.Format ("{0}\nstate: {1}", msg, _state)); return; } @@ -606,17 +760,18 @@ namespace WebSocketSharp.Server } stopListener (5000); - _serviceHosts.Stop (new byte []{}, true); + _serviceHosts.Stop (new byte [0], true); _state = ServerState.STOP; } /// - /// Stops receiving the WebSocket connection requests with the specified and - /// . + /// Stops receiving the WebSocket connection requests with the specified + /// and . /// /// - /// A that contains a status code indicating the reason for stop. + /// A that contains a status code indicating the reason + /// for stop. /// /// /// A that contains the reason for stop. @@ -624,16 +779,15 @@ namespace WebSocketSharp.Server public void Stop (ushort code, string reason) { byte [] data = null; - lock (_sync) - { + lock (_sync) { var msg = _state.CheckIfStarted () ?? code.CheckIfValidCloseStatusCode () ?? (data = code.Append (reason)).CheckIfValidCloseData (); - if (msg != null) - { - _logger.Error (String.Format ( - "{0}\nstate: {1}\ncode: {2}\nreason: {3}", msg, _state, code, reason)); + if (msg != null) { + _logger.Error ( + String.Format ( + "{0}\nstate: {1}\ncode: {2}\nreason: {3}", msg, _state, code, reason)); return; } @@ -648,12 +802,12 @@ namespace WebSocketSharp.Server } /// - /// Stops receiving the WebSocket connection requests with the specified - /// and . + /// Stops receiving the WebSocket connection requests with the specified + /// and . /// /// - /// One of the values that represent the status codes indicating - /// the reasons for stop. + /// One of the values that represent the status + /// codes indicating the reasons for stop. /// /// /// A that contains the reason for stop. @@ -661,14 +815,14 @@ namespace WebSocketSharp.Server public void Stop (CloseStatusCode code, string reason) { byte [] data = null; - lock (_sync) - { + lock (_sync) { var msg = _state.CheckIfStarted () ?? (data = ((ushort) code).Append (reason)).CheckIfValidCloseData (); - if (msg != null) - { - _logger.Error (String.Format ("{0}\nstate: {1}\nreason: {2}", msg, _state, reason)); + if (msg != null) { + _logger.Error ( + String.Format ("{0}\nstate: {1}\nreason: {2}", msg, _state, reason)); + return; } diff --git a/websocket-sharp/WebSocket.cs b/websocket-sharp/WebSocket.cs index 7ed42887..65390cdc 100644 --- a/websocket-sharp/WebSocket.cs +++ b/websocket-sharp/WebSocket.cs @@ -3,13 +3,15 @@ * WebSocket.cs * * A C# implementation of the WebSocket interface. - * This code derived from WebSocket.java (http://github.com/adamac/Java-WebSocket-client). + * + * This code is derived from WebSocket.java + * (http://github.com/adamac/Java-WebSocket-client). * * The MIT License * * Copyright (c) 2009 Adam MacBeth - * Copyright (c) 2010-2013 sta.blockhead - * + * Copyright (c) 2010-2014 sta.blockhead + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -19,7 +21,7 @@ * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -51,8 +53,9 @@ namespace WebSocketSharp /// Implements the WebSocket interface. /// /// - /// The WebSocket class provides a set of methods and properties for two-way communication - /// using the WebSocket protocol (RFC 6455). + /// The WebSocket class provides a set of methods and properties for two-way + /// communication using the WebSocket protocol + /// (RFC 6455). /// public class WebSocket : IDisposable { @@ -65,6 +68,7 @@ namespace WebSocketSharp #region Private Fields + private AuthenticationChallenge _authChallenge; private string _base64key; private RemoteCertificateValidationCallback _certValidationCallback; @@ -75,12 +79,13 @@ namespace WebSocketSharp private CookieCollection _cookies; private Func _cookiesValidation; - private WsCredential _credentials; + private NetworkCredential _credentials; private string _extensions; private AutoResetEvent _exitReceiving; private object _forClose; private object _forSend; private volatile Logger _logger; + private uint _nonceCount; private string _origin; private bool _preAuth; private string _protocol; @@ -109,8 +114,6 @@ namespace WebSocketSharp _extensions = String.Empty; _forClose = new object (); _forSend = new object (); - _origin = String.Empty; - _preAuth = false; _protocol = String.Empty; _readyState = WebSocketState.CONNECTING; } @@ -119,20 +122,20 @@ namespace WebSocketSharp #region Internal Constructors - internal WebSocket (HttpListenerWebSocketContext context) + internal WebSocket (HttpListenerWebSocketContext context, Logger logger) : this () { _stream = context.Stream; - _closeContext = () => context.Close (); - init (context); + _closeContext = context.Close; + init (context, logger); } - internal WebSocket (TcpListenerWebSocketContext context) + internal WebSocket (TcpListenerWebSocketContext context, Logger logger) : this () { _stream = context.Stream; - _closeContext = () => context.Close (); - init (context); + _closeContext = context.Close; + init (context, logger); } #endregion @@ -140,14 +143,15 @@ namespace WebSocketSharp #region Public Constructors /// - /// Initializes a new instance of the class with the specified WebSocket URL - /// and subprotocols. + /// Initializes a new instance of the class with the + /// specified WebSocket URL and subprotocols. /// /// /// A that contains a WebSocket URL to connect. /// /// - /// An array of that contains the WebSocket subprotocols if any. + /// An array of that contains the WebSocket subprotocols + /// if any. /// /// /// is . @@ -173,12 +177,13 @@ namespace WebSocketSharp } /// - /// Initializes a new instance of the class with the specified WebSocket URL, - /// OnOpen, OnMessage, OnError, OnClose event handlers and subprotocols. + /// Initializes a new instance of the class with the + /// specified WebSocket URL, OnOpen, OnMessage, OnError, OnClose event + /// handlers and subprotocols. /// /// - /// This constructor initializes a new instance of the class and - /// establishes a WebSocket connection. + /// This constructor initializes a new instance of the + /// class and establishes a WebSocket connection. /// /// /// A that contains a WebSocket URL to connect. @@ -196,7 +201,8 @@ namespace WebSocketSharp /// An event handler. /// /// - /// An array of that contains the WebSocket subprotocols if any. + /// An array of that contains the WebSocket subprotocols + /// if any. /// /// /// is . @@ -237,7 +243,8 @@ namespace WebSocketSharp internal bool IsOpened { get { - return _readyState == WebSocketState.OPEN || _readyState == WebSocketState.CLOSING; + return _readyState == WebSocketState.OPEN || + _readyState == WebSocketState.CLOSING; } } @@ -246,11 +253,13 @@ namespace WebSocketSharp #region Public Properties /// - /// Gets or sets the compression method used to compress the payload data of the WebSocket Data frame. + /// Gets or sets the compression method used to compress the payload data of + /// the WebSocket Data frame. /// /// - /// One of the values that indicates the compression method to use. - /// The default is . + /// One of the values that represents the + /// compression method to use. + /// The default value is . /// public CompressionMethod Compression { get { @@ -258,9 +267,13 @@ namespace WebSocketSharp } set { - if (IsOpened) - { - var msg = "A WebSocket connection has already been established."; + var msg = !_client + ? "Set operation of Compression isn't available as a server." + : IsOpened + ? "A WebSocket connection has already been established." + : null; + + if (msg != null) { _logger.Error (msg); error (msg); @@ -272,16 +285,15 @@ namespace WebSocketSharp } /// - /// Gets the cookies used in the WebSocket opening handshake. + /// Gets the cookies used in the WebSocket connection request. /// /// - /// An IEnumerable<Cookie> interface that provides an enumerator which supports the iteration - /// over the collection of cookies. + /// An IEnumerable<Cookie> interface that provides an enumerator which + /// supports the iteration over the collection of cookies. /// public IEnumerable Cookies { get { - lock (_cookies.SyncRoot) - { + lock (_cookies.SyncRoot) { return from Cookie cookie in _cookies select cookie; } @@ -292,9 +304,10 @@ namespace WebSocketSharp /// Gets the credentials for HTTP authentication (Basic/Digest). /// /// - /// A that contains the credentials for HTTP authentication. + /// A that represents the credentials for + /// HTTP authentication. The default value is . /// - public WsCredential Credentials { + public NetworkCredential Credentials { get { return _credentials; } @@ -304,7 +317,8 @@ namespace WebSocketSharp /// Gets the WebSocket extensions selected by the server. /// /// - /// A that contains the extensions if any. The default is . + /// A that represents the WebSocket extensions if any. + /// The default value is . /// public string Extensions { get { @@ -340,9 +354,9 @@ namespace WebSocketSharp /// Gets the logging functions. /// /// - /// The default logging level is the . - /// If you want to change the current logging level, you set the Log.Level property - /// to one of the values which you want. + /// The default logging level is the . If you + /// change the current logging level, you set the Log.Level property + /// to any of the values. /// /// /// A that provides the logging functions. @@ -361,19 +375,22 @@ namespace WebSocketSharp } /// - /// Gets or sets the value of the Origin header used in the WebSocket opening handshake. + /// Gets or sets the value of the Origin header used in the WebSocket + /// connection request. /// /// - /// A instance does not send the Origin header in the WebSocket opening handshake - /// if the value of this property is . + /// The sends the Origin header if this property has + /// any. /// /// /// - /// A that contains the value of the HTTP Origin header to send. - /// The default is . + /// A that represents the value of the + /// HTTP Origin + /// header to send. The default value is . /// /// - /// The value of the Origin header has the following syntax: <scheme>://<host>[:<port>] + /// The Origin header has the following syntax: + /// <scheme>://<host>[:<port>] /// /// public string Origin { @@ -383,24 +400,22 @@ namespace WebSocketSharp set { string msg = null; - if (IsOpened) - { + if (!_client) + msg = "Set operation of Origin isn't available as a server."; + else if (IsOpened) msg = "A WebSocket connection has already been established."; - } - else if (value.IsNullOrEmpty ()) - { - _origin = String.Empty; + else if (value.IsNullOrEmpty ()) { + _origin = value; return; } - else - { - var origin = new Uri (value); - if (!origin.IsAbsoluteUri || origin.Segments.Length > 1) - msg = "The syntax of value of Origin must be '://[:]'."; + else { + Uri origin; + if (!Uri.TryCreate (value, UriKind.Absolute, out origin) || + origin.Segments.Length > 1) + msg = "The syntax of Origin must be '://[:]'."; } - if (msg != null) - { + if (msg != null) { _logger.Error (msg); error (msg); @@ -415,7 +430,8 @@ namespace WebSocketSharp /// Gets the WebSocket subprotocol selected by the server. /// /// - /// A that contains the subprotocol if any. The default is . + /// A that represents the subprotocol if any. + /// The default value is . /// public string Protocol { get { @@ -437,15 +453,17 @@ namespace WebSocketSharp } /// - /// Gets or sets the callback used to validate the certificate supplied by the server. + /// Gets or sets the callback used to validate the certificate supplied by + /// the server. /// /// - /// If the value of this property is , the validation does nothing - /// with the server certificate, always returns valid. + /// If the value of this property is , the validation + /// does nothing with the server certificate, always returns valid. /// /// - /// A delegate that references the method(s) - /// used to validate the server certificate. The default is . + /// A delegate that + /// references the method(s) used to validate the server certificate. + /// The default value is . /// public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get { @@ -453,6 +471,19 @@ namespace WebSocketSharp } set { + var msg = !_client + ? "Set operation of ServerCertificateValidationCallback isn't available as a server." + : IsOpened + ? "A WebSocket connection has already been established." + : null; + + if (msg != null) { + _logger.Error (msg); + error (msg); + + return; + } + _certValidationCallback = value; } } @@ -461,7 +492,7 @@ namespace WebSocketSharp /// Gets the WebSocket URL to connect. /// /// - /// A that contains the WebSocket URL to connect. + /// A that represents the WebSocket URL to connect. /// public Uri Url { get { @@ -469,8 +500,7 @@ namespace WebSocketSharp } internal set { - if (_readyState == WebSocketState.CONNECTING && !_client) - _uri = value; + _uri = value; } } @@ -684,6 +714,18 @@ namespace WebSocketSharp return Convert.ToBase64String (src); } + // As client + private string createExtensionsRequest () + { + var extensions = new StringBuilder (64); + if (_compression != CompressionMethod.NONE) + extensions.Append (_compression.ToCompressionExtension ()); + + return extensions.Length > 0 + ? extensions.ToString () + : String.Empty; + } + // As client private HandshakeRequest createHandshakeRequest () { @@ -693,24 +735,35 @@ namespace WebSocketSharp : _uri.Authority; var req = new HandshakeRequest (path); - req.AddHeader ("Host", host); + var headers = req.Headers; - if (_origin.Length > 0) - req.AddHeader ("Origin", _origin); + headers ["Host"] = host; - req.AddHeader ("Sec-WebSocket-Key", _base64key); + if (!_origin.IsNullOrEmpty ()) + headers ["Origin"] = _origin; + + headers ["Sec-WebSocket-Key"] = _base64key; if (!_protocols.IsNullOrEmpty ()) - req.AddHeader ("Sec-WebSocket-Protocol", _protocols); + headers ["Sec-WebSocket-Protocol"] = _protocols; - var extensions = createRequestExtensions (); + var extensions = createExtensionsRequest (); if (extensions.Length > 0) - req.AddHeader ("Sec-WebSocket-Extensions", extensions); + headers ["Sec-WebSocket-Extensions"] = extensions; - req.AddHeader ("Sec-WebSocket-Version", _version); + headers ["Sec-WebSocket-Version"] = _version; - if (_preAuth && _credentials != null) - req.SetAuthorization (new AuthenticationResponse (_credentials)); + AuthenticationResponse authRes = null; + if (_authChallenge != null && _credentials != null) { + authRes = new AuthenticationResponse ( + _authChallenge, _credentials, _nonceCount); + _nonceCount = authRes.NonceCount; + } + else if (_preAuth) + authRes = new AuthenticationResponse (_credentials); + + if (authRes != null) + headers ["Authorization"] = authRes.ToString (); if (_cookies.Count > 0) req.SetCookies (_cookies); @@ -721,14 +774,16 @@ namespace WebSocketSharp // As server private HandshakeResponse createHandshakeResponse () { - var res = new HandshakeResponse (); - res.AddHeader ("Sec-WebSocket-Accept", createResponseKey ()); + var res = new HandshakeResponse (HttpStatusCode.SwitchingProtocols); + var headers = res.Headers; + + headers ["Sec-WebSocket-Accept"] = createResponseKey (); if (_protocol.Length > 0) - res.AddHeader ("Sec-WebSocket-Protocol", _protocol); + headers ["Sec-WebSocket-Protocol"] = _protocol; if (_extensions.Length > 0) - res.AddHeader ("Sec-WebSocket-Extensions", _extensions); + headers ["Sec-WebSocket-Extensions"] = _extensions; if (_cookies.Count > 0) res.SetCookies (_cookies); @@ -740,23 +795,11 @@ namespace WebSocketSharp private HandshakeResponse createHandshakeResponse (HttpStatusCode code) { var res = HandshakeResponse.CreateCloseResponse (code); - res.AddHeader ("Sec-WebSocket-Version", _version); + res.Headers ["Sec-WebSocket-Version"] = _version; return res; } - // As client - private string createRequestExtensions () - { - var extensions = new StringBuilder (64); - if (_compression != CompressionMethod.NONE) - extensions.Append (_compression.ToCompressionExtension ()); - - return extensions.Length > 0 - ? extensions.ToString () - : String.Empty; - } - private string createResponseKey () { var buffer = new StringBuilder (_base64key, 64); @@ -808,12 +851,12 @@ namespace WebSocketSharp } // As server - private void init (WebSocketContext context) + private void init (WebSocketContext context, Logger logger) { _context = context; - _uri = context.Path.ToUri (); + _logger = logger; + _uri = context.RequestUri; _secure = context.IsSecureConnection; - _client = false; } private void open () @@ -1168,7 +1211,7 @@ namespace WebSocketSharp int readLen = 0; byte [] buffer = null; - // Not fragmented + // Not fragment if (quo == 0) { buffer = new byte [rem]; @@ -1219,11 +1262,21 @@ namespace WebSocketSharp { var req = createHandshakeRequest (); var res = sendHandshakeRequest (req); - if (!_preAuth && res.IsUnauthorized && _credentials != null) - { - var challenge = res.AuthChallenge; - req.SetAuthorization (new AuthenticationResponse (_credentials, challenge)); - res = sendHandshakeRequest (req); + if (res.IsUnauthorized) { + _authChallenge = res.AuthChallenge; + if (_credentials != null && + (!_preAuth || _authChallenge.Scheme == "digest")) { + if (res.Headers.Contains ("Connection", "close")) { + closeClientResources (); + setClientStream (); + } + + var authRes = new AuthenticationResponse ( + _authChallenge, _credentials, _nonceCount); + _nonceCount = authRes.NonceCount; + req.Headers ["Authorization"] = authRes.ToString (); + res = sendHandshakeRequest (req); + } } return res; @@ -1316,16 +1369,22 @@ namespace WebSocketSharp #region Internal Methods // As server - internal void Close (HttpStatusCode code) + internal void Close (HandshakeResponse response) { _readyState = WebSocketState.CLOSING; - send (createHandshakeResponse (code)); + send (response); closeServerResources (); - + _readyState = WebSocketState.CLOSED; } + // As server + internal void Close (HttpStatusCode code) + { + Close (createHandshakeResponse (code)); + } + // As server internal void Close (CloseEventArgs args, byte [] frameAsBytes, int waitTimeOut) { @@ -1824,78 +1883,79 @@ namespace WebSocketSharp } /// - /// Sets a used in the WebSocket opening handshake. + /// Sets a used in the WebSocket connection request. /// /// - /// A that contains an HTTP Cookie to set. + /// A that represents an HTTP Cookie to set. /// public void SetCookie (Cookie cookie) { - var msg = IsOpened - ? "A WebSocket connection has already been established." - : cookie == null - ? "'cookie' must not be null." - : null; + var msg = !_client + ? "SetCookie isn't available as a server." + : IsOpened + ? "A WebSocket connection has already been established." + : cookie == null + ? "'cookie' must not be null." + : null; - if (msg != null) - { + if (msg != null) { _logger.Error (msg); error (msg); return; } - lock (_cookies.SyncRoot) - { + lock (_cookies.SyncRoot) { _cookies.SetOrRemove (cookie); } } /// - /// Sets the credentials for HTTP authentication (Basic/Digest). + /// Sets a pair of the and + /// for HTTP authentication (Basic/Digest). /// - /// - /// A that contains a user name associated with the credentials. + /// + /// A that represents the user name used to authenticate. /// /// - /// A that contains a password for associated with the credentials. + /// A that represents the password for + /// used to authenticate. /// /// - /// true if sends the credentials as a Basic authorization with the first request handshake; - /// otherwise, false. + /// true if the sends a Basic authentication + /// credentials with the first connection request; otherwise, false. /// - public void SetCredentials (string userName, string password, bool preAuth) + public void SetCredentials (string username, string password, bool preAuth) { string msg = null; - if (IsOpened) - { + if (!_client) + msg = "SetCredentials isn't available as a server."; + else if (IsOpened) msg = "A WebSocket connection has already been established."; - } - else if (userName == null) - { + else if (username.IsNullOrEmpty ()) { _credentials = null; _preAuth = false; + _logger.Warn ("Credentials was set back to the default."); return; } - else - { - msg = userName.Length > 0 && (userName.Contains (':') || !userName.IsText ()) - ? "'userName' contains an invalid character." + else { + msg = username.Contains (':') || !username.IsText () + ? "'username' contains an invalid character." : !password.IsNullOrEmpty () && !password.IsText () ? "'password' contains an invalid character." : null; } - if (msg != null) - { + if (msg != null) { _logger.Error (msg); error (msg); return; } - _credentials = new WsCredential (userName, password, _uri.PathAndQuery); + _credentials = new NetworkCredential ( + username, password, _uri.PathAndQuery); _preAuth = preAuth; } diff --git a/websocket-sharp/WsCredential.cs b/websocket-sharp/WsCredential.cs deleted file mode 100644 index 91875559..00000000 --- a/websocket-sharp/WsCredential.cs +++ /dev/null @@ -1,119 +0,0 @@ -#region License -/* - * WsCredential.cs - * - * The MIT License - * - * Copyright (c) 2013 sta.blockhead - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -#endregion - -using System; - -namespace WebSocketSharp { - - /// - /// Provides the credentials for HTTP authentication (Basic/Digest). - /// - public class WsCredential { - - #region Private Fields - - string _domain; - string _password; - string _userName; - - #endregion - - #region Internal Constructors - - internal WsCredential() - { - } - - internal WsCredential(string userName, string password) - : this(userName, password, null) - { - } - - internal WsCredential(string userName, string password, string domain) - { - _userName = userName; - _password = password; - _domain = domain; - } - - #endregion - - #region Public Properties - - /// - /// Gets the name of the user domain associated with the credentials. - /// - /// - /// A that contains the name of the user domain associated with the credentials. - /// Currently, returns the request uri of a WebSocket opening handshake. - /// - public string Domain { - get { - return _domain ?? String.Empty; - } - - internal set { - _domain = value; - } - } - - /// - /// Gets the password for the user name associated with the credentials. - /// - /// - /// A that contains the password for the user name associated with the credentials. - /// - public string Password { - get { - return _password ?? String.Empty; - } - - internal set { - _password = value; - } - } - - /// - /// Gets the user name associated with the credentials. - /// - /// - /// A that contains the user name associated with the credentials. - /// - public string UserName { - get { - return _userName ?? String.Empty; - } - - internal set { - _userName = value; - } - } - - #endregion - } -} diff --git a/websocket-sharp/WsStream.cs b/websocket-sharp/WsStream.cs index da33859f..92cacc52 100644 --- a/websocket-sharp/WsStream.cs +++ b/websocket-sharp/WsStream.cs @@ -4,8 +4,8 @@ * * The MIT License * - * Copyright (c) 2010-2013 sta.blockhead - * + * Copyright (c) 2010-2014 sta.blockhead + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -15,7 +15,7 @@ * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -122,11 +122,11 @@ namespace WebSocketSharp return new WsStream (netStream); } - internal static WsStream CreateServerStream (TcpClient client, bool secure, X509Certificate cert) + internal static WsStream CreateServerStream ( + TcpClient client, X509Certificate cert, bool secure) { var netStream = client.GetStream (); - if (secure) - { + if (secure) { var sslStream = new SslStream (netStream, false); sslStream.AuthenticateAsServer (cert); diff --git a/websocket-sharp/websocket-sharp.csproj b/websocket-sharp/websocket-sharp.csproj index e7245723..9fcae76a 100644 --- a/websocket-sharp/websocket-sharp.csproj +++ b/websocket-sharp/websocket-sharp.csproj @@ -116,7 +116,6 @@ - @@ -128,6 +127,9 @@ + + +