diff --git a/Example/bin/Debug/example.exe b/Example/bin/Debug/example.exe index db440730..3a91d762 100755 Binary files a/Example/bin/Debug/example.exe and b/Example/bin/Debug/example.exe differ diff --git a/Example/bin/Debug/example.exe.mdb b/Example/bin/Debug/example.exe.mdb index dfe384b5..f30bf839 100644 Binary files a/Example/bin/Debug/example.exe.mdb and b/Example/bin/Debug/example.exe.mdb differ diff --git a/Example/bin/Debug/websocket-sharp.dll b/Example/bin/Debug/websocket-sharp.dll index 0901b46b..3950c3fe 100755 Binary files a/Example/bin/Debug/websocket-sharp.dll and b/Example/bin/Debug/websocket-sharp.dll differ diff --git a/Example/bin/Debug/websocket-sharp.dll.mdb b/Example/bin/Debug/websocket-sharp.dll.mdb index 615a3a20..f3c03973 100644 Binary files a/Example/bin/Debug/websocket-sharp.dll.mdb and b/Example/bin/Debug/websocket-sharp.dll.mdb differ diff --git a/Example/bin/Debug_Ubuntu/example.exe b/Example/bin/Debug_Ubuntu/example.exe index a9777f9f..63febc5c 100755 Binary files a/Example/bin/Debug_Ubuntu/example.exe and b/Example/bin/Debug_Ubuntu/example.exe differ diff --git a/Example/bin/Debug_Ubuntu/example.exe.mdb b/Example/bin/Debug_Ubuntu/example.exe.mdb index 8a27087d..3a9268e1 100644 Binary files a/Example/bin/Debug_Ubuntu/example.exe.mdb and b/Example/bin/Debug_Ubuntu/example.exe.mdb differ diff --git a/Example/bin/Debug_Ubuntu/websocket-sharp.dll b/Example/bin/Debug_Ubuntu/websocket-sharp.dll index 764c252c..b2c76b4d 100755 Binary files a/Example/bin/Debug_Ubuntu/websocket-sharp.dll and b/Example/bin/Debug_Ubuntu/websocket-sharp.dll differ diff --git a/Example/bin/Debug_Ubuntu/websocket-sharp.dll.mdb b/Example/bin/Debug_Ubuntu/websocket-sharp.dll.mdb index f63a5858..86949826 100644 Binary files a/Example/bin/Debug_Ubuntu/websocket-sharp.dll.mdb and b/Example/bin/Debug_Ubuntu/websocket-sharp.dll.mdb differ diff --git a/Example/bin/Release/example.exe b/Example/bin/Release/example.exe index 6fd15adc..19465893 100755 Binary files a/Example/bin/Release/example.exe and b/Example/bin/Release/example.exe differ diff --git a/Example/bin/Release/websocket-sharp.dll b/Example/bin/Release/websocket-sharp.dll index cae2760e..d33d5220 100755 Binary files a/Example/bin/Release/websocket-sharp.dll and b/Example/bin/Release/websocket-sharp.dll differ diff --git a/Example/bin/Release_Ubuntu/example.exe b/Example/bin/Release_Ubuntu/example.exe index 4199bbd5..c732d6c1 100755 Binary files a/Example/bin/Release_Ubuntu/example.exe and b/Example/bin/Release_Ubuntu/example.exe differ diff --git a/Example/bin/Release_Ubuntu/websocket-sharp.dll b/Example/bin/Release_Ubuntu/websocket-sharp.dll index d8d1e088..4160d674 100755 Binary files a/Example/bin/Release_Ubuntu/websocket-sharp.dll and b/Example/bin/Release_Ubuntu/websocket-sharp.dll differ diff --git a/Example1/bin/Debug/example1.exe b/Example1/bin/Debug/example1.exe index 17fa2131..6405f7b5 100755 Binary files a/Example1/bin/Debug/example1.exe and b/Example1/bin/Debug/example1.exe differ diff --git a/Example1/bin/Debug/example1.exe.mdb b/Example1/bin/Debug/example1.exe.mdb index 5391beb5..f332a487 100644 Binary files a/Example1/bin/Debug/example1.exe.mdb and b/Example1/bin/Debug/example1.exe.mdb differ diff --git a/Example1/bin/Debug/websocket-sharp.dll b/Example1/bin/Debug/websocket-sharp.dll index 0901b46b..3950c3fe 100755 Binary files a/Example1/bin/Debug/websocket-sharp.dll and b/Example1/bin/Debug/websocket-sharp.dll differ diff --git a/Example1/bin/Debug/websocket-sharp.dll.mdb b/Example1/bin/Debug/websocket-sharp.dll.mdb index 615a3a20..f3c03973 100644 Binary files a/Example1/bin/Debug/websocket-sharp.dll.mdb and b/Example1/bin/Debug/websocket-sharp.dll.mdb differ diff --git a/Example1/bin/Debug_Ubuntu/example1.exe b/Example1/bin/Debug_Ubuntu/example1.exe index 4837cbb7..3b7d93cc 100755 Binary files a/Example1/bin/Debug_Ubuntu/example1.exe and b/Example1/bin/Debug_Ubuntu/example1.exe differ diff --git a/Example1/bin/Debug_Ubuntu/example1.exe.mdb b/Example1/bin/Debug_Ubuntu/example1.exe.mdb index 5760a7fb..22c62b47 100644 Binary files a/Example1/bin/Debug_Ubuntu/example1.exe.mdb and b/Example1/bin/Debug_Ubuntu/example1.exe.mdb differ diff --git a/Example1/bin/Debug_Ubuntu/websocket-sharp.dll b/Example1/bin/Debug_Ubuntu/websocket-sharp.dll index 764c252c..b2c76b4d 100755 Binary files a/Example1/bin/Debug_Ubuntu/websocket-sharp.dll and b/Example1/bin/Debug_Ubuntu/websocket-sharp.dll differ diff --git a/Example1/bin/Debug_Ubuntu/websocket-sharp.dll.mdb b/Example1/bin/Debug_Ubuntu/websocket-sharp.dll.mdb index f63a5858..86949826 100644 Binary files a/Example1/bin/Debug_Ubuntu/websocket-sharp.dll.mdb and b/Example1/bin/Debug_Ubuntu/websocket-sharp.dll.mdb differ diff --git a/Example1/bin/Release/example1.exe b/Example1/bin/Release/example1.exe index 437b2c97..8721bb86 100755 Binary files a/Example1/bin/Release/example1.exe and b/Example1/bin/Release/example1.exe differ diff --git a/Example1/bin/Release/websocket-sharp.dll b/Example1/bin/Release/websocket-sharp.dll index cae2760e..d33d5220 100755 Binary files a/Example1/bin/Release/websocket-sharp.dll and b/Example1/bin/Release/websocket-sharp.dll differ diff --git a/Example1/bin/Release_Ubuntu/example1.exe b/Example1/bin/Release_Ubuntu/example1.exe index 5b92a16b..d4dfc94e 100755 Binary files a/Example1/bin/Release_Ubuntu/example1.exe and b/Example1/bin/Release_Ubuntu/example1.exe differ diff --git a/Example1/bin/Release_Ubuntu/websocket-sharp.dll b/Example1/bin/Release_Ubuntu/websocket-sharp.dll index d8d1e088..4160d674 100755 Binary files a/Example1/bin/Release_Ubuntu/websocket-sharp.dll and b/Example1/bin/Release_Ubuntu/websocket-sharp.dll differ diff --git a/Example2/Example2.pidb b/Example2/Example2.pidb index 4d9246a4..c540fa46 100644 Binary files a/Example2/Example2.pidb and b/Example2/Example2.pidb differ diff --git a/Example2/bin/Debug/example2.exe b/Example2/bin/Debug/example2.exe index f649e800..eb52472d 100755 Binary files a/Example2/bin/Debug/example2.exe and b/Example2/bin/Debug/example2.exe differ diff --git a/Example2/bin/Debug/example2.exe.mdb b/Example2/bin/Debug/example2.exe.mdb index bc9dee95..ae882dc2 100644 Binary files a/Example2/bin/Debug/example2.exe.mdb and b/Example2/bin/Debug/example2.exe.mdb differ diff --git a/Example2/bin/Debug/websocket-sharp.dll b/Example2/bin/Debug/websocket-sharp.dll index 0901b46b..3950c3fe 100755 Binary files a/Example2/bin/Debug/websocket-sharp.dll and b/Example2/bin/Debug/websocket-sharp.dll differ diff --git a/Example2/bin/Debug/websocket-sharp.dll.mdb b/Example2/bin/Debug/websocket-sharp.dll.mdb index 615a3a20..f3c03973 100644 Binary files a/Example2/bin/Debug/websocket-sharp.dll.mdb and b/Example2/bin/Debug/websocket-sharp.dll.mdb differ diff --git a/Example2/bin/Debug_Ubuntu/example2.exe b/Example2/bin/Debug_Ubuntu/example2.exe index 27c01fbd..a3a0fdd8 100755 Binary files a/Example2/bin/Debug_Ubuntu/example2.exe and b/Example2/bin/Debug_Ubuntu/example2.exe differ diff --git a/Example2/bin/Debug_Ubuntu/example2.exe.mdb b/Example2/bin/Debug_Ubuntu/example2.exe.mdb index 4d39e0d2..f707c081 100644 Binary files a/Example2/bin/Debug_Ubuntu/example2.exe.mdb and b/Example2/bin/Debug_Ubuntu/example2.exe.mdb differ diff --git a/Example2/bin/Debug_Ubuntu/websocket-sharp.dll b/Example2/bin/Debug_Ubuntu/websocket-sharp.dll index 764c252c..b2c76b4d 100755 Binary files a/Example2/bin/Debug_Ubuntu/websocket-sharp.dll and b/Example2/bin/Debug_Ubuntu/websocket-sharp.dll differ diff --git a/Example2/bin/Debug_Ubuntu/websocket-sharp.dll.mdb b/Example2/bin/Debug_Ubuntu/websocket-sharp.dll.mdb index f63a5858..86949826 100644 Binary files a/Example2/bin/Debug_Ubuntu/websocket-sharp.dll.mdb and b/Example2/bin/Debug_Ubuntu/websocket-sharp.dll.mdb differ diff --git a/Example2/bin/Release/example2.exe b/Example2/bin/Release/example2.exe index 72fab764..063d17da 100755 Binary files a/Example2/bin/Release/example2.exe and b/Example2/bin/Release/example2.exe differ diff --git a/Example2/bin/Release/websocket-sharp.dll b/Example2/bin/Release/websocket-sharp.dll index cae2760e..d33d5220 100755 Binary files a/Example2/bin/Release/websocket-sharp.dll and b/Example2/bin/Release/websocket-sharp.dll differ diff --git a/Example2/bin/Release_Ubuntu/example2.exe b/Example2/bin/Release_Ubuntu/example2.exe index 387bc3b7..4ea4e2c1 100755 Binary files a/Example2/bin/Release_Ubuntu/example2.exe and b/Example2/bin/Release_Ubuntu/example2.exe differ diff --git a/Example2/bin/Release_Ubuntu/websocket-sharp.dll b/Example2/bin/Release_Ubuntu/websocket-sharp.dll index d8d1e088..4160d674 100755 Binary files a/Example2/bin/Release_Ubuntu/websocket-sharp.dll and b/Example2/bin/Release_Ubuntu/websocket-sharp.dll differ diff --git a/Example3/App.config b/Example3/App.config new file mode 100644 index 00000000..c1a9c3b8 --- /dev/null +++ b/Example3/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/Example3/AssemblyInfo.cs b/Example3/AssemblyInfo.cs new file mode 100644 index 00000000..c5396f60 --- /dev/null +++ b/Example3/AssemblyInfo.cs @@ -0,0 +1,27 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("Example3")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("sta")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] + diff --git a/Example3/Chat.cs b/Example3/Chat.cs new file mode 100644 index 00000000..55a14374 --- /dev/null +++ b/Example3/Chat.cs @@ -0,0 +1,14 @@ +using System; +using WebSocketSharp; +using WebSocketSharp.Server; + +namespace Example3 +{ + public class Chat : WebSocketService + { + protected override void onMessage(object sender, MessageEventArgs e) + { + Publish(e.Data); + } + } +} diff --git a/Example3/Echo.cs b/Example3/Echo.cs new file mode 100644 index 00000000..34c294c9 --- /dev/null +++ b/Example3/Echo.cs @@ -0,0 +1,19 @@ +using System; +using WebSocketSharp; +using WebSocketSharp.Server; + +namespace Example3 +{ + public class Echo : WebSocketService + { + protected override void onMessage(object sender, MessageEventArgs e) + { + Send(e.Data); + } + + protected override void onClose(object sender, CloseEventArgs e) + { + Console.WriteLine("[Echo] Close({0}: {1})", (ushort)e.Code, e.Code); + } + } +} diff --git a/Example3/Example3.csproj b/Example3/Example3.csproj new file mode 100644 index 00000000..cfb81b7b --- /dev/null +++ b/Example3/Example3.csproj @@ -0,0 +1,76 @@ + + + + Debug + AnyCPU + 9.0.21022 + 2.0 + {C648BA25-77E5-4A40-A97F-D0AA37B9FB26} + Exe + Example3 + Example3 + v3.5 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + true + + + none + false + bin\Release + prompt + 4 + true + + + true + full + false + bin\Debug_Ubuntu + DEBUG; + prompt + 4 + true + + + none + false + bin\Release_Ubuntu + prompt + 4 + true + + + + + + + + + + + + + + + {B357BAC7-529E-4D81-A0D2-71041B19C8DE} + websocket-sharp + + + + + + + + + + + + \ No newline at end of file diff --git a/Example3/Example3.pidb b/Example3/Example3.pidb new file mode 100644 index 00000000..820e77cc Binary files /dev/null and b/Example3/Example3.pidb differ diff --git a/Example3/Program.cs b/Example3/Program.cs new file mode 100644 index 00000000..b93b1413 --- /dev/null +++ b/Example3/Program.cs @@ -0,0 +1,72 @@ +using System; +using System.Configuration; +using System.IO; +using System.Text; +using WebSocketSharp; +using WebSocketSharp.Net; +using WebSocketSharp.Server; + +namespace Example3 +{ + public class Program + { + private static HttpServer _httpsv; + + public static void Main(string[] args) + { + _httpsv = new HttpServer(4649); + + _httpsv.OnResponse += (sender, e) => + { + onResponse(e.Context); + }; + + _httpsv.OnError += (sender, e) => + { + Console.WriteLine(e.Message); + }; + + _httpsv.Start(); + Console.WriteLine("HTTP Server listening on port: {0}\n", _httpsv.Port); + + Console.WriteLine("Press any key to stop server..."); + Console.ReadLine(); + + _httpsv.Stop(); + } + + private static byte[] getContent(string path) + { + if (path == "/") + path += "index.html"; + + return _httpsv.GetFile(path); + } + + private static void onGet(HttpListenerRequest request, HttpListenerResponse response) + { + var content = getContent(request.RawUrl); + if (content != null) + { + response.WriteContent(content); + return; + } + + response.StatusCode = (int)HttpStatusCode.NotFound; + } + + private static void onResponse(HttpListenerContext context) + { + var req = context.Request; + var res = context.Response; + + if (req.HttpMethod == "GET") + { + onGet(req, res); + return; + } + + res.StatusCode = (int)HttpStatusCode.MethodNotAllowed; + } + } +} diff --git a/Example3/Public/Js/echotest.js b/Example3/Public/Js/echotest.js new file mode 100644 index 00000000..3f61e039 --- /dev/null +++ b/Example3/Public/Js/echotest.js @@ -0,0 +1,67 @@ +/** + * echotest.js + * Derived from Echo Test of WebSocket.org (http://www.websocket.org/echo.html) + * + * Copyright (c) 2012 Kaazing Corporation. + * + */ + +var wsUri = "ws://localhost:4649/"; +var output; + +function init(){ + output = document.getElementById("output"); + testWebSocket(); +} + +function testWebSocket(){ + websocket = new WebSocket(wsUri); + + websocket.onopen = function(evt){ + onOpen(evt) + }; + + websocket.onclose = function(evt){ + onClose(evt) + }; + + websocket.onmessage = function(evt){ + onMessage(evt) + }; + + websocket.onerror = function(evt){ + onError(evt) + }; +} + +function onOpen(evt){ + writeToScreen("CONNECTED"); + doSend("WebSocket rocks"); +} + +function onClose(evt){ + writeToScreen("DISCONNECTED"); +} + +function onMessage(evt){ + writeToScreen('RESPONSE: ' + evt.data + ''); + websocket.close(); +} + +function onError(evt){ + writeToScreen('ERROR: ' + evt.data + ''); +} + +function doSend(message){ + writeToScreen("SENT: " + message); + websocket.send(message); +} + +function writeToScreen(message){ + var pre = document.createElement("p"); + pre.style.wordWrap = "break-word"; + pre.innerHTML = message; + output.appendChild(pre); +} + +window.addEventListener("load", init, false); \ No newline at end of file diff --git a/Example3/Public/index.html b/Example3/Public/index.html new file mode 100644 index 00000000..8d6fe43c --- /dev/null +++ b/Example3/Public/index.html @@ -0,0 +1,12 @@ + + + + WebSocket Echo Test + + + +

WebSocket Echo Test

+
+ + diff --git a/Example3/bin/Debug/Example3.exe b/Example3/bin/Debug/Example3.exe new file mode 100755 index 00000000..66dca144 Binary files /dev/null and b/Example3/bin/Debug/Example3.exe differ diff --git a/Example3/bin/Debug/Example3.exe.config b/Example3/bin/Debug/Example3.exe.config new file mode 100644 index 00000000..c1a9c3b8 --- /dev/null +++ b/Example3/bin/Debug/Example3.exe.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/Example3/bin/Debug/Example3.exe.mdb b/Example3/bin/Debug/Example3.exe.mdb new file mode 100644 index 00000000..6a5a0b11 Binary files /dev/null and b/Example3/bin/Debug/Example3.exe.mdb differ diff --git a/Example3/bin/Debug/websocket-sharp.dll b/Example3/bin/Debug/websocket-sharp.dll new file mode 100755 index 00000000..3950c3fe Binary files /dev/null and b/Example3/bin/Debug/websocket-sharp.dll differ diff --git a/Example3/bin/Debug/websocket-sharp.dll.mdb b/Example3/bin/Debug/websocket-sharp.dll.mdb new file mode 100644 index 00000000..f3c03973 Binary files /dev/null and b/Example3/bin/Debug/websocket-sharp.dll.mdb differ diff --git a/Example3/bin/Debug_Ubuntu/Example3.exe b/Example3/bin/Debug_Ubuntu/Example3.exe new file mode 100755 index 00000000..9e052a7c Binary files /dev/null and b/Example3/bin/Debug_Ubuntu/Example3.exe differ diff --git a/Example3/bin/Debug_Ubuntu/Example3.exe.config b/Example3/bin/Debug_Ubuntu/Example3.exe.config new file mode 100644 index 00000000..c1a9c3b8 --- /dev/null +++ b/Example3/bin/Debug_Ubuntu/Example3.exe.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/Example3/bin/Debug_Ubuntu/Example3.exe.mdb b/Example3/bin/Debug_Ubuntu/Example3.exe.mdb new file mode 100644 index 00000000..958b095a Binary files /dev/null and b/Example3/bin/Debug_Ubuntu/Example3.exe.mdb differ diff --git a/Example3/bin/Debug_Ubuntu/websocket-sharp.dll b/Example3/bin/Debug_Ubuntu/websocket-sharp.dll new file mode 100755 index 00000000..b2c76b4d Binary files /dev/null and b/Example3/bin/Debug_Ubuntu/websocket-sharp.dll differ diff --git a/Example3/bin/Debug_Ubuntu/websocket-sharp.dll.mdb b/Example3/bin/Debug_Ubuntu/websocket-sharp.dll.mdb new file mode 100644 index 00000000..86949826 Binary files /dev/null and b/Example3/bin/Debug_Ubuntu/websocket-sharp.dll.mdb differ diff --git a/Example3/bin/Release/Example3.exe b/Example3/bin/Release/Example3.exe new file mode 100755 index 00000000..c21abd53 Binary files /dev/null and b/Example3/bin/Release/Example3.exe differ diff --git a/Example3/bin/Release/Example3.exe.config b/Example3/bin/Release/Example3.exe.config new file mode 100644 index 00000000..c1a9c3b8 --- /dev/null +++ b/Example3/bin/Release/Example3.exe.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/Example3/bin/Release/websocket-sharp.dll b/Example3/bin/Release/websocket-sharp.dll new file mode 100755 index 00000000..d33d5220 Binary files /dev/null and b/Example3/bin/Release/websocket-sharp.dll differ diff --git a/Example3/bin/Release_Ubuntu/Example3.exe b/Example3/bin/Release_Ubuntu/Example3.exe new file mode 100755 index 00000000..cbafb085 Binary files /dev/null and b/Example3/bin/Release_Ubuntu/Example3.exe differ diff --git a/Example3/bin/Release_Ubuntu/Example3.exe.config b/Example3/bin/Release_Ubuntu/Example3.exe.config new file mode 100644 index 00000000..c1a9c3b8 --- /dev/null +++ b/Example3/bin/Release_Ubuntu/Example3.exe.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/Example3/bin/Release_Ubuntu/websocket-sharp.dll b/Example3/bin/Release_Ubuntu/websocket-sharp.dll new file mode 100755 index 00000000..4160d674 Binary files /dev/null and b/Example3/bin/Release_Ubuntu/websocket-sharp.dll differ diff --git a/README.md b/README.md index e9d89d19..f6def7fa 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,18 @@ Stopping server. wssv.Stop(); ``` +### HTTP Server with WebSocket ### + +I modified System.Net.HttpListener, System.Net.HttpListenerContext and some others of [Mono] to create the HTTP server that the connection can be upgraded to the WebSocket connection if the HTTP server received a WebSocket request. + +You can specify the WebSocket service in the same way as the WebSocket server. + +```cs +var httpsv = new HttpServer(4649); +``` + +For more information, please refer to the [Example3]. + ## Examples ## Examples of using **websocket-sharp**. @@ -253,7 +265,11 @@ Examples of using **websocket-sharp**. ### Example2 ### -[Example2] starts WebSocket server. +[Example2] starts the WebSocket server. + +### Example3 ### + +[Example3] starts the HTTP server that the connection can be upgraded to the WebSocket connection. ## Supported WebSocket Protocol ## @@ -286,10 +302,12 @@ Licensed under the **[MIT License]**. [Example]: https://github.com/sta/websocket-sharp/tree/master/Example [Example1]: https://github.com/sta/websocket-sharp/tree/master/Example1 [Example2]: https://github.com/sta/websocket-sharp/tree/master/Example2 +[Example3]: https://github.com/sta/websocket-sharp/tree/master/Example3 [hixie-75]: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75 [hybi-00]: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00 [Json.NET]: http://james.newtonking.com/projects/json-net.aspx [MIT License]: http://www.opensource.org/licenses/mit-license.php +[Mono]: http://www.mono-project.com/ [RFC 6455]: http://tools.ietf.org/html/rfc6455 [The WebSocket API]: http://dev.w3.org/html5/websockets [The WebSocket API 日本語訳]: http://www.hcn.zaq.ne.jp/___/WEB/WebSocket-ja.html diff --git a/websocket-sharp.sln b/websocket-sharp.sln index ef2da1cf..3c20e06a 100644 --- a/websocket-sharp.sln +++ b/websocket-sharp.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example1", "Example1\Exampl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example2", "Example2\Example2.csproj", "{B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example3", "Example3\Example3.csproj", "{C648BA25-77E5-4A40-A97F-D0AA37B9FB26}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -49,6 +51,14 @@ Global {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Release_Ubuntu|Any CPU.Build.0 = Release_Ubuntu|Any CPU {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Release|Any CPU.ActiveCfg = Release|Any CPU {B81A24C8-25BB-42B2-AF99-1E1EACCE74C7}.Release|Any CPU.Build.0 = Release|Any CPU + {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Debug_Ubuntu|Any CPU.ActiveCfg = Debug_Ubuntu|Any CPU + {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Debug_Ubuntu|Any CPU.Build.0 = Debug_Ubuntu|Any CPU + {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Release_Ubuntu|Any CPU.ActiveCfg = Release_Ubuntu|Any CPU + {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Release_Ubuntu|Any CPU.Build.0 = Release_Ubuntu|Any CPU + {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C648BA25-77E5-4A40-A97F-D0AA37B9FB26}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution StartupItem = websocket-sharp\websocket-sharp.csproj diff --git a/websocket-sharp/CloseEventArgs.cs b/websocket-sharp/CloseEventArgs.cs index a4ff419f..37c91085 100644 --- a/websocket-sharp/CloseEventArgs.cs +++ b/websocket-sharp/CloseEventArgs.cs @@ -69,19 +69,18 @@ namespace WebSocketSharp public CloseEventArgs(PayloadData data) : base(Opcode.CLOSE, data) { - _code = data.ToBytes().SubArray(0, 2).To(ByteOrder.BIG); + _code = (ushort)CloseStatusCode.NO_STATUS_CODE; + _reason = String.Empty; + _wasClean = false; + + if (data.Length >= 2) + _code = data.ToBytes().SubArray(0, 2).To(ByteOrder.BIG); if (data.Length > 2) { var buffer = data.ToBytes().SubArray(2, (int)(data.Length - 2)); _reason = Encoding.UTF8.GetString(buffer); } - else - { - _reason = String.Empty; - } - - _wasClean = false; } } } diff --git a/websocket-sharp/Ext.cs b/websocket-sharp/Ext.cs index 670aa64b..4f50ef7d 100644 --- a/websocket-sharp/Ext.cs +++ b/websocket-sharp/Ext.cs @@ -1,11 +1,17 @@ #region MIT License /** * Ext.cs + * IsPredefinedScheme and MaybeUri methods derived from System.Uri * * The MIT License * + * (C) 2001 Garrett Rooney (System.Uri) + * (C) 2003 Ian MacLean (System.Uri) + * (C) 2003 Ben Maurer (System.Uri) + * Copyright (C) 2003,2009 Novell, Inc (http://www.novell.com) (System.Uri) + * Copyright (c) 2009 Stephane Delcroix (System.Uri) * Copyright (c) 2010-2012 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 @@ -28,9 +34,11 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.IO; using System.Linq; using System.Text; +using WebSocketSharp.Net; namespace WebSocketSharp { @@ -64,6 +72,26 @@ namespace WebSocketSharp return b == Convert.ToByte(c); } + public static bool Exists(this NameValueCollection headers, string name) + { + return headers[name] != null + ? true + : false; + } + + public static bool Exists(this NameValueCollection headers, string name, string value) + { + var values = headers[name]; + if (values == null) + return false; + + foreach (string v in values.Split(',')) + if (String.Compare(v.Trim(), value, true) == 0) + return true; + + return false; + } + public static string GetHeaderValue(this string src, string separater) { int i = src.IndexOf(separater); @@ -94,6 +122,39 @@ namespace WebSocketSharp return false; } + // Derived from System.Uri.IsPredefinedScheme method + public static bool IsPredefinedScheme(this string scheme) + { + if (scheme == null && scheme.Length < 3) + return false; + + char c = scheme[0]; + + if (c == 'h') + return (scheme == "http" || scheme == "https"); + + if (c == 'f') + return (scheme == "file" || scheme == "ftp"); + + if (c == 'n') + { + c = scheme[1]; + + if (c == 'e') + return (scheme == "news" || scheme == "net.pipe" || scheme == "net.tcp"); + + if (scheme == "nntp") + return true; + + return false; + } + + if ((c == 'g' && scheme == "gopher") || (c == 'm' && scheme == "mailto")) + return true; + + return false; + } + public static bool NotEqualsDo( this string expected, string actual, @@ -111,6 +172,19 @@ namespace WebSocketSharp return false; } + // Derived from System.Uri.MaybeUri method + public static bool MaybeUri(this string uriString) + { + int p = uriString.IndexOf(':'); + if (p == -1) + return false; + + if (p >= 10) + return false; + + return uriString.Substring(0, p).IsPredefinedScheme(); + } + public static byte[] ReadBytes(this TStream stream, ulong length, int bufferLength) where TStream : System.IO.Stream { @@ -320,5 +394,21 @@ namespace WebSocketSharp return sb.ToString(); } + + public static Uri ToUri(this string uriString) + { + if (!uriString.MaybeUri()) + return new Uri(uriString, UriKind.Relative); + + return new Uri(uriString); + } + + public static void WriteContent(this HttpListenerResponse response, byte[] content) + { + var output = response.OutputStream; + response.ContentLength64 = content.Length; + output.Write(content, 0, content.Length); + output.Close(); + } } } diff --git a/websocket-sharp/Handshake.cs b/websocket-sharp/Handshake.cs index 6504b44f..577a24ba 100644 --- a/websocket-sharp/Handshake.cs +++ b/websocket-sharp/Handshake.cs @@ -29,19 +29,36 @@ using System; using System.Collections.Specialized; using System.Text; +using WebSocketSharp.Net; namespace WebSocketSharp { public abstract class Handshake { + #region Field + protected const string _crlf = "\r\n"; + #endregion + + #region Constructor + protected Handshake() { + ProtocolVersion = HttpVersion.Version11; + Headers = new NameValueCollection(); } - public NameValueCollection Headers { get; protected set; } - public string Version { get; protected set; } + #endregion + + #region Properties + + public NameValueCollection Headers { get; protected set; } + public Version ProtocolVersion { get; protected set; } + + #endregion + + #region Methods public void AddHeader(string name, string value) { @@ -55,27 +72,19 @@ namespace WebSocketSharp { public bool HeaderExists(string name) { - return Headers[name] != null - ? true - : false; + return Headers.Exists(name); } public bool HeaderExists(string name, string value) { - var values = GetHeaderValues(name); - if (values == null) - return false; - - foreach (string v in values) - if (String.Compare(value, v, true) == 0) - return true; - - return false; + return Headers.Exists(name, value); } public byte[] ToBytes() { return Encoding.UTF8.GetBytes(ToString()); } + + #endregion } } diff --git a/websocket-sharp/Net/AuthenticationSchemeSelector.cs b/websocket-sharp/Net/AuthenticationSchemeSelector.cs new file mode 100644 index 00000000..24e7dd03 --- /dev/null +++ b/websocket-sharp/Net/AuthenticationSchemeSelector.cs @@ -0,0 +1,35 @@ +// +// AuthenticationSchemeSelector.cs +// Copied from System.Net.AuthenticationSchemeSelector +// +// Author: +// Gonzalo Paniagua Javier +// +// 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. +// + +using System; + +namespace WebSocketSharp.Net { + + public delegate AuthenticationSchemes AuthenticationSchemeSelector (HttpListenerRequest httpRequest); +} diff --git a/websocket-sharp/Net/AuthenticationSchemes.cs b/websocket-sharp/Net/AuthenticationSchemes.cs new file mode 100644 index 00000000..e5fb7724 --- /dev/null +++ b/websocket-sharp/Net/AuthenticationSchemes.cs @@ -0,0 +1,45 @@ +// +// AuthenticationSchemes.cs +// Copied from System.Net.AuthenticationSchemes +// +// 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. +// + +using System; + +namespace WebSocketSharp.Net { + + [Flags] + public enum AuthenticationSchemes { + + None, + Digest = 1, + Negotiate = 2, + Ntlm = 4, + IntegratedWindowsAuthentication = 6, + Basic = 8, + Anonymous = 0x8000, + } +} diff --git a/websocket-sharp/Net/ChunkStream.cs b/websocket-sharp/Net/ChunkStream.cs new file mode 100644 index 00000000..dd31605a --- /dev/null +++ b/websocket-sharp/Net/ChunkStream.cs @@ -0,0 +1,342 @@ +// +// ChunkStream.cs +// Copied from System.Net.ChunkStream +// +// Authors: +// Gonzalo Paniagua Javier (gonzalo@ximian.com) +// +// (C) 2003 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. +// + +using System; +using System.Collections; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; + +namespace WebSocketSharp.Net { + + class ChunkStream { + + enum State { + None, + Body, + BodyFinished, + Trailer + } + + class Chunk { + public byte [] Bytes; + public int Offset; + + public Chunk (byte [] chunk) + { + this.Bytes = chunk; + } + + public int Read (byte [] buffer, int offset, int size) + { + int nread = (size > Bytes.Length - Offset) ? Bytes.Length - Offset : size; + Buffer.BlockCopy (Bytes, Offset, buffer, offset, nread); + Offset += nread; + return nread; + } + } + + internal WebHeaderCollection headers; + int chunkSize; + int chunkRead; + State state; + //byte [] waitBuffer; + StringBuilder saved; + bool sawCR; + bool gotit; + int trailerState; + ArrayList chunks; + + public ChunkStream (byte [] buffer, int offset, int size, WebHeaderCollection headers) + : this (headers) + { + Write (buffer, offset, size); + } + + public ChunkStream (WebHeaderCollection headers) + { + this.headers = headers; + saved = new StringBuilder (); + chunks = new ArrayList (); + chunkSize = -1; + } + + public void ResetBuffer () + { + chunkSize = -1; + chunkRead = 0; + chunks.Clear (); + } + + public void WriteAndReadBack (byte [] buffer, int offset, int size, ref int read) + { + if (offset + read > 0) + Write (buffer, offset, offset+read); + read = Read (buffer, offset, size); + } + + public int Read (byte [] buffer, int offset, int size) + { + return ReadFromChunks (buffer, offset, size); + } + + int ReadFromChunks (byte [] buffer, int offset, int size) + { + int count = chunks.Count; + int nread = 0; + for (int i = 0; i < count; i++) { + Chunk chunk = (Chunk) chunks [i]; + if (chunk == null) + continue; + + if (chunk.Offset == chunk.Bytes.Length) { + chunks [i] = null; + continue; + } + + nread += chunk.Read (buffer, offset + nread, size - nread); + if (nread == size) + break; + } + + return nread; + } + + public void Write (byte [] buffer, int offset, int size) + { + InternalWrite (buffer, ref offset, size); + } + + void InternalWrite (byte [] buffer, ref int offset, int size) + { + if (state == State.None) { + state = GetChunkSize (buffer, ref offset, size); + if (state == State.None) + return; + + saved.Length = 0; + sawCR = false; + gotit = false; + } + + if (state == State.Body && offset < size) { + state = ReadBody (buffer, ref offset, size); + if (state == State.Body) + return; + } + + if (state == State.BodyFinished && offset < size) { + state = ReadCRLF (buffer, ref offset, size); + if (state == State.BodyFinished) + return; + + sawCR = false; + } + + if (state == State.Trailer && offset < size) { + state = ReadTrailer (buffer, ref offset, size); + if (state == State.Trailer) + return; + + saved.Length = 0; + sawCR = false; + gotit = false; + } + + if (offset < size) + InternalWrite (buffer, ref offset, size); + } + + public bool WantMore { + get { return (chunkRead != chunkSize || chunkSize != 0 || state != State.None); } + } + + public int ChunkLeft { + get { return chunkSize - chunkRead; } + } + + State ReadBody (byte [] buffer, ref int offset, int size) + { + if (chunkSize == 0) + return State.BodyFinished; + + int diff = size - offset; + if (diff + chunkRead > chunkSize) + diff = chunkSize - chunkRead; + + byte [] chunk = new byte [diff]; + Buffer.BlockCopy (buffer, offset, chunk, 0, diff); + chunks.Add (new Chunk (chunk)); + offset += diff; + chunkRead += diff; + return (chunkRead == chunkSize) ? State.BodyFinished : State.Body; + + } + + State GetChunkSize (byte [] buffer, ref int offset, int size) + { + char c = '\0'; + while (offset < size) { + c = (char) buffer [offset++]; + if (c == '\r') { + if (sawCR) + ThrowProtocolViolation ("2 CR found"); + + sawCR = true; + continue; + } + + if (sawCR && c == '\n') + break; + + if (c == ' ') + gotit = true; + + if (!gotit) + saved.Append (c); + + if (saved.Length > 20) + ThrowProtocolViolation ("chunk size too long."); + } + + if (!sawCR || c != '\n') { + if (offset < size) + ThrowProtocolViolation ("Missing \\n"); + + try { + if (saved.Length > 0) { + chunkSize = Int32.Parse (RemoveChunkExtension (saved.ToString ()), NumberStyles.HexNumber); + } + } catch (Exception) { + ThrowProtocolViolation ("Cannot parse chunk size."); + } + + return State.None; + } + + chunkRead = 0; + try { + chunkSize = Int32.Parse (RemoveChunkExtension (saved.ToString ()), NumberStyles.HexNumber); + } catch (Exception) { + ThrowProtocolViolation ("Cannot parse chunk size."); + } + + if (chunkSize == 0) { + trailerState = 2; + return State.Trailer; + } + + return State.Body; + } + + static string RemoveChunkExtension (string input) + { + int idx = input.IndexOf (';'); + if (idx == -1) + return input; + return input.Substring (0, idx); + } + + State ReadCRLF (byte [] buffer, ref int offset, int size) + { + if (!sawCR) { + if ((char) buffer [offset++] != '\r') + ThrowProtocolViolation ("Expecting \\r"); + + sawCR = true; + if (offset == size) + return State.BodyFinished; + } + + if (sawCR && (char) buffer [offset++] != '\n') + ThrowProtocolViolation ("Expecting \\n"); + + return State.None; + } + + State ReadTrailer (byte [] buffer, ref int offset, int size) + { + char c = '\0'; + + // short path + if (trailerState == 2 && (char) buffer [offset] == '\r' && saved.Length == 0) { + offset++; + if (offset < size && (char) buffer [offset] == '\n') { + offset++; + return State.None; + } + offset--; + } + + int st = trailerState; + string stString = "\r\n\r"; + while (offset < size && st < 4) { + c = (char) buffer [offset++]; + if ((st == 0 || st == 2) && c == '\r') { + st++; + continue; + } + + if ((st == 1 || st == 3) && c == '\n') { + st++; + continue; + } + + if (st > 0) { + saved.Append (stString.Substring (0, saved.Length == 0? st-2: st)); + st = 0; + if (saved.Length > 4196) + ThrowProtocolViolation ("Error reading trailer (too long)."); + } + } + + if (st < 4) { + trailerState = st; + if (offset < size) + ThrowProtocolViolation ("Error reading trailer."); + + return State.Trailer; + } + + StringReader reader = new StringReader (saved.ToString ()); + string line; + while ((line = reader.ReadLine ()) != null && line != "") + headers.Add (line); + + return State.None; + } + + static void ThrowProtocolViolation (string message) + { + WebException we = new WebException (message, null, WebExceptionStatus.ServerProtocolViolation, null); + throw we; + } + } +} diff --git a/websocket-sharp/Net/ChunkedInputStream.cs b/websocket-sharp/Net/ChunkedInputStream.cs new file mode 100644 index 00000000..df988bc0 --- /dev/null +++ b/websocket-sharp/Net/ChunkedInputStream.cs @@ -0,0 +1,181 @@ +// +// ChunkedInputStream.cs +// Copied from System.Net.ChunkedInputStream +// +// Authors: +// 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. + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace WebSocketSharp.Net { + + class ChunkedInputStream : RequestStream + { + HttpListenerContext context; + ChunkStream decoder; + bool disposed; + bool no_more_data; + + class ReadBufferState { + + public HttpStreamAsyncResult Ares; + public byte [] Buffer; + public int Count; + public int InitialCount; + public int Offset; + + public ReadBufferState ( + byte [] buffer, int offset, int count, HttpStreamAsyncResult ares) + { + Buffer = buffer; + Offset = offset; + Count = count; + InitialCount = count; + Ares = ares; + } + } + + public ChunkedInputStream ( + HttpListenerContext context, Stream stream, byte [] buffer, int offset, int length) + : base (stream, buffer, offset, length) + { + this.context = context; + WebHeaderCollection coll = (WebHeaderCollection) context.Request.Headers; + decoder = new ChunkStream (coll); + } + + public ChunkStream Decoder { + get { return decoder; } + set { decoder = value; } + } + + void OnRead (IAsyncResult base_ares) + { + ReadBufferState rb = (ReadBufferState) base_ares.AsyncState; + HttpStreamAsyncResult ares = rb.Ares; + try { + int nread = base.EndRead (base_ares); + decoder.Write (ares.Buffer, ares.Offset, nread); + nread = decoder.Read (rb.Buffer, rb.Offset, rb.Count); + rb.Offset += nread; + rb.Count -= nread; + if (rb.Count == 0 || !decoder.WantMore || nread == 0) { + no_more_data = !decoder.WantMore && nread == 0; + ares.Count = rb.InitialCount - rb.Count; + ares.Complete (); + return; + } + ares.Offset = 0; + ares.Count = Math.Min (8192, decoder.ChunkLeft + 6); + base.BeginRead (ares.Buffer, ares.Offset, ares.Count, OnRead, rb); + } catch (Exception e) { + context.Connection.SendError (e.Message, 400); + ares.Complete (e); + } + } + + public override IAsyncResult BeginRead ( + byte [] buffer, int offset, int count, AsyncCallback cback, object state) + { + if (disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (buffer == null) + throw new ArgumentNullException ("buffer"); + + int len = buffer.Length; + if (offset < 0 || offset > len) + throw new ArgumentOutOfRangeException ("offset exceeds the size of buffer"); + + if (count < 0 || offset > len - count) + throw new ArgumentOutOfRangeException ("offset+size exceeds the size of buffer"); + + HttpStreamAsyncResult ares = new HttpStreamAsyncResult (); + ares.Callback = cback; + ares.State = state; + if (no_more_data) { + ares.Complete (); + return ares; + } + int nread = decoder.Read (buffer, offset, count); + offset += nread; + count -= nread; + if (count == 0) { + // got all we wanted, no need to bother the decoder yet + ares.Count = nread; + ares.Complete (); + return ares; + } + if (!decoder.WantMore) { + no_more_data = nread == 0; + ares.Count = nread; + ares.Complete (); + return ares; + } + ares.Buffer = new byte [8192]; + ares.Offset = 0; + ares.Count = 8192; + ReadBufferState rb = new ReadBufferState (buffer, offset, count, ares); + rb.InitialCount += nread; + base.BeginRead (ares.Buffer, ares.Offset, ares.Count, OnRead, rb); + return ares; + } + + public override void Close () + { + if (!disposed) { + disposed = true; + base.Close (); + } + } + + public override int EndRead (IAsyncResult ares) + { + if (disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + HttpStreamAsyncResult my_ares = ares as HttpStreamAsyncResult; + if (ares == null) + throw new ArgumentException ("Invalid IAsyncResult", "ares"); + + if (!ares.IsCompleted) + ares.AsyncWaitHandle.WaitOne (); + + if (my_ares.Error != null) + throw new HttpListenerException (400, "I/O operation aborted."); + + return my_ares.Count; + } + + public override int Read ([In,Out] byte [] buffer, int offset, int count) + { + IAsyncResult ares = BeginRead (buffer, offset, count, null, null); + return EndRead (ares); + } + } +} diff --git a/websocket-sharp/Net/Cookie.cs b/websocket-sharp/Net/Cookie.cs new file mode 100644 index 00000000..74a1d077 --- /dev/null +++ b/websocket-sharp/Net/Cookie.cs @@ -0,0 +1,350 @@ +// +// Cookie.cs +// Copied from System.Net.Cookie +// +// Authors: +// Lawrence Pit (loz@cable.a2000.nl) +// Gonzalo Paniagua Javier (gonzalo@ximian.com) +// Daniel Nauck (dna@mono-project.de) +// Sebastien Pouliot (sebastien@ximian.com) +// +// Copyright (C) 2004,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 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. +// + +using System; +using System.Text; +using System.Globalization; +using System.Collections; +using System.Net; + +namespace WebSocketSharp.Net { + + // Supported cookie formats are: + // Netscape: http://home.netscape.com/newsref/std/cookie_spec.html + // RFC 2109: http://www.ietf.org/rfc/rfc2109.txt + // RFC 2965: http://www.ietf.org/rfc/rfc2965.txt + [Serializable] + public sealed class Cookie + { + string comment; + Uri commentUri; + bool discard; + string domain; + DateTime expires; + bool httpOnly; + string name; + string path; + string port; + int [] ports; + bool secure; + DateTime timestamp; + string val; + int version; + + static char [] reservedCharsName = new char [] {' ', '=', ';', ',', '\n', '\r', '\t'}; + static char [] portSeparators = new char [] {'"', ','}; + static string tspecials = "()<>@,;:\\\"/[]?={} \t"; // from RFC 2965, 2068 + + public Cookie () + { + expires = DateTime.MinValue; + timestamp = DateTime.Now; + domain = String.Empty; + name = String.Empty; + val = String.Empty; + comment = String.Empty; + port = String.Empty; + } + + public Cookie (string name, string value) + : this () + { + Name = name; + Value = value; + } + + public Cookie (string name, string value, string path) + : this (name, value) + { + Path = path; + } + + public Cookie (string name, string value, string path, string domain) + : this (name, value, path) + { + Domain = domain; + } + + public string Comment { + get { return comment; } + set { comment = value == null ? String.Empty : value; } + } + + public Uri CommentUri { + get { return commentUri; } + set { commentUri = value; } + } + + public bool Discard { + get { return discard; } + set { discard = value; } + } + + public string Domain { + get { return domain; } + set { + if (String.IsNullOrEmpty (value)) { + domain = String.Empty; + ExactDomain = true; + } else { + domain = value; + ExactDomain = (value [0] != '.'); + } + } + } + + internal bool ExactDomain { get; set; } + + public bool Expired { + get { + return expires <= DateTime.Now && + expires != DateTime.MinValue; + } + set { + if (value) + expires = DateTime.Now; + } + } + + public DateTime Expires { + get { return expires; } + set { expires = value; } + } + + public bool HttpOnly { + get { return httpOnly; } + set { httpOnly = value; } + } + + public string Name { + get { return name; } + set { + if (String.IsNullOrEmpty (value)) + throw new CookieException ("Name cannot be empty"); + + if (value [0] == '$' || value.IndexOfAny (reservedCharsName) != -1) { + // see CookieTest, according to MS implementation + // the name value changes even though it's incorrect + name = String.Empty; + throw new CookieException ("Name contains invalid characters"); + } + + name = value; + } + } + + public string Path { + get { return (path == null) ? String.Empty : path; } + set { path = (value == null) ? String.Empty : value; } + } + + public string Port { + get { return port; } + set { + if (String.IsNullOrEmpty (value)) { + port = String.Empty; + return; + } + if (value [0] != '"' || value [value.Length - 1] != '"') { + throw new CookieException("The 'Port'='" + value + "' part of the cookie is invalid. Port must be enclosed by double quotes."); + } + port = value; + string [] values = port.Split (portSeparators); + ports = new int[values.Length]; + for (int i = 0; i < ports.Length; i++) { + ports [i] = Int32.MinValue; + if (values [i].Length == 0) + continue; + try { + ports [i] = Int32.Parse (values [i]); + } catch (Exception e) { + throw new CookieException("The 'Port'='" + value + "' part of the cookie is invalid. Invalid value: " + values [i], e); + } + } + Version = 1; + } + } + + internal int [] Ports { + get { return ports; } + } + + public bool Secure { + get { return secure; } + set { secure = value; } + } + + public DateTime TimeStamp { + get { return timestamp; } + } + + public string Value { + get { return val; } + set { + if (value == null) { + val = String.Empty; + return; + } + + // LAMESPEC: According to .Net specs the Value property should not accept + // the semicolon and comma characters, yet it does. For now we'll follow + // the behaviour of MS.Net instead of the specs. + /* + if (value.IndexOfAny(reservedCharsValue) != -1) + throw new CookieException("Invalid value. Value cannot contain semicolon or comma characters."); + */ + + val = value; + } + } + + public int Version { + get { return version; } + set { + if ((value < 0) || (value > 10)) + version = 0; + else + version = value; + } + } + + public override bool Equals (Object obj) + { + System.Net.Cookie c = obj as System.Net.Cookie; + + return c != null && + String.Compare (this.name, c.Name, true, CultureInfo.InvariantCulture) == 0 && + String.Compare (this.val, c.Value, false, CultureInfo.InvariantCulture) == 0 && + String.Compare (this.Path, c.Path, false, CultureInfo.InvariantCulture) == 0 && + String.Compare (this.domain, c.Domain, true, CultureInfo.InvariantCulture) == 0 && + this.version == c.Version; + } + + public override int GetHashCode () + { + return hash ( + StringComparer.InvariantCultureIgnoreCase.GetHashCode (name), + val.GetHashCode (), + Path.GetHashCode (), + StringComparer.InvariantCultureIgnoreCase.GetHashCode (domain), + version); + } + + private static int hash (int i, int j, int k, int l, int m) + { + return i ^ (j << 13 | j >> 19) ^ (k << 26 | k >> 6) ^ (l << 7 | l >> 25) ^ (m << 20 | m >> 12); + } + + // returns a string that can be used to send a cookie to an Origin Server + // i.e., only used for clients + // see para 4.2.2 of RFC 2109 and para 3.3.4 of RFC 2965 + // see also bug #316017 + public override string ToString () + { + return ToString (null); + } + + internal string ToString (Uri uri) + { + if (name.Length == 0) + return String.Empty; + + StringBuilder result = new StringBuilder (64); + + if (version > 0) + result.Append ("$Version=").Append (version).Append ("; "); + + result.Append (name).Append ("=").Append (val); + + if (version == 0) + return result.ToString (); + + if (!String.IsNullOrEmpty (path)) + result.Append ("; $Path=").Append (path); + else if (uri != null) + result.Append ("; $Path=/").Append (path); + + bool append_domain = (uri == null) || (uri.Host != domain); + if (append_domain && !String.IsNullOrEmpty (domain)) + result.Append ("; $Domain=").Append (domain); + + if (port != null && port.Length != 0) + result.Append ("; $Port=").Append (port); + + return result.ToString (); + } + + internal string ToClientString () + { + if (name.Length == 0) + return String.Empty; + + StringBuilder result = new StringBuilder (64); + + if (version > 0) + result.Append ("Version=").Append (version).Append (";"); + + result.Append (name).Append ("=").Append (val); + + if (path != null && path.Length != 0) + result.Append (";Path=").Append (QuotedString (path)); + + if (domain != null && domain.Length != 0) + result.Append (";Domain=").Append (QuotedString (domain)); + + if (port != null && port.Length != 0) + result.Append (";Port=").Append (port); + + return result.ToString (); + } + + // See par 3.6 of RFC 2616 + string QuotedString (string value) + { + if (version == 0 || IsToken (value)) + return value; + else + return "\"" + value.Replace("\"", "\\\"") + "\""; + } + + bool IsToken (string value) + { + int len = value.Length; + for (int i = 0; i < len; i++) { + char c = value [i]; + if (c < 0x20 || c >= 0x7f || tspecials.IndexOf (c) != -1) + return false; + } + return true; + } + } +} diff --git a/websocket-sharp/Net/CookieCollection.cs b/websocket-sharp/Net/CookieCollection.cs new file mode 100644 index 00000000..cf2a501b --- /dev/null +++ b/websocket-sharp/Net/CookieCollection.cs @@ -0,0 +1,176 @@ +// +// CookieCollection.cs +// Copied from System.Net.CookieCollection +// +// Authors: +// Lawrence Pit (loz@cable.a2000.nl) +// Gonzalo Paniagua Javier (gonzalo@ximian.com) +// Sebastien Pouliot +// +// Copyright (C) 2004,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 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. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Runtime.Serialization; + +namespace WebSocketSharp.Net { + + [Serializable] + public class CookieCollection : ICollection, IEnumerable + { + // not 100% identical to MS implementation + sealed class CookieCollectionComparer : IComparer + { + public int Compare (Cookie x, Cookie y) + { + if (x == null || y == null) + return 0; + + int c1 = x.Name.Length + x.Value.Length; + int c2 = y.Name.Length + y.Value.Length; + + return (c1 - c2); + } + } + + static CookieCollectionComparer Comparer = new CookieCollectionComparer (); + + List list = new List (); + + internal IList List { + get { return list; } + } + // ICollection + public int Count { + get { return list.Count; } + } + + public bool IsSynchronized { + get { return false; } + } + + public Object SyncRoot { + get { return this; } + } + + public void CopyTo (Array array, int index) + { + (list as IList).CopyTo (array, index); + } + + public void CopyTo (Cookie [] array, int index) + { + list.CopyTo (array, index); + } + + // IEnumerable + public IEnumerator GetEnumerator () + { + return list.GetEnumerator (); + } + + // This + + // LAMESPEC: So how is one supposed to create a writable CookieCollection + // instance?? We simply ignore this property, as this collection is always + // writable. + public bool IsReadOnly { + get { return true; } + } + + public void Add (Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + int pos = SearchCookie (cookie); + if (pos == -1) + list.Add (cookie); + else + list [pos] = cookie; + } + + internal void Sort () + { + if (list.Count > 0) + list.Sort (Comparer); + } + + int SearchCookie (Cookie cookie) + { + string name = cookie.Name; + string domain = cookie.Domain; + string path = cookie.Path; + + for (int i = list.Count - 1; i >= 0; i--) { + Cookie c = list [i]; + if (c.Version != cookie.Version) + continue; + + if (0 != String.Compare (domain, c.Domain, true, CultureInfo.InvariantCulture)) + continue; + + if (0 != String.Compare (name, c.Name, true, CultureInfo.InvariantCulture)) + continue; + + if (0 != String.Compare (path, c.Path, true, CultureInfo.InvariantCulture)) + continue; + + return i; + } + + return -1; + } + + public void Add (CookieCollection cookies) + { + if (cookies == null) + throw new ArgumentNullException ("cookies"); + + foreach (Cookie c in cookies) + Add (c); + } + + public Cookie this [int index] { + get { + if (index < 0 || index >= list.Count) + throw new ArgumentOutOfRangeException ("index"); + + return list [index]; + } + } + + public Cookie this [string name] { + get { + foreach (Cookie c in list) { + if (0 == String.Compare (c.Name, name, true, CultureInfo.InvariantCulture)) + return c; + } + return null; + } + } + } +} diff --git a/websocket-sharp/Net/CookieException.cs b/websocket-sharp/Net/CookieException.cs new file mode 100644 index 00000000..24f39661 --- /dev/null +++ b/websocket-sharp/Net/CookieException.cs @@ -0,0 +1,69 @@ +// +// CookieException.cs +// Copied from System.Net.CookieException +// +// Author: +// Lawrence Pit (loz@cable.a2000.nl) +// +// 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. +// + +using System; +using System.Globalization; +using System.Runtime.Serialization; + +namespace WebSocketSharp.Net { + + [Serializable] + public class CookieException : FormatException, ISerializable + { + // Constructors + public CookieException () + : base () + { + } + + internal CookieException (string msg) + : base (msg) + { + } + + internal CookieException (string msg, Exception e) + : base (msg, e) + { + } + + protected CookieException (SerializationInfo info, StreamingContext context) + : base (info, context) + { + } + + // Methods + void ISerializable.GetObjectData (SerializationInfo info, StreamingContext context) + { + base.GetObjectData (info, context); + } + + public override void GetObjectData (SerializationInfo serializationInfo, StreamingContext streamingContext) + { + base.GetObjectData (serializationInfo, streamingContext); + } + } +} diff --git a/websocket-sharp/Net/EndPointListener.cs b/websocket-sharp/Net/EndPointListener.cs new file mode 100644 index 00000000..7865d016 --- /dev/null +++ b/websocket-sharp/Net/EndPointListener.cs @@ -0,0 +1,404 @@ +// +// EndPointListener.cs +// Copied from System.Net.EndPointListener +// +// Author: +// Gonzalo Paniagua Javier (gonzalo@novell.com) +// +// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) +// Copyright (c) 2012 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. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Threading; + +namespace WebSocketSharp.Net { + + sealed class EndPointListener { + + #region Fields + + List all; // host = '+' + X509Certificate2 cert; + IPEndPoint endpoint; + AsymmetricAlgorithm key; + Dictionary prefixes; + bool secure; + Socket sock; + List unhandled; // host = '*' + Hashtable unregistered; + + #endregion + + #region Constructor + + public EndPointListener (IPAddress addr, int port, bool secure) + { + if (secure) { + this.secure = secure; + LoadCertificateAndKey (addr, port); + } + + endpoint = new IPEndPoint (addr, port); + sock = new Socket (addr.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + sock.Bind (endpoint); + sock.Listen (500); + var args = new SocketAsyncEventArgs (); + args.UserToken = this; + args.Completed += OnAccept; + sock.AcceptAsync (args); + prefixes = new Dictionary (); + unregistered = Hashtable.Synchronized (new Hashtable ()); + } + + #endregion + + #region Private Static Methods + + static void OnAccept (object sender, EventArgs e) + { + SocketAsyncEventArgs args = (SocketAsyncEventArgs) e; + EndPointListener epl = (EndPointListener) args.UserToken; + Socket accepted = null; + if (args.SocketError == SocketError.Success) { + accepted = args.AcceptSocket; + args.AcceptSocket = null; + } + + try { + if (epl.sock != null) + epl.sock.AcceptAsync (args); + } catch { + if (accepted != null) { + try { + accepted.Close (); + } catch {} + accepted = null; + } + } + + if (accepted == null) + return; + + if (epl.secure && (epl.cert == null || epl.key == null)) { + accepted.Close (); + return; + } + HttpConnection conn = new HttpConnection (accepted, epl, epl.secure, epl.cert, epl.key); + epl.unregistered [conn] = conn; + conn.BeginReadRequest (); + } + + #endregion + + #region Private Methods + + void AddSpecial (List coll, ListenerPrefix prefix) + { + if (coll == null) + return; + + foreach (ListenerPrefix p in coll) { + if (p.Path == prefix.Path) // TODO: code + throw new HttpListenerException (400, "Prefix already in use."); + } + coll.Add (prefix); + } + + void CheckIfRemove () + { + if (prefixes.Count > 0) + return; + + var list = unhandled; + if (list != null && list.Count > 0) + return; + + list = all; + if (list != null && list.Count > 0) + return; + + EndPointManager.RemoveEndPoint (this, endpoint); + } + + RSACryptoServiceProvider CreateRSAFromFile (string filename) + { + if (filename == null) + throw new ArgumentNullException ("filename"); + + var rsa = new RSACryptoServiceProvider (); + byte[] pvk = null; + using (FileStream 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; + } + + void LoadCertificateAndKey (IPAddress addr, int port) + { + // Actually load the certificate + try { + string dirname = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData); + string path = Path.Combine (dirname, ".mono"); + path = Path.Combine (path, "httplistener"); + string cert_file = Path.Combine (path, String.Format ("{0}.cer", port)); + string pvk_file = Path.Combine (path, String.Format ("{0}.pvk", port)); + cert = new X509Certificate2 (cert_file); + key = CreateRSAFromFile (pvk_file); + } catch { + // ignore errors + } + } + + HttpListener MatchFromList ( + string host, string path, List list, out ListenerPrefix prefix) + { + prefix = null; + if (list == null) + return null; + + HttpListener best_match = null; + int best_length = -1; + + foreach (ListenerPrefix p in list) { + string 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; + } + + bool RemoveSpecial (List coll, ListenerPrefix prefix) + { + if (coll == null) + return false; + + int c = coll.Count; + for (int i = 0; i < c; i++) { + ListenerPrefix p = coll [i]; + if (p.Path == prefix.Path) { + coll.RemoveAt (i); + return true; + } + } + return false; + } + + HttpListener SearchListener (Uri uri, out ListenerPrefix prefix) + { + prefix = null; + if (uri == null) + return null; + + string host = uri.Host; + int port = uri.Port; + string path = HttpUtility.UrlDecode (uri.AbsolutePath); + string path_slash = path [path.Length - 1] == '/' ? path : path + "/"; + + HttpListener best_match = null; + int best_length = -1; + + if (host != null && host != "") { + var p_ro = prefixes; + foreach (ListenerPrefix p in p_ro.Keys) { + string 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 = p_ro [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 Method + + internal void RemoveConnection (HttpConnection conn) + { + unregistered.Remove (conn); + } + + #endregion + + #region Public Methods + + public void AddPrefix (ListenerPrefix prefix, HttpListener listener) + { + List current; + List 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) + { + HttpListenerRequest req = context.Request; + ListenerPrefix prefix; + HttpListener listener = SearchListener (req.Url, out prefix); + if (listener == null) + return false; + + context.Listener = listener; + context.Connection.Prefix = prefix; + return true; + } + + public void Close () + { + sock.Close (); + lock (unregistered.SyncRoot) { + foreach (HttpConnection c in unregistered.Keys) + c.Close (true); + unregistered.Clear (); + } + } + + public void RemovePrefix (ListenerPrefix prefix, HttpListener listener) + { + List current; + List 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 + } +} diff --git a/websocket-sharp/Net/EndPointManager.cs b/websocket-sharp/Net/EndPointManager.cs new file mode 100644 index 00000000..af62fd56 --- /dev/null +++ b/websocket-sharp/Net/EndPointManager.cs @@ -0,0 +1,148 @@ +// +// EndPointManager.cs +// Copied from System.Net.EndPointManager +// +// Author: +// Gonzalo Paniagua Javier (gonzalo@ximian.com) +// +// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) +// Copyright (c) 2012 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. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Net; + +namespace WebSocketSharp.Net { + + sealed class EndPointManager { + + static Dictionary> ip_to_endpoints = new Dictionary> (); + + private EndPointManager () + { + } + + static void AddPrefixInternal (string p, HttpListener listener) + { + ListenerPrefix lp = new ListenerPrefix (p); + if (lp.Path.IndexOf ('%') != -1) + throw new HttpListenerException (400, "Invalid path."); + + if (lp.Path.IndexOf ("//", StringComparison.Ordinal) != -1) // TODO: Code? + throw new HttpListenerException (400, "Invalid path."); + + // Always listens on all the interfaces, no matter the host name/ip used. + EndPointListener epl = GetEPListener (IPAddress.Any, lp.Port, listener, lp.Secure); + epl.AddPrefix (lp, listener); + } + + static EndPointListener GetEPListener (IPAddress addr, int port, HttpListener listener, bool secure) + { + Dictionary p = null; + if (ip_to_endpoints.ContainsKey (addr)) { + p = ip_to_endpoints [addr]; + } else { + p = new Dictionary (); + ip_to_endpoints [addr] = p; + } + + EndPointListener epl = null; + if (p.ContainsKey (port)) { + epl = p [port]; + } else { + epl = new EndPointListener (addr, port, secure); + p [port] = epl; + } + + return epl; + } + + static void RemovePrefixInternal (string prefix, HttpListener listener) + { + ListenerPrefix lp = new ListenerPrefix (prefix); + if (lp.Path.IndexOf ('%') != -1) + return; + + if (lp.Path.IndexOf ("//", StringComparison.Ordinal) != -1) + return; + + EndPointListener epl = GetEPListener (IPAddress.Any, lp.Port, listener, lp.Secure); + epl.RemovePrefix (lp, listener); + } + + public static void AddListener (HttpListener listener) + { + List added = new List (); + try { + lock (((ICollection)ip_to_endpoints).SyncRoot) { + foreach (string prefix in listener.Prefixes) { + AddPrefixInternal (prefix, listener); + added.Add (prefix); + } + } + } catch { + foreach (string prefix in added) { + RemovePrefix (prefix, listener); + } + throw; + } + } + + public static void AddPrefix (string prefix, HttpListener listener) + { + lock (((ICollection)ip_to_endpoints).SyncRoot) { + AddPrefixInternal (prefix, listener); + } + } + + public static void RemoveEndPoint (EndPointListener epl, IPEndPoint ep) + { + lock (((ICollection)ip_to_endpoints).SyncRoot) { + Dictionary p = null; + p = ip_to_endpoints [ep.Address]; + p.Remove (ep.Port); + if (p.Count == 0) { + ip_to_endpoints.Remove (ep.Address); + } + epl.Close (); + } + } + + public static void RemoveListener (HttpListener listener) + { + lock (((ICollection)ip_to_endpoints).SyncRoot) { + foreach (string prefix in listener.Prefixes) { + RemovePrefixInternal (prefix, listener); + } + } + } + + public static void RemovePrefix (string prefix, HttpListener listener) + { + lock (((ICollection)ip_to_endpoints).SyncRoot) { + RemovePrefixInternal (prefix, listener); + } + } + } +} diff --git a/websocket-sharp/Net/HttpConnection.cs b/websocket-sharp/Net/HttpConnection.cs new file mode 100644 index 00000000..44f56449 --- /dev/null +++ b/websocket-sharp/Net/HttpConnection.cs @@ -0,0 +1,522 @@ +// +// HttpConnection.cs +// Copied from System.Net.HttpConnection +// +// Author: +// Gonzalo Paniagua Javier (gonzalo@novell.com) +// +// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) +// Copyright (c) 2012 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. +// + +using System; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Reflection; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; + +namespace WebSocketSharp.Net { + + sealed class HttpConnection { + + #region Enums + + enum InputState { + RequestLine, + Headers + } + + enum LineState { + None, + CR, + LF + } + + #endregion + + #region Private Const Field + + const int BufferSize = 8192; + + #endregion + + #region Private Static Field + + static AsyncCallback onread_cb = new AsyncCallback (OnRead); + + #endregion + + #region Private Fields + + byte [] buffer; + bool chunked; + HttpListenerContext context; + bool context_bound; + StringBuilder current_line; + EndPointListener epl; + InputState input_state; + RequestStream i_stream; + AsymmetricAlgorithm key; + HttpListener last_listener; + LineState line_state; +// IPEndPoint local_ep; // never used + MemoryStream ms; + ResponseStream o_stream; + int position; + ListenerPrefix prefix; + int reuses; + bool secure; + Socket sock; + Stream stream; + int s_timeout; + Timer timer; + + #endregion + + #region Constructor + + public HttpConnection ( + Socket sock, + EndPointListener epl, + bool secure, + X509Certificate2 cert, + AsymmetricAlgorithm key + ) + { + this.sock = sock; + this.epl = epl; + this.secure = secure; + this.key = key; +// if (secure == false) { +// stream = new NetworkStream (sock, false); +// } else { +// var ssl_stream = new SslServerStream (new NetworkStream (sock, false), cert, false, false); +// ssl_stream.PrivateKeyCertSelectionDelegate += OnPVKSelection; +// stream = ssl_stream; +// } + var net_stream = new NetworkStream (sock, false); + if (!secure) { + stream = net_stream; + } else { + var ssl_stream = new SslStream(net_stream); + ssl_stream.AuthenticateAsServer(cert); + stream = ssl_stream; + } + timer = new Timer (OnTimeout, null, Timeout.Infinite, Timeout.Infinite); + Init (); + } + + #endregion + + #region Properties + + public bool IsClosed { + get { return (sock == null); } + } + + public bool IsSecure { + get { return secure; } + } + + public IPEndPoint LocalEndPoint { + get { return (IPEndPoint) sock.LocalEndPoint; } + } + + public ListenerPrefix Prefix { + get { return prefix; } + set { prefix = value; } + } + + public IPEndPoint RemoteEndPoint { + get { return (IPEndPoint) sock.RemoteEndPoint; } + } + + public int Reuses { + get { return reuses; } + } + + public Stream Stream { + get { return stream; } + } + + #endregion + + #region Private Methods + + void CloseSocket () + { + if (sock == null) + return; + + try { + sock.Close (); + } catch { + } finally { + sock = null; + } + RemoveConnection (); + } + + void Init () + { + context_bound = false; + i_stream = null; + o_stream = null; + prefix = null; + chunked = false; + ms = new MemoryStream (); + position = 0; + input_state = InputState.RequestLine; + line_state = LineState.None; + context = new HttpListenerContext (this); + s_timeout = 90000; // 90k ms for first request, 15k ms from then on + } + + AsymmetricAlgorithm OnPVKSelection (X509Certificate certificate, string targetHost) + { + return key; + } + + static void OnRead (IAsyncResult ares) + { + HttpConnection cnc = (HttpConnection) ares.AsyncState; + cnc.OnReadInternal (ares); + } + + void OnReadInternal (IAsyncResult ares) + { + timer.Change (Timeout.Infinite, Timeout.Infinite); + int nread = -1; + try { + nread = stream.EndRead (ares); + ms.Write (buffer, 0, nread); + if (ms.Length > 32768) { + SendError ("Bad request", 400); + Close (true); + return; + } + } catch { + if (ms != null && ms.Length > 0) + SendError (); + if (sock != null) { + CloseSocket (); + Unbind (); + } + return; + } + + if (nread == 0) { + //if (ms.Length > 0) + // SendError (); // Why bother? + CloseSocket (); + Unbind (); + return; + } + + if (ProcessInput (ms)) { + if (!context.HaveError) + context.Request.FinishInitialization (); + + if (context.HaveError) { + SendError (); + Close (true); + return; + } + + if (!epl.BindContext (context)) { + SendError ("Invalid host", 400); + Close (true); + return; + } + HttpListener listener = context.Listener; + if (last_listener != listener) { + RemoveConnection (); + listener.AddConnection (this); + last_listener = listener; + } + + context_bound = true; + listener.RegisterContext (context); + return; + } + stream.BeginRead (buffer, 0, BufferSize, onread_cb, this); + } + + void OnTimeout (object unused) + { + CloseSocket (); + Unbind (); + } + + // true -> done processing + // false -> need more input + bool ProcessInput (MemoryStream ms) + { + byte [] buffer = ms.GetBuffer (); + int len = (int) ms.Length; + int used = 0; + string line; + + try { + line = ReadLine (buffer, position, len - position, ref used); + position += used; + } catch { + context.ErrorMessage = "Bad request"; + context.ErrorStatus = 400; + return true; + } + + do { + if (line == null) + break; + if (line == "") { + if (input_state == InputState.RequestLine) + continue; + current_line = null; + ms = null; + return true; + } + + if (input_state == InputState.RequestLine) { + context.Request.SetRequestLine (line); + input_state = InputState.Headers; + } else { + try { + context.Request.AddHeader (line); + } catch (Exception e) { + context.ErrorMessage = e.Message; + context.ErrorStatus = 400; + return true; + } + } + + if (context.HaveError) + return true; + + if (position >= len) + break; + try { + line = ReadLine (buffer, position, len - position, ref used); + position += used; + } catch { + context.ErrorMessage = "Bad request"; + context.ErrorStatus = 400; + return true; + } + } while (line != null); + + if (used == len) { + ms.SetLength (0); + position = 0; + } + return false; + } + + string ReadLine (byte [] buffer, int offset, int len, ref int used) + { + if (current_line == null) + current_line = new StringBuilder (); + int last = offset + len; + used = 0; + for (int i = offset; i < last && line_state != LineState.LF; i++) { + used++; + byte b = buffer [i]; + if (b == 13) { + line_state = LineState.CR; + } else if (b == 10) { + line_state = LineState.LF; + } else { + current_line.Append ((char) b); + } + } + + string result = null; + if (line_state == LineState.LF) { + line_state = LineState.None; + result = current_line.ToString (); + current_line.Length = 0; + } + + return result; + } + + void RemoveConnection () + { + if (last_listener == null) + epl.RemoveConnection (this); + else + last_listener.RemoveConnection (this); + } + + void Unbind () + { + if (context_bound) { + epl.UnbindContext (context); + context_bound = false; + } + } + + #endregion + + #region Internal Method + + internal void Close (bool force_close) + { + if (sock != null) { + Stream st = GetResponseStream (); + st.Close (); + o_stream = null; + } + + if (sock != null) { + force_close |= !context.Request.KeepAlive; + if (!force_close) + force_close = (context.Response.Headers ["connection"] == "close"); + /* + if (!force_close) { +// bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 || +// status_code == 413 || status_code == 414 || status_code == 500 || +// status_code == 503); + + force_close |= (context.Request.ProtocolVersion <= HttpVersion.Version10); + } + */ + + if (!force_close && context.Request.FlushInput ()) { + if (chunked && context.Response.ForceCloseChunked == false) { + // Don't close. Keep working. + reuses++; + Unbind (); + Init (); + BeginReadRequest (); + return; + } + + reuses++; + Unbind (); + Init (); + BeginReadRequest (); + return; + } + + Socket s = sock; + sock = null; + try { + if (s != null) + s.Shutdown (SocketShutdown.Both); + } catch { + } finally { + if (s != null) + s.Close (); + } + Unbind (); + RemoveConnection (); + return; + } + } + + #endregion + + #region Public Methods + + public void BeginReadRequest () + { + if (buffer == null) + buffer = new byte [BufferSize]; + try { + if (reuses == 1) + s_timeout = 15000; + timer.Change (s_timeout, Timeout.Infinite); + stream.BeginRead (buffer, 0, BufferSize, onread_cb, this); + } catch { + timer.Change (Timeout.Infinite, Timeout.Infinite); + CloseSocket (); + Unbind (); + } + } + + public void Close () + { + Close (false); + } + + public RequestStream GetRequestStream (bool chunked, long contentlength) + { + if (i_stream == null) { + byte [] buffer = ms.GetBuffer (); + int length = (int) ms.Length; + ms = null; + if (chunked) { + this.chunked = true; + context.Response.SendChunked = true; + i_stream = new ChunkedInputStream (context, stream, buffer, position, length - position); + } else { + i_stream = new RequestStream (stream, buffer, position, length - position, contentlength); + } + } + return i_stream; + } + + public ResponseStream GetResponseStream () + { + // TODO: can we get this stream before reading the input? + if (o_stream == null) { + HttpListener listener = context.Listener; + bool ign = (listener == null) ? true : listener.IgnoreWriteExceptions; + o_stream = new ResponseStream (stream, context.Response, ign); + } + return o_stream; + } + + public void SendError () + { + SendError (context.ErrorMessage, context.ErrorStatus); + } + + public void SendError (string msg, int status) + { + try { + HttpListenerResponse response = context.Response; + response.StatusCode = status; + response.ContentType = "text/html"; + string description = HttpListenerResponse.GetStatusDescription (status); + string str; + if (msg != null) + str = String.Format ("

{0} ({1})

", description, msg); + else + str = String.Format ("

{0}

", description); + + byte [] error = context.Response.ContentEncoding.GetBytes (str); + response.Close (error, false); + } catch { + // response was already closed + } + } + + #endregion + } +} diff --git a/websocket-sharp/Net/HttpListener.cs b/websocket-sharp/Net/HttpListener.cs new file mode 100644 index 00000000..d568f3a5 --- /dev/null +++ b/websocket-sharp/Net/HttpListener.cs @@ -0,0 +1,372 @@ +// +// HttpListener.cs +// Copied from System.Net.HttpListener +// +// Author: +// Gonzalo Paniagua Javier (gonzalo@novell.com) +// +// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) +// Copyright (c) 2012 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. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Net; +using System.Threading; + +// TODO: logging +namespace WebSocketSharp.Net { + + public sealed class HttpListener : IDisposable { + + #region Fields + + AuthenticationSchemes auth_schemes; + AuthenticationSchemeSelector auth_selector; + Dictionary connections; + List ctx_queue; + bool disposed; + bool ignore_write_exceptions; + bool listening; + HttpListenerPrefixCollection prefixes; + string realm; + Dictionary registry; + bool unsafe_ntlm_auth; + List wait_queue; + + #endregion + + #region Constructor + + 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 Properties + + // TODO: Digest, NTLM and Negotiate require ControlPrincipal + public AuthenticationSchemes AuthenticationSchemes { + get { return auth_schemes; } + set { + CheckDisposed (); + auth_schemes = value; + } + } + + public AuthenticationSchemeSelector AuthenticationSchemeSelectorDelegate { + get { return auth_selector; } + set { + CheckDisposed (); + auth_selector = value; + } + } + + public bool IgnoreWriteExceptions { + get { return ignore_write_exceptions; } + set { + CheckDisposed (); + ignore_write_exceptions = value; + } + } + + public bool IsListening { + get { return listening; } + } + + public static bool IsSupported { + get { return true; } + } + + public HttpListenerPrefixCollection Prefixes { + get { + CheckDisposed (); + return prefixes; + } + } + + // TODO: Use this + public string Realm { + get { return realm; } + set { + CheckDisposed (); + realm = value; + } + } + + // TODO: Support for NTLM needs some loving. + public bool UnsafeConnectionNtlmAuthentication { + get { return unsafe_ntlm_auth; } + set { + CheckDisposed (); + unsafe_ntlm_auth = value; + } + } + + #endregion + + #region Private Methods + + void Cleanup (bool close_existing) + { + lock (((ICollection)registry).SyncRoot) { + if (close_existing) { + // 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); + } + + lock (((ICollection)connections).SyncRoot) { + 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); + } + + lock (((ICollection)ctx_queue).SyncRoot) { + var ctxs = ctx_queue.ToArray (); + ctx_queue.Clear (); + for (int i = ctxs.Length - 1; i >= 0; i--) + ctxs [i].Connection.Close (true); + } + + lock (((ICollection)wait_queue).SyncRoot) { + Exception exc = new ObjectDisposedException ("listener"); + foreach (ListenerAsyncResult ares in wait_queue) { + ares.Complete (exc); + } + wait_queue.Clear (); + } + } + } + + void Close (bool force) + { + CheckDisposed (); + 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 IDisposable.Dispose () + { + if (disposed) + return; + + Close (true); //TODO: Should we force here or not? + disposed = true; + } + + #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 + + public void Abort () + { + if (disposed) + return; + + if (!listening) { + return; + } + + Close (true); + } + + 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; + } + + public void Close () + { + if (disposed) + return; + + if (!listening) { + disposed = true; + return; + } + + Close (true); + disposed = true; + } + + 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 ArgumentException ("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. + } + + 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); + } + + public void Start () + { + CheckDisposed (); + if (listening) + return; + + EndPointManager.AddListener (this); + listening = true; + } + + public void Stop () + { + CheckDisposed (); + listening = false; + Close (false); + } + + #endregion + } +} diff --git a/websocket-sharp/Net/HttpListenerContext.cs b/websocket-sharp/Net/HttpListenerContext.cs new file mode 100644 index 00000000..f8258323 --- /dev/null +++ b/websocket-sharp/Net/HttpListenerContext.cs @@ -0,0 +1,179 @@ +// +// HttpListenerContext.cs +// Copied from System.Net.HttpListenerContext +// +// Author: +// Gonzalo Paniagua Javier (gonzalo@novell.com) +// +// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) +// Copyright (c) 2012 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. +// + +using System; +using System.Collections.Specialized; +using System.IO; +using System.Net; +using System.Security.Principal; +using System.Text; + +namespace WebSocketSharp.Net { + + public sealed class HttpListenerContext { + + #region Private Fields + + HttpConnection cnc; + string error; + int err_status; + HttpListenerRequest request; + HttpListenerResponse response; + IPrincipal user; + + #endregion + + #region Internal Fields + + internal HttpListener Listener; + + #endregion + + #region Constructor + + internal HttpListenerContext (HttpConnection cnc) + { + this.cnc = cnc; + err_status = 400; + request = new HttpListenerRequest (this); + response = new HttpListenerResponse (this); + } + + #endregion + + #region Internal Properties + + internal HttpConnection Connection { + get { return cnc; } + } + + internal string ErrorMessage { + get { return error; } + set { error = value; } + } + + internal int ErrorStatus { + get { return err_status; } + set { err_status = value; } + } + + internal bool HaveError { + get { return (error != null); } + } + + #endregion + + #region Public Properties + + public HttpListenerRequest Request { + get { return request; } + } + + public HttpListenerResponse Response { + get { return response; } + } + + public IPrincipal User { + get { return user; } + } + + #endregion + + #region Internal Methods + + internal void ParseAuthentication (AuthenticationSchemes expectedSchemes) + { + if (expectedSchemes == AuthenticationSchemes.Anonymous) + return; + + // TODO: Handle NTLM/Digest modes + string header = request.Headers ["Authorization"]; + if (header == null || header.Length < 2) + return; + + string [] authenticationData = header.Split (new char [] {' '}, 2); + if (string.Compare (authenticationData [0], "basic", true) == 0) { + user = ParseBasicAuthentication (authenticationData [1]); + } + // TODO: throw if malformed -> 400 bad request + } + + internal IPrincipal ParseBasicAuthentication (string authData) + { + try { + // Basic AUTH Data is a formatted Base64 String + //string domain = null; + string user = null; + string password = null; + int pos = -1; + string authString = Encoding.Default.GetString (Convert.FromBase64String (authData)); + + // The format is DOMAIN\username:password + // Domain is optional + + pos = authString.IndexOf (':'); + + // parse the password off the end + password = authString.Substring (pos+1); + + // discard the password + authString = authString.Substring (0, pos); + + // check if there is a domain + pos = authString.IndexOf ('\\'); + + if (pos > 0) { + //domain = authString.Substring (0, pos); + user = authString.Substring (pos); + } else { + user = authString; + } + + HttpListenerBasicIdentity identity = new HttpListenerBasicIdentity (user, password); + // TODO: What are the roles MS sets + return new GenericPrincipal (identity, new string [0]); + } catch (Exception) { + // Invalid auth data is swallowed silently + return null; + } + } + + #endregion + + #region Public Method + + public HttpListenerWebSocketContext AcceptWebSocket () + { + return new HttpListenerWebSocketContext (this); + } + + #endregion + } +} diff --git a/websocket-sharp/Net/HttpListenerException.cs b/websocket-sharp/Net/HttpListenerException.cs new file mode 100644 index 00000000..b8842808 --- /dev/null +++ b/websocket-sharp/Net/HttpListenerException.cs @@ -0,0 +1,59 @@ +// +// HttpListenerException.cs +// Copied from System.Net.HttpListenerException +// +// 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. +// + +using System; +using System.ComponentModel; +using System.Runtime.Serialization; + +namespace WebSocketSharp.Net { + + [Serializable] + public class HttpListenerException : Win32Exception + { + public HttpListenerException () + { + } + + public HttpListenerException (int errorCode) : base (errorCode) + { + } + + public HttpListenerException (int errorCode, string message) : base (errorCode, message) + { + } + + protected HttpListenerException (SerializationInfo serializationInfo, StreamingContext streamingContext) : base (serializationInfo, streamingContext) + { + } + + public override int ErrorCode { + get { return base.ErrorCode; } + } + } +} diff --git a/websocket-sharp/Net/HttpListenerPrefixCollection.cs b/websocket-sharp/Net/HttpListenerPrefixCollection.cs new file mode 100644 index 00000000..904c8f46 --- /dev/null +++ b/websocket-sharp/Net/HttpListenerPrefixCollection.cs @@ -0,0 +1,127 @@ +// +// HttpListenerPrefixCollection.cs +// Copied from System.Net.HttpListenerPrefixCollection +// +// Author: +// Gonzalo Paniagua Javier (gonzalo@novell.com) +// +// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) +// Copyright (c) 2012 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. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Net; + +namespace WebSocketSharp.Net { + + public class HttpListenerPrefixCollection : ICollection, IEnumerable, IEnumerable + { + HttpListener listener; + List prefixes; + + private HttpListenerPrefixCollection () + { + prefixes = new List (); + } + + internal HttpListenerPrefixCollection (HttpListener listener) + : this () + { + this.listener = listener; + } + + public int Count { + get { return prefixes.Count; } + } + + public bool IsReadOnly { + get { return false; } + } + + public bool IsSynchronized { + get { return false; } + } + + public void Add (string uriPrefix) + { + listener.CheckDisposed (); + ListenerPrefix.CheckUri (uriPrefix); + if (prefixes.Contains (uriPrefix)) + return; + + prefixes.Add (uriPrefix); + if (listener.IsListening) + EndPointManager.AddPrefix (uriPrefix, listener); + } + + public void Clear () + { + listener.CheckDisposed (); + prefixes.Clear (); + if (listener.IsListening) + EndPointManager.RemoveListener (listener); + } + + public bool Contains (string uriPrefix) + { + listener.CheckDisposed (); + return prefixes.Contains (uriPrefix); + } + + public void CopyTo (string [] array, int offset) + { + listener.CheckDisposed (); + prefixes.CopyTo (array, offset); + } + + public void CopyTo (Array array, int offset) + { + listener.CheckDisposed (); + ((ICollection) prefixes).CopyTo (array, offset); + } + + public IEnumerator GetEnumerator () + { + return prefixes.GetEnumerator (); + } + + IEnumerator IEnumerable.GetEnumerator () + { + return prefixes.GetEnumerator (); + } + + public bool Remove (string uriPrefix) + { + listener.CheckDisposed (); + if (uriPrefix == null) + throw new ArgumentNullException ("uriPrefix"); + + bool result = prefixes.Remove (uriPrefix); + if (result && listener.IsListening) + EndPointManager.RemovePrefix (uriPrefix, listener); + + return result; + } + } +} diff --git a/websocket-sharp/Net/HttpListenerRequest.cs b/websocket-sharp/Net/HttpListenerRequest.cs new file mode 100644 index 00000000..1b25efa2 --- /dev/null +++ b/websocket-sharp/Net/HttpListenerRequest.cs @@ -0,0 +1,546 @@ +// +// HttpListenerRequest.cs +// Copied from System.Net.HttpListenerRequest +// +// Author: +// Gonzalo Paniagua Javier (gonzalo@novell.com) +// +// Copyright (c) 2005 Novell, Inc. (http://www.novell.com) +// Copyright (c) 2012 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. +// + +using System; +using System.Collections; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Text; + +namespace WebSocketSharp.Net { + + public sealed class HttpListenerRequest { + + #region Private Static Fields + + static char [] separators = new char [] { ' ' }; + static byte [] _100continue = Encoding.ASCII.GetBytes ("HTTP/1.1 100 Continue\r\n\r\n"); + + #endregion + + #region Private Fields + + string [] accept_types; +// int client_cert_error; + bool cl_set; + Encoding content_encoding; + long content_length; + HttpListenerContext context; + CookieCollection cookies; + WebHeaderCollection headers; + Stream input_stream; + bool is_chunked; + bool ka_set; + bool keep_alive; + string method; +// bool no_get_certificate; + Version version; + NameValueCollection query_string; // check if null is ok, check if read-only, check case-sensitiveness + string raw_url; + Uri referrer; + Uri url; + string [] user_languages; + + #endregion + + #region Constructor + + internal HttpListenerRequest (HttpListenerContext context) + { + this.context = context; + headers = new WebHeaderCollection (); + version = HttpVersion.Version10; + } + + #endregion + + #region Properties + + public string [] AcceptTypes { + get { return accept_types; } + } + + // TODO: Always returns 0 + public int ClientCertificateError { + get { +/* + if (no_get_certificate) + throw new InvalidOperationException ( + "Call GetClientCertificate() before calling this method."); + return client_cert_error; +*/ + return 0; + } + } + + public Encoding ContentEncoding { + get { + if (content_encoding == null) + content_encoding = Encoding.Default; + return content_encoding; + } + } + + public long ContentLength64 { + get { return content_length; } + } + + public string ContentType { + get { return headers ["content-type"]; } + } + + public CookieCollection Cookies { + get { + // TODO: check if the collection is read-only + if (cookies == null) + cookies = new CookieCollection (); + return cookies; + } + } + + public bool HasEntityBody { + get { return (content_length > 0 || is_chunked); } + } + + public NameValueCollection Headers { + get { return headers; } + } + + public string HttpMethod { + get { return method; } + } + + public Stream InputStream { + get { + if (input_stream == null) { + if (is_chunked || content_length > 0) + input_stream = context.Connection.GetRequestStream (is_chunked, content_length); + else + input_stream = Stream.Null; + } + + return input_stream; + } + } + + // TODO: Always returns false + public bool IsAuthenticated { + get { return false; } + } + + public bool IsLocal { + get { return IPAddress.IsLoopback (RemoteEndPoint.Address); } + } + + public bool IsSecureConnection { + get { return context.Connection.IsSecure; } + } + + public bool IsWebSocketRequest { + get { + if (method != "GET") + return false; + + if (version != HttpVersion.Version11) + return false; + + if (!headers.Exists("Upgrade", "websocket")) + return false; + + if (!headers.Exists("Connection", "Upgrade")) + return false; + + if (!headers.Exists("Host")) + return false; + + if (!headers.Exists("Sec-WebSocket-Key")) + return false; + + if (!headers.Exists("Sec-WebSocket-Version")) + return false; + + return true; + } + } + + public bool KeepAlive { + get { + if (ka_set) + return keep_alive; + + ka_set = true; + // 1. Connection header + // 2. Protocol (1.1 == keep-alive by default) + // 3. Keep-Alive header + string cnc = headers ["Connection"]; + if (!String.IsNullOrEmpty (cnc)) { + keep_alive = (0 == String.Compare (cnc, "keep-alive", StringComparison.OrdinalIgnoreCase)); + } else if (version == HttpVersion.Version11) { + keep_alive = true; + } else { + cnc = headers ["keep-alive"]; + if (!String.IsNullOrEmpty (cnc)) + keep_alive = (0 != String.Compare (cnc, "closed", StringComparison.OrdinalIgnoreCase)); + } + return keep_alive; + } + } + + public IPEndPoint LocalEndPoint { + get { return context.Connection.LocalEndPoint; } + } + + public Version ProtocolVersion { + get { return version; } + } + + public NameValueCollection QueryString { + get { return query_string; } + } + + public string RawUrl { + get { return raw_url; } + } + + public IPEndPoint RemoteEndPoint { + get { return context.Connection.RemoteEndPoint; } + } + + // TODO: Always returns Guid.Empty + public Guid RequestTraceIdentifier { + get { return Guid.Empty; } + } + + public Uri Url { + get { return url; } + } + + public Uri UrlReferrer { + get { return referrer; } + } + + public string UserAgent { + get { return headers ["user-agent"]; } + } + + public string UserHostAddress { + get { return LocalEndPoint.ToString (); } + } + + public string UserHostName { + get { return headers ["host"]; } + } + + public string [] UserLanguages { + get { return user_languages; } + } + + #endregion + + #region Private Methods + + void CreateQueryString (string query) + { + if (query == null || query.Length == 0) { + query_string = new NameValueCollection (1); + return; + } + + query_string = new NameValueCollection (); + if (query [0] == '?') + query = query.Substring (1); + string [] components = query.Split ('&'); + foreach (string kv in components) { + int pos = kv.IndexOf ('='); + if (pos == -1) { + query_string.Add (null, HttpUtility.UrlDecode (kv)); + } else { + string key = HttpUtility.UrlDecode (kv.Substring (0, pos)); + string val = HttpUtility.UrlDecode (kv.Substring (pos + 1)); + + query_string.Add (key, val); + } + } + } + + #endregion + + #region Internal Methods + + internal void AddHeader (string header) + { + int colon = header.IndexOf (':'); + if (colon == -1 || colon == 0) { + context.ErrorMessage = "Bad Request"; + context.ErrorStatus = 400; + return; + } + + string name = header.Substring (0, colon).Trim (); + string val = header.Substring (colon + 1).Trim (); + string lower = name.ToLower (CultureInfo.InvariantCulture); + headers.SetInternal (name, val); + switch (lower) { + case "accept-language": + user_languages = val.Split (','); // yes, only split with a ',' + break; + case "accept": + accept_types = val.Split (','); // yes, only split with a ',' + break; + case "content-length": + try { + //TODO: max. content_length? + content_length = Int64.Parse (val.Trim ()); + if (content_length < 0) + context.ErrorMessage = "Invalid Content-Length."; + cl_set = true; + } catch { + context.ErrorMessage = "Invalid Content-Length."; + } + + break; + case "referer": + try { + referrer = new Uri (val); + } catch { + referrer = new Uri ("http://someone.is.screwing.with.the.headers.com/"); + } + break; + case "cookie": + if (cookies == null) + cookies = new CookieCollection(); + + string[] cookieStrings = val.Split(new char[] {',', ';'}); + Cookie current = null; + int version = 0; + foreach (string cookieString in cookieStrings) { + string str = cookieString.Trim (); + if (str.Length == 0) + continue; + if (str.StartsWith ("$Version")) { + version = Int32.Parse (Unquote (str.Substring (str.IndexOf ('=') + 1))); + } else if (str.StartsWith ("$Path")) { + if (current != null) + current.Path = str.Substring (str.IndexOf ('=') + 1).Trim (); + } else if (str.StartsWith ("$Domain")) { + if (current != null) + current.Domain = str.Substring (str.IndexOf ('=') + 1).Trim (); + } else if (str.StartsWith ("$Port")) { + if (current != null) + current.Port = str.Substring (str.IndexOf ('=') + 1).Trim (); + } else { + if (current != null) { + cookies.Add (current); + } + current = new Cookie (); + int idx = str.IndexOf ('='); + if (idx > 0) { + current.Name = str.Substring (0, idx).Trim (); + current.Value = str.Substring (idx + 1).Trim (); + } else { + current.Name = str.Trim (); + current.Value = String.Empty; + } + current.Version = version; + } + } + if (current != null) { + cookies.Add (current); + } + break; + } + } + + internal void FinishInitialization () + { + string host = UserHostName; + if (version > HttpVersion.Version10 && (host == null || host.Length == 0)) { + context.ErrorMessage = "Invalid host name"; + return; + } + + string path; + Uri raw_uri = null; + if (raw_url.MaybeUri () && Uri.TryCreate (raw_url, UriKind.Absolute, out raw_uri)) + path = raw_uri.PathAndQuery; + else + path = HttpUtility.UrlDecode (raw_url); + + if ((host == null || host.Length == 0)) + host = UserHostAddress; + + if (raw_uri != null) + host = raw_uri.Host; + + int colon = host.IndexOf (':'); + if (colon >= 0) + host = host.Substring (0, colon); + + string base_uri = String.Format ("{0}://{1}:{2}", + (IsSecureConnection) ? "https" : "http", + host, + LocalEndPoint.Port); + + if (!Uri.TryCreate (base_uri + path, UriKind.Absolute, out url)){ + context.ErrorMessage = "Invalid url: " + base_uri + path; + return; + } + + CreateQueryString (url.Query); + + if (version >= HttpVersion.Version11) { + string t_encoding = Headers ["Transfer-Encoding"]; + is_chunked = (t_encoding != null && String.Compare (t_encoding, "chunked", StringComparison.OrdinalIgnoreCase) == 0); + // 'identity' is not valid! + if (t_encoding != null && !is_chunked) { + context.Connection.SendError (null, 501); + return; + } + } + + if (!is_chunked && !cl_set) { + if (String.Compare (method, "POST", StringComparison.OrdinalIgnoreCase) == 0 || + String.Compare (method, "PUT", StringComparison.OrdinalIgnoreCase) == 0) { + context.Connection.SendError (null, 411); + return; + } + } + + if (String.Compare (Headers ["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0) { + ResponseStream 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; + + int length = 2048; + if (content_length > 0) + length = (int) Math.Min (content_length, (long) length); + + byte [] bytes = new byte [length]; + while (true) { + // TODO: test if MS has a timeout when doing this + try { + IAsyncResult ares = InputStream.BeginRead (bytes, 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 req) + { + string [] parts = req.Split (separators, 3); + if (parts.Length != 3) { + context.ErrorMessage = "Invalid request line (parts)."; + return; + } + + method = parts [0]; + foreach (char c in method){ + int ic = (int) c; + + if ((ic >= 'A' && ic <= 'Z') || + (ic > 32 && c < 127 && c != '(' && c != ')' && c != '<' && + c != '<' && c != '>' && c != '@' && c != ',' && c != ';' && + c != ':' && c != '\\' && c != '"' && c != '/' && c != '[' && + c != ']' && c != '?' && c != '=' && c != '{' && c != '}')) + continue; + + context.ErrorMessage = "(Invalid verb)"; + return; + } + + raw_url = 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)."; + return; + } + } + + internal static string Unquote (String str) { + int start = str.IndexOf ('\"'); + int end = str.LastIndexOf ('\"'); + if (start >= 0 && end >=0) + str = str.Substring (start + 1, end - 1); + return str.Trim (); + } + + #endregion + + #region Public Methods + + // TODO: Always returns null + public IAsyncResult BeginGetClientCertificate (AsyncCallback requestCallback, Object state) + { + return null; + } + + // TODO: Always returns null + public X509Certificate2 EndGetClientCertificate (IAsyncResult asyncResult) + { + // set no_client_certificate once done. + + return null; + } + + // TODO: Always returns null + public X509Certificate2 GetClientCertificate () + { + // set no_client_certificate once done. + + // InvalidOp if call in progress. + return null; + } + + #endregion + } +} diff --git a/websocket-sharp/Net/HttpListenerResponse.cs b/websocket-sharp/Net/HttpListenerResponse.cs new file mode 100644 index 00000000..4ae5344d --- /dev/null +++ b/websocket-sharp/Net/HttpListenerResponse.cs @@ -0,0 +1,510 @@ +// +// HttpListenerResponse.cs +// Copied from System.Net.HttpListenerResponse +// +// 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. +// + +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; + +namespace WebSocketSharp.Net { + + public sealed class HttpListenerResponse : IDisposable + { + bool disposed; + Encoding content_encoding; + long content_length; + bool cl_set; + string content_type; + CookieCollection cookies; + WebHeaderCollection headers = new WebHeaderCollection (); + bool keep_alive = true; + ResponseStream output_stream; + Version version = HttpVersion.Version11; + string location; + int status_code = 200; + string status_description = "OK"; + bool chunked; + HttpListenerContext context; + internal bool HeadersSent; + bool force_close_chunked; + + internal HttpListenerResponse (HttpListenerContext context) + { + this.context = context; + } + + internal bool ForceCloseChunked { + get { return force_close_chunked; } + } + + public Encoding ContentEncoding { + get { + if (content_encoding == null) + content_encoding = Encoding.Default; + return content_encoding; + } + set { + if (disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + //TODO: is null ok? + if (HeadersSent) + throw new InvalidOperationException ("Cannot be changed after headers are sent."); + + content_encoding = value; + } + } + + 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 >= 0", "value"); + + cl_set = true; + content_length = value; + } + } + + public string ContentType { + get { return content_type; } + set { + // TODO: is null ok? + if (disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + if (HeadersSent) + throw new InvalidOperationException ("Cannot be changed after headers are sent."); + + content_type = value; + } + } + + // RFC 2109, 2965 + the netscape specification at http://wp.netscape.com/newsref/std/cookie_spec.html + public CookieCollection Cookies { + get { + if (cookies == null) + cookies = new CookieCollection (); + return cookies; + } + set { cookies = value; } // null allowed? + } + + 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: check if this is marked readonly after headers are sent. + headers = value; + } + } + + 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; + } + } + + public Stream OutputStream { + get { + if (output_stream == null) + output_stream = context.Connection.GetResponseStream (); + return output_stream; + } + } + + 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"); + + if (disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + version = value; + } + } + + 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."); + + location = value; + } + } + + 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; + } + } + + 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 = GetStatusDescription (value); + } + } + + internal static string GetStatusDescription (int code) + { + switch (code){ + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 307: return "Temporary Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-Uri Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 422: return "Unprocessable Entity"; + case 423: return "Locked"; + case 424: return "Failed Dependency"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "Http Version Not Supported"; + case 507: return "Insufficient Storage"; + } + return ""; + } + + public string StatusDescription { + get { return status_description; } + set { + status_description = value; + } + } + + void IDisposable.Dispose () + { + Close (true); //TODO: Abort or Close? + } + + public void Abort () + { + if (disposed) + return; + + Close (true); + } + + public void AddHeader (string name, string value) + { + if (name == null) + throw new ArgumentNullException ("name"); + + if (name == "") + throw new ArgumentException ("'name' cannot be empty", "name"); + + //TODO: check for forbidden headers and invalid characters + if (value.Length > 65535) + throw new ArgumentOutOfRangeException ("value"); + + headers.Set (name, value); + } + + public void AppendCookie (Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + Cookies.Add (cookie); + } + + public void AppendHeader (string name, string value) + { + if (name == null) + throw new ArgumentNullException ("name"); + + if (name == "") + throw new ArgumentException ("'name' cannot be empty", "name"); + + if (value.Length > 65535) + throw new ArgumentOutOfRangeException ("value"); + + headers.Add (name, value); + } + + void Close (bool force) + { + disposed = true; + context.Connection.Close (force); + } + + public void Close () + { + if (disposed) + return; + + Close (false); + } + + public void Close (byte [] responseEntity, bool willBlock) + { + if (disposed) + return; + + if (responseEntity == null) + throw new ArgumentNullException ("responseEntity"); + + //TODO: if willBlock -> BeginWrite + Close ? + ContentLength64 = responseEntity.Length; + OutputStream.Write (responseEntity, 0, (int) content_length); + Close (false); + } + + 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; + } + + public void Redirect (string url) + { + StatusCode = 302; // Found + location = url; + } + + bool FindCookie (Cookie cookie) + { + string name = cookie.Name; + string domain = cookie.Domain; + string path = cookie.Path; + foreach (Cookie c in cookies) { + if (name != c.Name) + continue; + if (domain != c.Domain) + continue; + if (path == c.Path) + return true; + } + + return false; + } + + 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); + } else { + headers.SetInternal ("Content-Type", content_type); + } + } + + if (headers ["Server"] == null) + headers.SetInternal ("Server", "Mono-HTTPAPI/1.0"); + + CultureInfo inv = CultureInfo.InvariantCulture; + if (headers ["Date"] == null) + headers.SetInternal ("Date", DateTime.UtcNow.ToString ("r", inv)); + + if (!chunked) { + if (!cl_set && closing) { + cl_set = true; + content_length = 0; + } + + if (cl_set) + headers.SetInternal ("Content-Length", content_length.ToString (inv)); + } + + 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"); + conn_close = true; + } + + if (chunked) + headers.SetInternal ("Transfer-Encoding", "chunked"); + + int reuses = context.Connection.Reuses; + if (reuses >= 100) { + force_close_chunked = true; + if (!conn_close) { + headers.SetInternal ("Connection", "close"); + conn_close = true; + } + } + + if (!conn_close) { + headers.SetInternal ("Keep-Alive", String.Format ("timeout=15,max={0}", 100 - reuses)); + if (context.Request.ProtocolVersion <= HttpVersion.Version10) + headers.SetInternal ("Connection", "keep-alive"); + } + + if (location != null) + headers.SetInternal ("Location", location); + + if (cookies != null) { + foreach (Cookie cookie in cookies) + headers.SetInternal ("Set-Cookie", cookie.ToClientString ()); + } + + 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 (); + 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; + } + + public void SetCookie (Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + if (cookies != null) { + if (FindCookie (cookie)) + throw new ArgumentException ("The cookie already exists."); + } else { + cookies = new CookieCollection (); + } + + cookies.Add (cookie); + } + } +} diff --git a/websocket-sharp/Net/HttpListenerWebSocketContext.cs b/websocket-sharp/Net/HttpListenerWebSocketContext.cs new file mode 100644 index 00000000..e0d24925 --- /dev/null +++ b/websocket-sharp/Net/HttpListenerWebSocketContext.cs @@ -0,0 +1,101 @@ +#region MIT License +/** + * HttpListenerWebSocketContext.cs + * + * The MIT License + * + * Copyright (c) 2012 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.Generic; +using System.Collections.Specialized; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Principal; + +namespace WebSocketSharp.Net { + + public class HttpListenerWebSocketContext : WebSocketContext + { + private HttpListenerContext _context; + private WebSocket _socket; + + internal HttpListenerWebSocketContext(HttpListenerContext context) + { + _context = context; + _socket = new WebSocket(this); + } + + internal HttpListenerContext BaseContext { + get { return _context; } + } + + public override CookieCollection CookieCollection { + get { return _context.Request.Cookies; } + } + + public override NameValueCollection Headers { + get { return _context.Request.Headers; } + } + + public override bool IsAuthenticated { + get { return _context.Request.IsAuthenticated; } + } + + public override bool IsSecureConnection { + get { return _context.Request.IsSecureConnection; } + } + + public override bool IsLocal { + get { return _context.Request.IsLocal; } + } + + public override string Origin { + get { return Headers["Origin"]; } + } + + public override Uri RequestUri { + get { return _context.Request.RawUrl.ToUri(); } + } + + public override string SecWebSocketKey { + get { return Headers["Sec-WebSocket-Key"]; } + } + + public override IEnumerable SecWebSocketProtocols { + get { return Headers.GetValues("Sec-WebSocket-Protocol"); } + } + + public override string SecWebSocketVersion { + get { return Headers["Sec-WebSocket-Version"]; } + } + + public override IPrincipal User { + get { return _context.User; } + } + + public override WebSocket WebSocket { + get { return _socket; } + } + } +} diff --git a/websocket-sharp/Net/HttpStatusCode.cs b/websocket-sharp/Net/HttpStatusCode.cs new file mode 100644 index 00000000..0338ab01 --- /dev/null +++ b/websocket-sharp/Net/HttpStatusCode.cs @@ -0,0 +1,84 @@ +// +// HttpStatusCode.cs +// Copied from System.Net.HttpStatusCode +// +// This code was automatically generated from +// ECMA CLI XML Library Specification. +// Generator: libgen.xsl [1.0; (C) Sergey Chaban (serge@wildwestsoftware.com)] +// Created: Wed, 5 Sep 2001 06:32:05 UTC +// Source file: AllTypes.xml +// URL: http://msdn.microsoft.com/net/ecma/AllTypes.xml +// +// Copyright (C) 2001 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. +// + +namespace WebSocketSharp.Net { + + public enum HttpStatusCode { + Continue = 100, + SwitchingProtocols = 101, + OK = 200, + Created = 201, + Accepted = 202, + NonAuthoritativeInformation = 203, + NoContent = 204, + ResetContent = 205, + PartialContent = 206, + MultipleChoices = 300, + Ambiguous = 300, + MovedPermanently = 301, + Moved = 301, + Found = 302, + Redirect = 302, + SeeOther = 303, + RedirectMethod = 303, + NotModified = 304, + UseProxy = 305, + Unused = 306, + TemporaryRedirect = 307, + RedirectKeepVerb = 307, + BadRequest = 400, + Unauthorized = 401, + PaymentRequired = 402, + Forbidden = 403, + NotFound = 404, + MethodNotAllowed = 405, + NotAcceptable = 406, + ProxyAuthenticationRequired = 407, + RequestTimeout = 408, + Conflict = 409, + Gone = 410, + LengthRequired = 411, + PreconditionFailed = 412, + RequestEntityTooLarge = 413, + RequestUriTooLong = 414, + UnsupportedMediaType = 415, + RequestedRangeNotSatisfiable = 416, + ExpectationFailed = 417, + InternalServerError = 500, + NotImplemented = 501, + BadGateway = 502, + ServiceUnavailable = 503, + GatewayTimeout = 504, + HttpVersionNotSupported = 505, + } +} diff --git a/websocket-sharp/Net/HttpStreamAsyncResult.cs b/websocket-sharp/Net/HttpStreamAsyncResult.cs new file mode 100644 index 00000000..6ae5e6f0 --- /dev/null +++ b/websocket-sharp/Net/HttpStreamAsyncResult.cs @@ -0,0 +1,98 @@ +// +// HttpStreamAsyncResult.cs +// Copied from System.Net.HttpStreamAsyncResult +// +// Authors: +// 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. +// + +using System; +using System.Net; +using System.Threading; + +namespace WebSocketSharp.Net { + + class HttpStreamAsyncResult : IAsyncResult + { + bool completed; + ManualResetEvent handle; + object locker = new object (); + + internal AsyncCallback Callback; + internal int Count; + internal byte [] Buffer; + internal Exception Error; + internal int Offset; + internal object State; + internal int SynchRead; + + public object AsyncState { + get { return State; } + } + + public WaitHandle AsyncWaitHandle { + get { + lock (locker) { + if (handle == null) + handle = new ManualResetEvent (completed); + } + + return handle; + } + } + + public bool CompletedSynchronously { + get { return (SynchRead == Count); } + } + + public bool IsCompleted { + get { + lock (locker) { + return completed; + } + } + } + + public void Complete () + { + lock (locker) { + if (completed) + return; + + completed = true; + if (handle != null) + handle.Set (); + + if (Callback != null) + Callback.BeginInvoke (this, null, null); + } + } + + public void Complete (Exception e) + { + Error = e; + Complete (); + } + } +} diff --git a/websocket-sharp/Net/HttpUtility.cs b/websocket-sharp/Net/HttpUtility.cs new file mode 100644 index 00000000..7eaffa04 --- /dev/null +++ b/websocket-sharp/Net/HttpUtility.cs @@ -0,0 +1,1116 @@ +// +// HttpUtility.cs +// Copied from System.Net.HttpUtility +// +// 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) +// +// 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. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Net; +using System.Security.Permissions; +using System.Text; + +namespace WebSocketSharp.Net { + + internal sealed class HttpUtility { + + sealed class HttpQSCollection : NameValueCollection + { + public override string ToString () + { + int count = Count; + if (count == 0) + return ""; + StringBuilder sb = new StringBuilder (); + string [] keys = AllKeys; + for (int i = 0; i < count; i++) { + sb.AppendFormat ("{0}={1}&", keys [i], this [keys [i]]); + } + if (sb.Length > 0) + sb.Length--; + return sb.ToString (); + } + } + #region Fields + + static Hashtable entities; + static object lock_ = new object (); + + #endregion // Fields + + static Hashtable Entities { + get { + lock (lock_) { + if (entities == null) + InitEntities (); + + return entities; + } + } + } + + #region Constructors + + static void InitEntities () + { + // Build the hash table of HTML entity references. This list comes + // from the HTML 4.01 W3C recommendation. + entities = new Hashtable (); + 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'); + } + + public HttpUtility () + { + } + + #endregion // Constructors + + #region Methods + + public static void HtmlAttributeEncode (string s, TextWriter output) + { + output.Write(HtmlAttributeEncode(s)); + } + + public static string HtmlAttributeEncode (string s) + { + if (null == s) + return null; + + bool needEncode = false; + for (int i = 0; i < s.Length; i++) { + if (s [i] == '&' || s [i] == '"' || s [i] == '<') { + needEncode = true; + break; + } + } + + if (!needEncode) + return s; + + StringBuilder output = new StringBuilder (); + int len = s.Length; + for (int i = 0; i < len; i++) + switch (s [i]) { + case '&' : + output.Append ("&"); + break; + case '"' : + output.Append ("""); + break; + case '<': + output.Append ("<"); + break; + default: + output.Append (s [i]); + break; + } + + return output.ToString(); + } + + public static string UrlDecode (string str) + { + return UrlDecode(str, Encoding.UTF8); + } + + static char [] GetChars (MemoryStream b, Encoding e) + { + return e.GetChars (b.GetBuffer (), 0, (int) b.Length); + } + + static void WriteCharBytes (IList buf, char ch, Encoding e) + { + if (ch > 255) { + foreach (byte b in e.GetBytes (new char[] { ch })) + buf.Add (b); + } else + buf.Add ((byte)ch); + } + + public static string UrlDecode (string s, Encoding e) + { + if (null == s) + return null; + + if (s.IndexOf ('%') == -1 && s.IndexOf ('+') == -1) + return s; + + if (e == null) + e = Encoding.UTF8; + + long len = s.Length; + var bytes = new List (); + int xchar; + char ch; + + for (int i = 0; i < len; i++) { + ch = s [i]; + if (ch == '%' && i + 2 < len && s [i + 1] != '%') { + if (s [i + 1] == 'u' && i + 5 < len) { + // unicode hex sequence + xchar = GetChar (s, i + 2, 4); + if (xchar != -1) { + WriteCharBytes (bytes, (char)xchar, e); + i += 5; + } else + WriteCharBytes (bytes, '%', e); + } else if ((xchar = GetChar (s, i + 1, 2)) != -1) { + WriteCharBytes (bytes, (char)xchar, e); + i += 2; + } else { + WriteCharBytes (bytes, '%', e); + } + continue; + } + + if (ch == '+') + WriteCharBytes (bytes, ' ', e); + else + WriteCharBytes (bytes, ch, e); + } + + byte[] buf = bytes.ToArray (); + bytes = null; + return e.GetString (buf); + + } + + public static string UrlDecode (byte [] bytes, Encoding e) + { + if (bytes == null) + return null; + + return UrlDecode (bytes, 0, bytes.Length, e); + } + + static int GetInt (byte b) + { + char c = (char) b; + if (c >= '0' && c <= '9') + return c - '0'; + + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + + return -1; + } + + static int GetChar (byte [] bytes, int offset, int length) + { + int value = 0; + int end = length + offset; + for (int i = offset; i < end; i++) { + int current = GetInt (bytes [i]); + if (current == -1) + return -1; + value = (value << 4) + current; + } + + return value; + } + + static int GetChar (string str, int offset, int length) + { + int val = 0; + int end = length + offset; + for (int i = offset; i < end; i++) { + char c = str [i]; + if (c > 127) + return -1; + + int current = GetInt ((byte) c); + if (current == -1) + return -1; + val = (val << 4) + current; + } + + return val; + } + + public static string UrlDecode (byte [] bytes, int offset, int count, Encoding e) + { + if (bytes == null) + return null; + if (count == 0) + return String.Empty; + + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + if (offset < 0 || offset > bytes.Length) + throw new ArgumentOutOfRangeException ("offset"); + + if (count < 0 || offset + count > bytes.Length) + throw new ArgumentOutOfRangeException ("count"); + + StringBuilder output = new StringBuilder (); + MemoryStream acc = new MemoryStream (); + + int 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, e)); + 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, e)); + acc.SetLength (0); + } + + if (bytes [i] == '+') { + output.Append (' '); + } else { + output.Append ((char) bytes [i]); + } + } + + if (acc.Length > 0) { + output.Append (GetChars (acc, e)); + } + + acc = null; + return output.ToString (); + } + + public static byte [] UrlDecodeToBytes (byte [] bytes) + { + if (bytes == null) + return null; + + return UrlDecodeToBytes (bytes, 0, bytes.Length); + } + + public static byte [] UrlDecodeToBytes (string str) + { + return UrlDecodeToBytes (str, Encoding.UTF8); + } + + public static byte [] UrlDecodeToBytes (string str, Encoding e) + { + if (str == null) + return null; + + if (e == null) + throw new ArgumentNullException ("e"); + + return UrlDecodeToBytes (e.GetBytes (str)); + } + + public static byte [] UrlDecodeToBytes (byte [] bytes, int offset, int count) + { + if (bytes == null) + return null; + if (count == 0) + return new byte [0]; + + int len = bytes.Length; + if (offset < 0 || offset >= len) + throw new ArgumentOutOfRangeException("offset"); + + if (count < 0 || offset > len - count) + throw new ArgumentOutOfRangeException("count"); + + MemoryStream result = new MemoryStream (); + int end = offset + count; + for (int i = offset; i < end; i++){ + char c = (char) bytes [i]; + if (c == '+') { + c = ' '; + } else if (c == '%' && i < end - 2) { + int xchar = GetChar (bytes, i + 1, 2); + if (xchar != -1) { + c = (char) xchar; + i += 2; + } + } + result.WriteByte ((byte) c); + } + + return result.ToArray (); + } + + public static string UrlEncode(string str) + { + return UrlEncode(str, Encoding.UTF8); + } + + public static string UrlEncode (string s, Encoding Enc) + { + if (s == null) + return null; + + if (s == "") + return ""; + + bool needEncode = false; + int len = s.Length; + for (int i = 0; i < len; i++) { + char c = s [i]; + if ((c < '0') || (c < 'A' && c > '9') || (c > 'Z' && c < 'a') || (c > 'z')) { + if (NotEncoded (c)) + continue; + + needEncode = true; + break; + } + } + + if (!needEncode) + return s; + + // avoided GetByteCount call + byte [] bytes = new byte[Enc.GetMaxByteCount(s.Length)]; + int realLen = Enc.GetBytes (s, 0, s.Length, bytes, 0); + return Encoding.ASCII.GetString (UrlEncodeToBytes (bytes, 0, realLen)); + } + + public static string UrlEncode (byte [] bytes) + { + if (bytes == null) + return null; + + if (bytes.Length == 0) + return ""; + + return Encoding.ASCII.GetString (UrlEncodeToBytes (bytes, 0, bytes.Length)); + } + + public static string UrlEncode (byte [] bytes, int offset, int count) + { + if (bytes == null) + return null; + + if (bytes.Length == 0) + return ""; + + return Encoding.ASCII.GetString (UrlEncodeToBytes (bytes, offset, count)); + } + + public static byte [] UrlEncodeToBytes (string str) + { + return UrlEncodeToBytes (str, Encoding.UTF8); + } + + public static byte [] UrlEncodeToBytes (string str, Encoding e) + { + if (str == null) + return null; + + if (str == "") + return new byte [0]; + + byte [] bytes = e.GetBytes (str); + return UrlEncodeToBytes (bytes, 0, bytes.Length); + } + + public static byte [] UrlEncodeToBytes (byte [] bytes) + { + if (bytes == null) + return null; + + if (bytes.Length == 0) + return new byte [0]; + + return UrlEncodeToBytes (bytes, 0, bytes.Length); + } + + static char [] hexChars = "0123456789abcdef".ToCharArray (); + + static bool NotEncoded (char c) + { + return (c == '!' || c == '\'' || c == '(' || c == ')' || c == '*' || c == '-' || c == '.' || c == '_'); + } + + 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"); + int idx; + int i = (int) c; + + result.WriteByte ((byte)'%'); + result.WriteByte ((byte)'u'); + 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)'%'); + + int idx = ((int) c) >> 4; + result.WriteByte ((byte)hexChars [idx]); + idx = ((int) c) & 0x0F; + result.WriteByte ((byte)hexChars [idx]); + } + else + result.WriteByte ((byte)c); + } + + public static byte [] UrlEncodeToBytes (byte [] bytes, int offset, int count) + { + if (bytes == null) + return null; + + int len = bytes.Length; + if (len == 0) + return new byte [0]; + + if (offset < 0 || offset >= len) + throw new ArgumentOutOfRangeException("offset"); + + if (count < 0 || count > len - offset) + throw new ArgumentOutOfRangeException("count"); + + MemoryStream result = new MemoryStream (count); + int end = offset + count; + for (int i = offset; i < end; i++) + UrlEncodeChar ((char)bytes [i], result, false); + + return result.ToArray(); + } + + public static string UrlEncodeUnicode (string str) + { + if (str == null) + return null; + + return Encoding.ASCII.GetString (UrlEncodeUnicodeToBytes (str)); + } + + public static byte [] UrlEncodeUnicodeToBytes (string str) + { + if (str == null) + return null; + + if (str == "") + return new byte [0]; + + MemoryStream result = new MemoryStream (str.Length); + foreach (char c in str){ + UrlEncodeChar (c, result, true); + } + return result.ToArray (); + } + + /// + /// Decodes an HTML-encoded string and returns the decoded string. + /// + /// The HTML string to decode. + /// The decoded text. + public static string HtmlDecode (string s) + { + if (s == null) + throw new ArgumentNullException ("s"); + + if (s.IndexOf ('&') == -1) + return s; + + StringBuilder entity = new StringBuilder (); + StringBuilder output = new StringBuilder (); + int len = s.Length; + // 0 -> nothing, + // 1 -> right after '&' + // 2 -> between '&' and ';' but no '#' + // 3 -> '#' found after '&' and getting numbers + int state = 0; + int number = 0; + bool have_trailing_digits = false; + + for (int i = 0; i < len; i++) { + char c = s [i]; + if (state == 0) { + if (c == '&') { + entity.Append (c); + state = 1; + } else { + output.Append (c); + } + continue; + } + + if (c == '&') { + state = 1; + if (have_trailing_digits) { + entity.Append (number.ToString (CultureInfo.InvariantCulture)); + have_trailing_digits = 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 == ';') { + string 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; + have_trailing_digits = false; + } else if (Char.IsDigit (c)) { + number = number * 10 + ((int) c - '0'); + have_trailing_digits = true; + } else { + state = 2; + if (have_trailing_digits) { + entity.Append (number.ToString (CultureInfo.InvariantCulture)); + have_trailing_digits = false; + } + entity.Append (c); + } + } + } + + if (entity.Length > 0) { + output.Append (entity.ToString ()); + } else if (have_trailing_digits) { + output.Append (number.ToString (CultureInfo.InvariantCulture)); + } + return output.ToString (); + } + + /// + /// Decodes an HTML-encoded string and sends the resulting output to a TextWriter output stream. + /// + /// The HTML string to decode + /// The TextWriter output stream containing the decoded string. + public static void HtmlDecode(string s, TextWriter output) + { + if (s != null) + output.Write (HtmlDecode (s)); + } + + /// + /// HTML-encodes a string and returns the encoded string. + /// + /// The text string to encode. + /// The HTML-encoded text. + public static string HtmlEncode (string s) + { + if (s == null) + return null; + + bool needEncode = false; + for (int i = 0; i < s.Length; i++) { + char c = s [i]; + if (c == '&' || c == '"' || c == '<' || c == '>' || c > 159) { + needEncode = true; + break; + } + } + + if (!needEncode) + return s; + + StringBuilder output = new StringBuilder (); + + int len = s.Length; + for (int i = 0; i < len; i++) + switch (s [i]) { + case '&' : + output.Append ("&"); + break; + case '>' : + output.Append (">"); + break; + case '<' : + output.Append ("<"); + break; + case '"' : + output.Append ("""); + break; + default: + // 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 TARGET_JVM + if (s [i] > 159 && s [i] < 256) { +#else + if (s [i] > 159) { +#endif + output.Append ("&#"); + output.Append (((int) s [i]).ToString (CultureInfo.InvariantCulture)); + output.Append (";"); + } else { + output.Append (s [i]); + } + break; + } + return output.ToString (); + } + + /// + /// HTML-encodes a string and sends the resulting output to a TextWriter output stream. + /// + /// The string to encode. + /// The TextWriter output stream containing the encoded string. + public static void HtmlEncode(string s, TextWriter output) + { + if (s != null) + output.Write (HtmlEncode (s)); + } + + public static string UrlPathEncode (string s) + { + if (s == null || s.Length == 0) + return s; + + MemoryStream result = new MemoryStream (); + int length = s.Length; + for (int i = 0; i < length; i++) { + UrlPathEncodeChar (s [i], result); + } + return Encoding.ASCII.GetString (result.ToArray ()); + } + + static void UrlPathEncodeChar (char c, Stream result) + { + if (c < 33 || c > 126) { + byte [] bIn = Encoding.UTF8.GetBytes (c.ToString ()); + for (int i = 0; i < bIn.Length; i++) { + result.WriteByte ((byte) '%'); + int idx = ((int) bIn [i]) >> 4; + result.WriteByte ((byte) hexChars [idx]); + idx = ((int) bIn [i]) & 0x0F; + result.WriteByte ((byte) hexChars [idx]); + } + } + else if (c == ' ') { + result.WriteByte ((byte) '%'); + result.WriteByte ((byte) '2'); + result.WriteByte ((byte) '0'); + } + else + result.WriteByte ((byte) c); + } + + 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"); + if (encoding == null) + throw new ArgumentNullException ("encoding"); + if (query.Length == 0 || (query.Length == 1 && query[0] == '?')) + return new NameValueCollection (); + if (query[0] == '?') + query = query.Substring (1); + + NameValueCollection result = new HttpQSCollection (); + ParseQueryString (query, encoding, result); + return result; + } + + internal static void ParseQueryString (string query, Encoding encoding, NameValueCollection result) + { + if (query.Length == 0) + return; + + string decoded = HtmlDecode (query); + int decodedLength = decoded.Length; + int namePos = 0; + bool 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; + } + } + #endregion // Methods + } +} + diff --git a/websocket-sharp/Net/HttpVersion.cs b/websocket-sharp/Net/HttpVersion.cs new file mode 100644 index 00000000..570009ad --- /dev/null +++ b/websocket-sharp/Net/HttpVersion.cs @@ -0,0 +1,42 @@ +// +// HttpVersion.cs +// Copied from System.Net.HttpVersion +// +// Author: +// Lawrence Pit (loz@cable.a2000.nl) +// +// 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. +// + +using System; + +namespace WebSocketSharp.Net { + + // + // + public class HttpVersion { + + public static readonly Version Version10 = new Version (1, 0); + public static readonly Version Version11 = new Version (1, 1); + + // pretty useless.. + public HttpVersion () {} + } +} diff --git a/websocket-sharp/Net/ListenerAsyncResult.cs b/websocket-sharp/Net/ListenerAsyncResult.cs new file mode 100644 index 00000000..78f64438 --- /dev/null +++ b/websocket-sharp/Net/ListenerAsyncResult.cs @@ -0,0 +1,186 @@ +// +// ListenerAsyncResult.cs +// Copied from System.Net.ListenerAsyncResult +// +// 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. +// + +using System; +using System.Net; +using System.Threading; + +namespace WebSocketSharp.Net { + + class ListenerAsyncResult : IAsyncResult + { + static WaitCallback InvokeCB = new WaitCallback (InvokeCallback); + + AsyncCallback cb; + bool completed; + HttpListenerContext context; + Exception exception; + ListenerAsyncResult forward; + ManualResetEvent handle; + object locker; + object state; + bool synch; + + internal bool EndCalled; + internal bool InGet; + + public ListenerAsyncResult (AsyncCallback cb, object state) + { + this.cb = cb; + this.state = state; + this.locker = new object(); + } + + public object AsyncState { + get { + if (forward != null) + return forward.AsyncState; + return state; + } + } + + public WaitHandle AsyncWaitHandle { + get { + if (forward != null) + return forward.AsyncWaitHandle; + + lock (locker) { + if (handle == null) + handle = new ManualResetEvent (completed); + } + + return handle; + } + } + + public bool CompletedSynchronously { + get { + if (forward != null) + return forward.CompletedSynchronously; + return synch; + } + + } + + public bool IsCompleted { + get { + if (forward != null) + return forward.IsCompleted; + + lock (locker) { + return completed; + } + } + } + + static void InvokeCallback (object o) + { + ListenerAsyncResult ares = (ListenerAsyncResult) o; + if (ares.forward != null) { + InvokeCallback (ares.forward); + return; + } + try { + ares.cb (ares); + } catch { + } + } + + internal void Complete (Exception exc) + { + if (forward != null) { + forward.Complete (exc); + return; + } + exception = exc; + if (InGet && (exc is ObjectDisposedException)) + exception = new HttpListenerException (500, "Listener closed"); + lock (locker) { + completed = true; + if (handle != null) + handle.Set (); + + if (cb != null) + ThreadPool.UnsafeQueueUserWorkItem (InvokeCB, this); + } + } + + internal void Complete (HttpListenerContext context) + { + Complete (context, false); + } + + 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; + } + } +} diff --git a/websocket-sharp/Net/ListenerPrefix.cs b/websocket-sharp/Net/ListenerPrefix.cs new file mode 100644 index 00000000..41f4d590 --- /dev/null +++ b/websocket-sharp/Net/ListenerPrefix.cs @@ -0,0 +1,166 @@ +// +// ListenerPrefix.cs +// Copied from System.ListenerPrefix +// +// Author: +// Gonzalo Paniagua Javier (gonzalo@novell.com) +// Oleg Mihailik (mihailik gmail co_m) +// +// 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. +// + +using System; +using System.Net; + +namespace WebSocketSharp.Net { + + sealed class ListenerPrefix { + + IPAddress [] addresses; + string host; + string original; + string path; + ushort port; + bool secure; + + public HttpListener Listener; + + public ListenerPrefix (string prefix) + { + original = prefix; + Parse (prefix); + } + + public IPAddress [] Addresses { + get { return addresses; } + set { addresses = value; } + } + + public string Host { + get { return host; } + } + + public int Port { + get { return (int) port; } + } + + public string Path { + get { return path; } + } + + public bool Secure { + get { return secure; } + } + + void Parse (string uri) + { + int default_port = (uri.StartsWith ("http://")) ? 80 : -1; + if (default_port == -1) { + default_port = (uri.StartsWith ("https://")) ? 443 : -1; + secure = true; + } + + int length = uri.Length; + int start_host = uri.IndexOf (':') + 3; + if (start_host >= length) + throw new ArgumentException ("No host specified."); + + int colon = uri.IndexOf (':', start_host, length - start_host); + int root; + if (colon > 0) { + host = uri.Substring (start_host, colon - start_host); + root = uri.IndexOf ('/', colon, length - colon); + port = (ushort) Int32.Parse (uri.Substring (colon + 1, root - colon - 1)); + path = uri.Substring (root); + } else { + root = uri.IndexOf ('/', start_host, length - start_host); + host = uri.Substring (start_host, root - start_host); + path = uri.Substring (root); + } + if (path.Length != 1) + path = path.Substring (0, path.Length - 1); + } + + public static void CheckUri (string uri) + { + if (uri == null) + throw new ArgumentNullException ("uriPrefix"); + + int default_port = (uri.StartsWith ("http://")) ? 80 : -1; + if (default_port == -1) + default_port = (uri.StartsWith ("https://")) ? 443 : -1; + if (default_port == -1) + throw new ArgumentException ("Only 'http' and 'https' schemes are supported."); + + int length = uri.Length; + int start_host = uri.IndexOf (':') + 3; + if (start_host >= length) + throw new ArgumentException ("No host specified."); + + int colon = uri.IndexOf (':', start_host, length - start_host); + if (start_host == colon) + throw new ArgumentException ("No host specified."); + + int root; + if (colon > 0) { + root = uri.IndexOf ('/', colon, length - colon); + if (root == -1) + throw new ArgumentException ("No path specified."); + + try { + int p = Int32.Parse (uri.Substring (colon + 1, root - colon - 1)); + if (p <= 0 || p >= 65536) + throw new Exception (); + } catch { + throw new ArgumentException ("Invalid port."); + } + } else { + root = uri.IndexOf ('/', start_host, length - start_host); + if (root == -1) + throw new ArgumentException ("No path specified."); + } + + if (uri [uri.Length - 1] != '/') + throw new ArgumentException ("The prefix must end with '/'"); + } + + // Equals and GetHashCode are required to detect duplicates in HttpListenerPrefixCollection. + public override bool Equals (object o) + { + ListenerPrefix other = o as ListenerPrefix; + if (other == null) + return false; + + return (original == other.original); + } + + public override int GetHashCode () + { + return original.GetHashCode (); + } + + public override string ToString () + { + return original; + } + } +} diff --git a/websocket-sharp/Net/RequestStream.cs b/websocket-sharp/Net/RequestStream.cs new file mode 100644 index 00000000..2b08ea40 --- /dev/null +++ b/websocket-sharp/Net/RequestStream.cs @@ -0,0 +1,227 @@ +// +// RequestStream.cs +// Copied from System.Net.RequestStream +// +// 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. +// + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace WebSocketSharp.Net { + + class RequestStream : Stream + { + byte [] buffer; + int offset; + int length; + long remaining_body; + bool disposed; + System.IO.Stream stream; + + internal RequestStream (System.IO.Stream stream, byte [] buffer, int offset, int length) + : this (stream, buffer, offset, length, -1) + { + } + + internal RequestStream (System.IO.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; + } + + public override bool CanRead { + get { return true; } + } + + public override bool CanSeek { + get { return false; } + } + + public override bool CanWrite { + get { return false; } + } + + public override long Length { + get { throw new NotSupportedException (); } + } + + public override long Position { + get { throw new NotSupportedException (); } + set { throw new NotSupportedException (); } + } + + + public override void Close () + { + disposed = true; + } + + public override void Flush () + { + } + + + // 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 off, int count) + { + if (buffer == null) + throw new ArgumentNullException ("buffer"); + if (off < 0) + throw new ArgumentOutOfRangeException ("offset", "< 0"); + if (count < 0) + throw new ArgumentOutOfRangeException ("count", "< 0"); + int len = buffer.Length; + if (off > len) + throw new ArgumentException ("destination offset is beyond array size"); + if (off > len - count) + throw new ArgumentException ("Reading would overrun buffer"); + + if (this.remaining_body == 0) + return -1; + + if (this.length == 0) + return 0; + + int size = Math.Min (this.length, count); + if (this.remaining_body > 0) + size = (int) Math.Min (size, this.remaining_body); + + if (this.offset > this.buffer.Length - size) { + size = Math.Min (size, this.buffer.Length - this.offset); + } + if (size == 0) + return 0; + + Buffer.BlockCopy (this.buffer, this.offset, buffer, off, size); + this.offset += size; + this.length -= size; + if (this.remaining_body > 0) + remaining_body -= size; + return size; + } + + public override int Read ([In,Out] byte[] buffer, int offset, int count) + { + if (disposed) + throw new ObjectDisposedException (typeof (RequestStream).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; + } + + nread = stream.Read (buffer, offset, count); + if (nread > 0 && remaining_body > 0) + remaining_body -= nread; + return nread; + } + + public override IAsyncResult BeginRead (byte [] buffer, int offset, int count, + AsyncCallback cback, object state) + { + if (disposed) + throw new ObjectDisposedException (typeof (RequestStream).ToString ()); + + int nread = FillFromBuffer (buffer, offset, count); + if (nread > 0 || nread == -1) { + HttpStreamAsyncResult ares = new HttpStreamAsyncResult (); + ares.Buffer = buffer; + ares.Offset = offset; + ares.Count = count; + ares.Callback = cback; + ares.State = state; + ares.SynchRead = nread; + ares.Complete (); + 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); + return stream.BeginRead (buffer, offset, count, cback, state); + } + + public override int EndRead (IAsyncResult ares) + { + if (disposed) + throw new ObjectDisposedException (typeof (RequestStream).ToString ()); + + if (ares == null) + throw new ArgumentNullException ("async_result"); + + if (ares is HttpStreamAsyncResult) { + HttpStreamAsyncResult r = (HttpStreamAsyncResult) ares; + if (!ares.IsCompleted) + ares.AsyncWaitHandle.WaitOne (); + return r.SynchRead; + } + + // Close on exception? + int nread = stream.EndRead (ares); + if (remaining_body > 0 && nread > 0) + remaining_body -= nread; + return nread; + } + + public override long Seek (long offset, SeekOrigin origin) + { + 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 IAsyncResult BeginWrite (byte [] buffer, int offset, int count, + AsyncCallback cback, object state) + { + throw new NotSupportedException (); + } + + public override void EndWrite (IAsyncResult async_result) + { + throw new NotSupportedException (); + } + } +} diff --git a/websocket-sharp/Net/ResponseStream.cs b/websocket-sharp/Net/ResponseStream.cs new file mode 100644 index 00000000..b3ccb5c4 --- /dev/null +++ b/websocket-sharp/Net/ResponseStream.cs @@ -0,0 +1,243 @@ +// +// ResponseStream.cs +// Copied from System.Net.ResponseStream +// +// 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. +// + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Runtime.InteropServices; + +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? + class ResponseStream : Stream + { + + HttpListenerResponse response; + bool ignore_errors; + bool disposed; + bool trailer_sent; + System.IO.Stream stream; + + internal ResponseStream (System.IO.Stream stream, HttpListenerResponse response, bool ignore_errors) + { + this.response = response; + this.ignore_errors = ignore_errors; + this.stream = stream; + } + + public override bool CanRead { + get { return false; } + } + + public override bool CanSeek { + get { return false; } + } + + public override bool CanWrite { + get { return true; } + } + + public override long Length { + get { throw new NotSupportedException (); } + } + + public override long Position { + get { throw new NotSupportedException (); } + set { throw new NotSupportedException (); } + } + + + 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; + } + response.Close (); + } + } + + MemoryStream GetHeaders (bool closing) + { + if (response.HeadersSent) + return null; + MemoryStream ms = new MemoryStream (); + response.SendHeaders (closing, ms); + return ms; + } + + public override void Flush () + { + } + + static byte [] crlf = new byte [] { 13, 10 }; + 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); + } + + 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); + } + } + + public override void Write (byte [] buffer, int offset, int count) + { + if (disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + 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); + } + + 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); + } + + if (count > 0) + InternalWrite (buffer, offset, count); + if (chunked) + InternalWrite (crlf, 0, 2); + } + + public override IAsyncResult BeginWrite (byte [] buffer, int offset, int count, + AsyncCallback cback, object state) + { + if (disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + 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); + } + + return stream.BeginWrite (buffer, offset, count, cback, state); + } + + public override void EndWrite (IAsyncResult ares) + { + if (disposed) + throw new ObjectDisposedException (GetType ().ToString ()); + + 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); + } + } + + public override int Read ([In,Out] byte[] buffer, int offset, int count) + { + throw new NotSupportedException (); + } + + public override IAsyncResult BeginRead (byte [] buffer, int offset, int count, + AsyncCallback cback, object state) + { + throw new NotSupportedException (); + } + + public override int EndRead (IAsyncResult ares) + { + throw new NotSupportedException (); + } + + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException (); + } + + public override void SetLength (long value) + { + throw new NotSupportedException (); + } + } +} diff --git a/websocket-sharp/Net/WebHeaderCollection.cs b/websocket-sharp/Net/WebHeaderCollection.cs new file mode 100644 index 00000000..7db31aa1 --- /dev/null +++ b/websocket-sharp/Net/WebHeaderCollection.cs @@ -0,0 +1,730 @@ +// +// WebHeaderCollection.cs +// Copied from System.Net.WebHeaderCollection +// +// Authors: +// Lawrence Pit (loz@cable.a2000.nl) +// Gonzalo Paniagua Javier (gonzalo@ximian.com) +// Miguel de Icaza (miguel@novell.com) +// +// Copyright 2003 Ximian, Inc. (http://www.ximian.com) +// Copyright 2007 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. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Text; + +// See RFC 2068 par 4.2 Message Headers + +namespace WebSocketSharp.Net { + + [Serializable] + [ComVisible(true)] + public class WebHeaderCollection : NameValueCollection, ISerializable + { + private static readonly Dictionary multiValue; + private static readonly Dictionary restricted; + private static readonly Dictionary restricted_response; + + private bool internallyCreated = false; + + // Static Initializer + + static WebHeaderCollection () + { + // the list of restricted header names as defined + // by the ms.net spec + restricted = new Dictionary (StringComparer.InvariantCultureIgnoreCase); + restricted.Add ("accept", true); + restricted.Add ("connection", true); + restricted.Add ("content-length", true); + restricted.Add ("content-type", true); + restricted.Add ("date", true); + restricted.Add ("expect", true); + restricted.Add ("host", true); + restricted.Add ("if-modified-since", true); + restricted.Add ("range", true); + restricted.Add ("referer", true); + restricted.Add ("transfer-encoding", true); + restricted.Add ("user-agent", true); + restricted.Add ("proxy-connection", true); + + // + restricted_response = new Dictionary (StringComparer.InvariantCultureIgnoreCase); + restricted_response.Add ("Content-Length", true); + restricted_response.Add ("Transfer-Encoding", true); + restricted_response.Add ("WWW-Authenticate", true); + + // see par 14 of RFC 2068 to see which header names + // accept multiple values each separated by a comma + multiValue = new Dictionary (StringComparer.InvariantCultureIgnoreCase); + multiValue.Add ("accept", true); + multiValue.Add ("accept-charset", true); + multiValue.Add ("accept-encoding", true); + multiValue.Add ("accept-language", true); + multiValue.Add ("accept-ranges", true); + multiValue.Add ("allow", true); + multiValue.Add ("authorization", true); + multiValue.Add ("cache-control", true); + multiValue.Add ("connection", true); + multiValue.Add ("content-encoding", true); + multiValue.Add ("content-language", true); + multiValue.Add ("expect", true); + multiValue.Add ("if-match", true); + multiValue.Add ("if-none-match", true); + multiValue.Add ("proxy-authenticate", true); + multiValue.Add ("public", true); + multiValue.Add ("range", true); + multiValue.Add ("transfer-encoding", true); + multiValue.Add ("upgrade", true); + multiValue.Add ("vary", true); + multiValue.Add ("via", true); + multiValue.Add ("warning", true); + multiValue.Add ("www-authenticate", true); + + // Extra + multiValue.Add ("set-cookie", true); + multiValue.Add ("set-cookie2", true); + } + + // Constructors + + public WebHeaderCollection () { } + + protected WebHeaderCollection ( + SerializationInfo serializationInfo, + StreamingContext streamingContext) + { + int count; + + try { + count = serializationInfo.GetInt32("Count"); + for (int i = 0; i < count; i++) + this.Add (serializationInfo.GetString (i.ToString ()), + serializationInfo.GetString ((count + i).ToString ())); + } catch (SerializationException){ + count = serializationInfo.GetInt32("count"); + for (int i = 0; i < count; i++) + this.Add (serializationInfo.GetString ("k" + i), + serializationInfo.GetString ("v" + i)); + } + + } + + internal WebHeaderCollection (bool internallyCreated) + { + this.internallyCreated = internallyCreated; + } + + // Methods + + public void Add (string header) + { + if (header == null) + throw new ArgumentNullException ("header"); + int pos = header.IndexOf (':'); + if (pos == -1) + throw new ArgumentException ("no colon found", "header"); + this.Add (header.Substring (0, pos), + header.Substring (pos + 1)); + } + + public override void Add (string name, string value) + { + if (name == null) + throw new ArgumentNullException ("name"); + if (internallyCreated && IsRestricted (name)) + throw new ArgumentException ("This header must be modified with the appropiate property."); + this.AddWithoutValidate (name, value); + } + + protected void AddWithoutValidate (string headerName, string headerValue) + { + if (!IsHeaderName (headerName)) + throw new ArgumentException ("invalid header name: " + headerName, "headerName"); + if (headerValue == null) + headerValue = String.Empty; + else + headerValue = headerValue.Trim (); + if (!IsHeaderValue (headerValue)) + throw new ArgumentException ("invalid header value: " + headerValue, "headerValue"); + base.Add (headerName, headerValue); + } + + public override string [] GetValues (string header) + { + if (header == null) + throw new ArgumentNullException ("header"); + + string [] values = base.GetValues (header); + if (values == null || values.Length == 0) + return null; + + /* + if (IsMultiValue (header)) { + values = GetMultipleValues (values); + } + */ + + return values; + } + + public override string[] GetValues (int index) + { + string[] values = base.GetValues (index); + if (values == null || values.Length == 0) { + return(null); + } + + return(values); + } + + /* Now i wonder why this is here... + static string [] GetMultipleValues (string [] values) + { + ArrayList mvalues = new ArrayList (values.Length); + StringBuilder sb = null; + for (int i = 0; i < values.Length; ++i) { + string val = values [i]; + if (val.IndexOf (',') == -1) { + mvalues.Add (val); + continue; + } + + if (sb == null) + sb = new StringBuilder (); + + bool quote = false; + for (int k = 0; k < val.Length; k++) { + char c = val [k]; + if (c == '"') { + quote = !quote; + } else if (!quote && c == ',') { + mvalues.Add (sb.ToString ().Trim ()); + sb.Length = 0; + continue; + } + sb.Append (c); + } + + if (sb.Length > 0) { + mvalues.Add (sb.ToString ().Trim ()); + sb.Length = 0; + } + } + + return (string []) mvalues.ToArray (typeof (string)); + } + */ + + public static bool IsRestricted (string headerName) + { + if (headerName == null) + throw new ArgumentNullException ("headerName"); + + if (headerName == "") // MS throw nullexception here! + throw new ArgumentException ("empty string", "headerName"); + + if (!IsHeaderName (headerName)) + throw new ArgumentException ("Invalid character in header"); + + return restricted.ContainsKey (headerName); + } + + public static bool IsRestricted (string headerName, bool response) + { + if (String.IsNullOrEmpty (headerName)) + throw new ArgumentNullException ("headerName"); + + if (!IsHeaderName (headerName)) + throw new ArgumentException ("Invalid character in header"); + + + if (response) + return restricted_response.ContainsKey (headerName); + return restricted.ContainsKey (headerName); + } + + public override void OnDeserialization (object sender) + { + } + + public override void Remove (string name) + { + if (name == null) + throw new ArgumentNullException ("name"); + if (internallyCreated && IsRestricted (name)) + throw new ArgumentException ("restricted header"); + base.Remove (name); + } + + public override void Set (string name, string value) + { + if (name == null) + throw new ArgumentNullException ("name"); + if (internallyCreated && IsRestricted (name)) + throw new ArgumentException ("restricted header"); + if (!IsHeaderName (name)) + throw new ArgumentException ("invalid header name"); + if (value == null) + value = String.Empty; + else + value = value.Trim (); + if (!IsHeaderValue (value)) + throw new ArgumentException ("invalid header value"); + base.Set (name, value); + } + + public byte[] ToByteArray () + { + return Encoding.UTF8.GetBytes(ToString ()); + } + + internal string ToStringMultiValue () + { + StringBuilder sb = new StringBuilder(); + + int count = base.Count; + for (int i = 0; i < count ; i++) { + string key = GetKey (i); + if (IsMultiValue (key)) { + foreach (string v in GetValues (i)) { + sb.Append (key) + .Append (": ") + .Append (v) + .Append ("\r\n"); + } + } else { + sb.Append (key) + .Append (": ") + .Append (Get (i)) + .Append ("\r\n"); + } + } + return sb.Append("\r\n").ToString(); + } + + public override string ToString () + { + StringBuilder sb = new StringBuilder(); + + int count = base.Count; + for (int i = 0; i < count ; i++) + sb.Append (GetKey (i)) + .Append (": ") + .Append (Get (i)) + .Append ("\r\n"); + + return sb.Append("\r\n").ToString(); + } + + void ISerializable.GetObjectData ( + SerializationInfo serializationInfo, + StreamingContext streamingContext) + { + GetObjectData (serializationInfo, streamingContext); + } + + public override void GetObjectData (SerializationInfo serializationInfo, StreamingContext streamingContext) + { + int count = base.Count; + serializationInfo.AddValue ("Count", count); + for (int i = 0; i < count; i++) { + serializationInfo.AddValue (i.ToString (), GetKey (i)); + serializationInfo.AddValue ((count + i).ToString (), Get (i)); + } + } + + public override string[] AllKeys + { + get { + return(base.AllKeys); + } + } + + public override int Count + { + get { + return(base.Count); + } + } + + public override KeysCollection Keys + { + get { + return(base.Keys); + } + } + + public override string Get (int index) + { + return(base.Get (index)); + } + + public override string Get (string name) + { + return(base.Get (name)); + } + + public override string GetKey (int index) + { + return(base.GetKey (index)); + } + + public void Add (HttpRequestHeader header, string value) + { + Add (RequestHeaderToString (header), value); + } + + public void Remove (HttpRequestHeader header) + { + Remove (RequestHeaderToString (header)); + } + + public void Set (HttpRequestHeader header, string value) + { + Set (RequestHeaderToString (header), value); + } + + public void Add (HttpResponseHeader header, string value) + { + Add (ResponseHeaderToString (header), value); + } + + public void Remove (HttpResponseHeader header) + { + Remove (ResponseHeaderToString (header)); + } + + public void Set (HttpResponseHeader header, string value) + { + Set (ResponseHeaderToString (header), value); + } + + string RequestHeaderToString (HttpRequestHeader value) + { + switch (value){ + case HttpRequestHeader.CacheControl: + return "Cache-Control"; + case HttpRequestHeader.Connection: + return "Connection"; + case HttpRequestHeader.Date: + return "Date"; + case HttpRequestHeader.KeepAlive: + return "Keep-Alive"; + case HttpRequestHeader.Pragma: + return "Pragma"; + case HttpRequestHeader.Trailer: + return "Trailer"; + case HttpRequestHeader.TransferEncoding: + return "Transfer-Encoding"; + case HttpRequestHeader.Upgrade: + return "Upgrade"; + case HttpRequestHeader.Via: + return "Via"; + case HttpRequestHeader.Warning: + return "Warning"; + case HttpRequestHeader.Allow: + return "Allow"; + case HttpRequestHeader.ContentLength: + return "Content-Length"; + case HttpRequestHeader.ContentType: + return "Content-Type"; + case HttpRequestHeader.ContentEncoding: + return "Content-Encoding"; + case HttpRequestHeader.ContentLanguage: + return "Content-Language"; + case HttpRequestHeader.ContentLocation: + return "Content-Location"; + case HttpRequestHeader.ContentMd5: + return "Content-MD5"; + case HttpRequestHeader.ContentRange: + return "Content-Range"; + case HttpRequestHeader.Expires: + return "Expires"; + case HttpRequestHeader.LastModified: + return "Last-Modified"; + case HttpRequestHeader.Accept: + return "Accept"; + case HttpRequestHeader.AcceptCharset: + return "Accept-Charset"; + case HttpRequestHeader.AcceptEncoding: + return "Accept-Encoding"; + case HttpRequestHeader.AcceptLanguage: + return "accept-language"; + case HttpRequestHeader.Authorization: + return "Authorization"; + case HttpRequestHeader.Cookie: + return "Cookie"; + case HttpRequestHeader.Expect: + return "Expect"; + case HttpRequestHeader.From: + return "From"; + case HttpRequestHeader.Host: + return "Host"; + case HttpRequestHeader.IfMatch: + return "If-Match"; + case HttpRequestHeader.IfModifiedSince: + return "If-Modified-Since"; + case HttpRequestHeader.IfNoneMatch: + return "If-None-Match"; + case HttpRequestHeader.IfRange: + return "If-Range"; + case HttpRequestHeader.IfUnmodifiedSince: + return "If-Unmodified-Since"; + case HttpRequestHeader.MaxForwards: + return "Max-Forwards"; + case HttpRequestHeader.ProxyAuthorization: + return "Proxy-Authorization"; + case HttpRequestHeader.Referer: + return "Referer"; + case HttpRequestHeader.Range: + return "Range"; + case HttpRequestHeader.Te: + return "TE"; + case HttpRequestHeader.Translate: + return "Translate"; + case HttpRequestHeader.UserAgent: + return "User-Agent"; + default: + throw new InvalidOperationException (); + } + } + + + public string this[HttpRequestHeader hrh] + { + get { + return Get (RequestHeaderToString (hrh)); + } + + set { + Add (RequestHeaderToString (hrh), value); + } + } + + string ResponseHeaderToString (HttpResponseHeader value) + { + switch (value){ + case HttpResponseHeader.CacheControl: + return "Cache-Control"; + case HttpResponseHeader.Connection: + return "Connection"; + case HttpResponseHeader.Date: + return "Date"; + case HttpResponseHeader.KeepAlive: + return "Keep-Alive"; + case HttpResponseHeader.Pragma: + return "Pragma"; + case HttpResponseHeader.Trailer: + return "Trailer"; + case HttpResponseHeader.TransferEncoding: + return "Transfer-Encoding"; + case HttpResponseHeader.Upgrade: + return "Upgrade"; + case HttpResponseHeader.Via: + return "Via"; + case HttpResponseHeader.Warning: + return "Warning"; + case HttpResponseHeader.Allow: + return "Allow"; + case HttpResponseHeader.ContentLength: + return "Content-Length"; + case HttpResponseHeader.ContentType: + return "Content-Type"; + case HttpResponseHeader.ContentEncoding: + return "Content-Encoding"; + case HttpResponseHeader.ContentLanguage: + return "Content-Language"; + case HttpResponseHeader.ContentLocation: + return "Content-Location"; + case HttpResponseHeader.ContentMd5: + return "Content-MD5"; + case HttpResponseHeader.ContentRange: + return "Content-Range"; + case HttpResponseHeader.Expires: + return "Expires"; + case HttpResponseHeader.LastModified: + return "Last-Modified"; + case HttpResponseHeader.AcceptRanges: + return "Accept-Ranges"; + case HttpResponseHeader.Age: + return "Age"; + case HttpResponseHeader.ETag: + return "ETag"; + case HttpResponseHeader.Location: + return "Location"; + case HttpResponseHeader.ProxyAuthenticate: + return "Proxy-Authenticate"; + case HttpResponseHeader.RetryAfter: + return "Retry-After"; + case HttpResponseHeader.Server: + return "Server"; + case HttpResponseHeader.SetCookie: + return "Set-Cookie"; + case HttpResponseHeader.Vary: + return "Vary"; + case HttpResponseHeader.WwwAuthenticate: + return "WWW-Authenticate"; + default: + throw new InvalidOperationException (); + } + } + + public string this[HttpResponseHeader hrh] + { + get + { + return Get (ResponseHeaderToString (hrh)); + } + + set + { + Add (ResponseHeaderToString (hrh), value); + } + } + + public override void Clear () + { + base.Clear (); + } + + public override IEnumerator GetEnumerator () + { + return(base.GetEnumerator ()); + } + + // Internal Methods + + // With this we don't check for invalid characters in header. See bug #55994. + internal void SetInternal (string header) + { + int pos = header.IndexOf (':'); + if (pos == -1) + throw new ArgumentException ("no colon found", "header"); + + SetInternal (header.Substring (0, pos), header.Substring (pos + 1)); + } + + internal void SetInternal (string name, string value) + { + if (value == null) + value = String.Empty; + else + value = value.Trim (); + if (!IsHeaderValue (value)) + throw new ArgumentException ("invalid header value"); + + if (IsMultiValue (name)) { + base.Add (name, value); + } else { + base.Remove (name); + base.Set (name, value); + } + } + + internal void RemoveAndAdd (string name, string value) + { + if (value == null) + value = String.Empty; + else + value = value.Trim (); + + base.Remove (name); + base.Set (name, value); + } + + internal void RemoveInternal (string name) + { + if (name == null) + throw new ArgumentNullException ("name"); + base.Remove (name); + } + + // Private Methods + + internal static bool IsMultiValue (string headerName) + { + if (headerName == null || headerName == "") + return false; + + return multiValue.ContainsKey (headerName); + } + + internal static bool IsHeaderValue (string value) + { + // TEXT any 8 bit value except CTL's (0-31 and 127) + // but including \r\n space and \t + // after a newline at least one space or \t must follow + // certain header fields allow comments () + + int len = value.Length; + for (int i = 0; i < len; i++) { + char c = value [i]; + if (c == 127) + return false; + if (c < 0x20 && (c != '\r' && c != '\n' && c != '\t')) + return false; + if (c == '\n' && ++i < len) { + c = value [i]; + if (c != ' ' && c != '\t') + return false; + } + } + + return true; + } + + internal static bool IsHeaderName (string name) + { + if (name == null || name.Length == 0) + return false; + + int len = name.Length; + for (int i = 0; i < len; i++) { + char c = name [i]; + if (c > 126 || !allowed_chars [(int) c]) + return false; + } + + return true; + } + + static bool [] allowed_chars = new bool [126] { + false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, true, false, true, true, true, true, false, false, false, true, + true, false, true, true, false, true, true, true, true, true, true, true, true, true, true, false, + false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + false, true, false + }; + } +} diff --git a/websocket-sharp/Net/WebSocketContext.cs b/websocket-sharp/Net/WebSocketContext.cs new file mode 100644 index 00000000..55f86480 --- /dev/null +++ b/websocket-sharp/Net/WebSocketContext.cs @@ -0,0 +1,55 @@ +#region MIT License +/** + * WebSocketContext.cs + * + * The MIT License + * + * Copyright (c) 2012 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.Generic; +using System.Collections.Specialized; +using System.Security.Principal; + +namespace WebSocketSharp.Net { + + public abstract class WebSocketContext { + + protected WebSocketContext() + { + } + + public abstract CookieCollection CookieCollection { get; } + public abstract NameValueCollection Headers { get; } + public abstract bool IsAuthenticated { get; } + public abstract bool IsSecureConnection { get; } + public abstract bool IsLocal { get; } + public abstract string Origin { get; } + public abstract Uri RequestUri { get; } + public abstract string SecWebSocketKey { get; } + public abstract IEnumerable SecWebSocketProtocols { get; } + public abstract string SecWebSocketVersion { get; } + public abstract IPrincipal User { get; } + public abstract WebSocket WebSocket { get; } + } +} diff --git a/websocket-sharp/RequestHandshake.cs b/websocket-sharp/RequestHandshake.cs index 47ca9e7c..912e7a3b 100644 --- a/websocket-sharp/RequestHandshake.cs +++ b/websocket-sharp/RequestHandshake.cs @@ -27,36 +27,47 @@ #endregion using System; -using System.Net; using System.Collections.Specialized; using System.Text; +using WebSocketSharp.Net; namespace WebSocketSharp { public class RequestHandshake : Handshake { + #region Private Constructor + private RequestHandshake() { } - public RequestHandshake(string uri) + #endregion + + + #region Public Constructor + + public RequestHandshake(string uriString) { - Method = "GET"; - Uri = uri; - Version = "HTTP/1.1"; - Headers = new NameValueCollection(); + HttpMethod = "GET"; + RequestUri = uriString.ToUri(); AddHeader("Upgrade", "websocket"); AddHeader("Connection", "Upgrade"); } + #endregion + + #region Properties + + public string HttpMethod { get; private set; } + public bool IsWebSocketRequest { get { - if (Method != "GET") + if (HttpMethod != "GET") return false; - if (Version != "HTTP/1.1") + if (ProtocolVersion != HttpVersion.Version11) return false; if (!HeaderExists("Upgrade", "websocket")) @@ -78,8 +89,21 @@ namespace WebSocketSharp { } } - public string Method { get; private set; } - public string Uri { get; private set; } + public Uri RequestUri { get; private set; } + + #endregion + + #region Public Static Methods + + public static RequestHandshake Parse(WebSocketContext context) + { + return new RequestHandshake { + Headers = context.Headers, + HttpMethod = "GET", + RequestUri = context.RequestUri, + ProtocolVersion = HttpVersion.Version11 + }; + } public static RequestHandshake Parse(string[] request) { @@ -92,18 +116,22 @@ namespace WebSocketSharp { headers.Add(request[i]); return new RequestHandshake { - Headers = headers, - Method = requestLine[0], - Uri = requestLine[1], - Version = requestLine[2] + Headers = headers, + HttpMethod = requestLine[0], + RequestUri = requestLine[1].ToUri(), + ProtocolVersion = new Version(requestLine[2].Substring(5)) }; } + #endregion + + #region Public Method + public override string ToString() { var buffer = new StringBuilder(); - buffer.AppendFormat("{0} {1} {2}{3}", Method, Uri, Version, _crlf); + buffer.AppendFormat("{0} {1} HTTP/{2}{3}", HttpMethod, RequestUri, ProtocolVersion, _crlf); foreach (string key in Headers.AllKeys) buffer.AppendFormat("{0}: {1}{2}", key, Headers[key], _crlf); @@ -112,5 +140,7 @@ namespace WebSocketSharp { return buffer.ToString(); } + + #endregion } } diff --git a/websocket-sharp/ResponseHandshake.cs b/websocket-sharp/ResponseHandshake.cs index bda69941..41356219 100644 --- a/websocket-sharp/ResponseHandshake.cs +++ b/websocket-sharp/ResponseHandshake.cs @@ -28,31 +28,35 @@ using System; using System.Collections.Specialized; -using System.Net; using System.Text; +using WebSocketSharp.Net; namespace WebSocketSharp { public class ResponseHandshake : Handshake { + #region Public Constructor + public ResponseHandshake() { - Version = "HTTP/1.1"; - Status = "101"; - Reason = "Switching Protocols"; - Headers = new NameValueCollection(); + StatusCode = "101"; + Reason = "Switching Protocols"; AddHeader("Upgrade", "websocket"); AddHeader("Connection", "Upgrade"); } + #endregion + + #region Properties + public bool IsWebSocketResponse { get { - if (Version != "HTTP/1.1") + if (ProtocolVersion != HttpVersion.Version11) return false; - if (Status != "101") + if (StatusCode != "101") return false; if (!HeaderExists("Upgrade", "websocket")) @@ -68,8 +72,12 @@ namespace WebSocketSharp { } } - public string Reason { get; private set; } - public string Status { get; private set; } + public string Reason { get; private set; } + public string StatusCode { get; private set; } + + #endregion + + #region Public Static Methods public static ResponseHandshake Parse(string[] response) { @@ -86,18 +94,22 @@ namespace WebSocketSharp { headers.Add(response[i]); return new ResponseHandshake { - Headers = headers, - Reason = reason.ToString(), - Status = statusLine[1], - Version = statusLine[0] + Headers = headers, + Reason = reason.ToString(), + StatusCode = statusLine[1], + ProtocolVersion = new Version(statusLine[0].Substring(5)) }; } + #endregion + + #region Public Methods + public override string ToString() { var buffer = new StringBuilder(); - buffer.AppendFormat("{0} {1} {2}{3}", Version, Status, Reason, _crlf); + 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); @@ -106,5 +118,7 @@ namespace WebSocketSharp { return buffer.ToString(); } + + #endregion } } diff --git a/websocket-sharp/Server/HttpServer.cs b/websocket-sharp/Server/HttpServer.cs new file mode 100644 index 00000000..1d834801 --- /dev/null +++ b/websocket-sharp/Server/HttpServer.cs @@ -0,0 +1,187 @@ +#region MIT License +/** + * HttpServer.cs + * + * The MIT License + * + * Copyright (c) 2012 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.Configuration; +using System.IO; +using System.Threading; +using WebSocketSharp.Net; + +namespace WebSocketSharp.Server { + + public class HttpServer + where T : WebSocketService, new() + { + #region Fields + + private Thread _acceptRequestThread; + private HttpListener _listener; + private int _port; + private string _rootPath; + private Uri _wsPath; + private WebSocketServer _wsServer; + + #endregion + + #region Constructors + + public HttpServer() + : this(80) + { + } + + public HttpServer(int port) + : this(port, "/") + { + } + + public HttpServer(int port, string wsPath) + { + _listener = new HttpListener(); + _port = port; + var prefix = String.Format( + "http{0}://*:{1}/", _port == 443 ? "s" : String.Empty, _port); + _listener.Prefixes.Add(prefix); + _rootPath = ConfigurationManager.AppSettings["RootPath"]; + _wsPath = wsPath.ToUri(); + _wsServer = new WebSocketServer(); + } + + #endregion + + #region Property + + public int Port { + get { return _port; } + } + + #endregion + + #region Events + + public event EventHandler OnError; + public event EventHandler OnResponse; + + #endregion + + #region Private Methods + + private void acceptRequest() + { + while (true) + { + try + { + var context = _listener.GetContext(); + respond(context); + } + catch (HttpListenerException) + { + // HttpListener has been closed. + break; + } + catch (Exception ex) + { + OnError.Emit(this, new ErrorEventArgs(ex.Message)); + break; + } + } + } + + private void respond(HttpListenerContext context) + { + WaitCallback respondCb = (state) => + { + try + { + var req = context.Request; + var res = context.Response; + + if (req.IsWebSocketRequest) + { + upgradeToWebSocket(context); + } + else + { + OnResponse.Emit(this, new ResponseEventArgs(context)); + res.Close(); + } + } + catch (Exception ex) + { + OnError.Emit(this, new ErrorEventArgs(ex.Message)); + } + }; + ThreadPool.QueueUserWorkItem(respondCb); + } + + private void startAcceptRequestThread() + { + _acceptRequestThread = new Thread(new ThreadStart(acceptRequest)); + _acceptRequestThread.IsBackground = true; + _acceptRequestThread.Start(); + } + + private void upgradeToWebSocket(HttpListenerContext context) + { + var wsContext = context.AcceptWebSocket(); + var socket = wsContext.WebSocket; + if (_wsPath.ToString() != "/") + socket.Url = _wsPath; + _wsServer.BindWebSocket(socket); + } + + #endregion + + #region Public Methods + + public byte[] GetFile(string path) + { + var filePath = _rootPath + path; + if (File.Exists(filePath)) + return File.ReadAllBytes(filePath); + + return null; + } + + public void Start() + { + _listener.Start(); + startAcceptRequestThread(); + } + + public void Stop() + { + _listener.Close(); + _acceptRequestThread.Join(5 * 1000); + _wsServer.StopServices(); + } + + #endregion + } +} diff --git a/websocket-sharp/Server/ResponseEventArgs.cs b/websocket-sharp/Server/ResponseEventArgs.cs new file mode 100644 index 00000000..2dbe7fa1 --- /dev/null +++ b/websocket-sharp/Server/ResponseEventArgs.cs @@ -0,0 +1,43 @@ +#region MIT License +/** + * ResponseEventArgs.cs + * + * The MIT License + * + * Copyright (c) 2012 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 WebSocketSharp.Net; + +namespace WebSocketSharp.Server { + + public class ResponseEventArgs : EventArgs + { + public HttpListenerContext Context { get; private set; } + + public ResponseEventArgs(HttpListenerContext context) + { + Context = context; + } + } +} diff --git a/websocket-sharp/Server/WebSocketServer.cs b/websocket-sharp/Server/WebSocketServer.cs index c55a4d95..a3b9e7b8 100644 --- a/websocket-sharp/Server/WebSocketServer.cs +++ b/websocket-sharp/Server/WebSocketServer.cs @@ -42,16 +42,27 @@ namespace WebSocketSharp.Server { public class WebSocketServer where T : WebSocketService, new() { - #region Private Fields + #region Fields private Thread _acceptClientThread; + private bool _isSelfHost; private Dictionary _services; private TcpListener _tcpListener; private Uri _uri; #endregion - #region Constructors + #region Internal Constructor + + internal WebSocketServer() + { + _services = new Dictionary(); + _isSelfHost = false; + } + + #endregion + + #region Public Constructors public WebSocketServer(string url) { @@ -81,6 +92,7 @@ namespace WebSocketSharp.Server { _tcpListener = new TcpListener(ips[0], port); _services = new Dictionary(); + _isSelfHost = true; } public WebSocketServer(int port) @@ -97,6 +109,7 @@ namespace WebSocketSharp.Server { _tcpListener = new TcpListener(IPAddress.Any, port); _services = new Dictionary(); + _isSelfHost = true; } #endregion @@ -113,6 +126,10 @@ namespace WebSocketSharp.Server { get { return (IPEndPoint)_tcpListener.LocalEndpoint; } } + public bool IsSelfHost { + get { return _isSelfHost; } + } + public int Port { get { return Endpoint.Port; } @@ -209,14 +226,21 @@ namespace WebSocketSharp.Server { public void Start() { + if (!_isSelfHost) + return; + _tcpListener.Start(); startAcceptClientThread(); } public void Stop() { - _tcpListener.Stop(); - _acceptClientThread.Join(5 * 1000); + if (_isSelfHost) + { + _tcpListener.Stop(); + _acceptClientThread.Join(5 * 1000); + } + StopServices(); } diff --git a/websocket-sharp/WebSocket.cs b/websocket-sharp/WebSocket.cs index 74df5b35..8e581edf 100644 --- a/websocket-sharp/WebSocket.cs +++ b/websocket-sharp/WebSocket.cs @@ -46,6 +46,7 @@ using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using WebSocketSharp.Frame; +using WebSocketSharp.Net; namespace WebSocketSharp { @@ -62,6 +63,7 @@ namespace WebSocketSharp private string _base64key; private string _binaryType; + private HttpListenerWebSocketContext _context; private IPEndPoint _endPoint; private AutoResetEvent _exitedMessageLoop; private string _extensions; @@ -103,6 +105,15 @@ namespace WebSocketSharp #region Internal Constructor + internal WebSocket(HttpListenerWebSocketContext context) + : this() + { + _uri = new Uri("/", UriKind.Relative); + _context = context; + _isClient = false; + _isSecure = _context.IsSecureConnection; + } + internal WebSocket(Uri uri, TcpClient tcpClient) : this() { @@ -219,8 +230,14 @@ namespace WebSocketSharp get { return _unTransmittedBuffer; } } - public string Url { - get { return _uri.ToString(); } + public Uri Url { + get { return _uri; } + set { + if (_readyState != WsState.CONNECTING || _isClient) + return; + + _uri = value; + } } #endregion @@ -260,7 +277,7 @@ namespace WebSocketSharp private void close(PayloadData data) { #if DEBUG - Console.WriteLine("WS: Info@close: Current thread IsBackground?: {0}", Thread.CurrentThread.IsBackground); + Console.WriteLine("WS: Info@close: Current thread IsBackground ?: {0}", Thread.CurrentThread.IsBackground); #endif lock(_forClose) { @@ -278,7 +295,7 @@ namespace WebSocketSharp ReadyState = WsState.CLOSING; } - + OnClose.Emit(this, new CloseEventArgs(data)); var frame = createFrame(Fin.FINAL, Opcode.CLOSE, data); closeHandshake(frame); @@ -311,12 +328,20 @@ namespace WebSocketSharp private void closeConnection() { + if (_context != null) + { + _context.BaseContext.Response.Close(); + _wsStream = null; + _context = null; + } + if (_wsStream != null) { _wsStream.Dispose(); _wsStream = null; } - else if (_netStream != null) + + if (_netStream != null) { _netStream.Dispose(); _netStream = null; @@ -441,6 +466,31 @@ namespace WebSocketSharp } private void createServerStream() + { + if (_context != null) + { + _wsStream = createServerStreamFromContext(); + return; + } + + if (_tcpClient != null) + { + _wsStream = createServerStreamFromTcpClient(); + return; + } + } + + private IWsStream createServerStreamFromContext() + { + var stream = _context.BaseContext.Connection.Stream; + + if (IsSecure) + return new WsStream((SslStream)stream); + + return new WsStream((NetworkStream)stream); + } + + private IWsStream createServerStreamFromTcpClient() { _netStream = _tcpClient.GetStream(); @@ -448,15 +498,13 @@ namespace WebSocketSharp { _sslStream = new SslStream(_netStream); - string certPath = ConfigurationManager.AppSettings["ServerCertPath"]; + var certPath = ConfigurationManager.AppSettings["ServerCertPath"]; _sslStream.AuthenticateAsServer(new X509Certificate(certPath)); - _wsStream = new WsStream(_sslStream); - - return; + return new WsStream(_sslStream); } - _wsStream = new WsStream(_netStream); + return new WsStream(_netStream); } private void doHandshake() @@ -492,7 +540,7 @@ namespace WebSocketSharp { if (!request.IsWebSocketRequest) { - message = "Not WebSocket request."; + message = "Invalid WebSocket request."; return false; } @@ -504,18 +552,12 @@ namespace WebSocketSharp }; }; + if (!isValidRequestUri(request.RequestUri, func("Request URI"), out message)) + return false; + if (_uri.IsAbsoluteUri) - { - if (_uri.PathAndQuery.NotEqualsDo(request.Uri, func("Request URI"), out message, false)) - return false; - if (!isValidRequestHost(request.GetHeaderValues("Host")[0], func("Host"), out message)) return false; - } - - if (!_uri.IsAbsoluteUri) - if (_uri.ToString().NotEqualsDo(request.Uri, func("Request URI"), out message, false)) - return false; if (!request.HeaderExists("Sec-WebSocket-Version", _version)) { @@ -556,11 +598,33 @@ namespace WebSocketSharp return true; } + private bool isValidRequestUri(Uri requestUri, Func func, out string message) + { + if (_uri.IsAbsoluteUri && requestUri.IsAbsoluteUri) + if (_uri.ToString().NotEqualsDo(requestUri.ToString(), func, out message, false)) + return false; + + if (_uri.IsAbsoluteUri && !requestUri.IsAbsoluteUri) + if (_uri.PathAndQuery.NotEqualsDo(requestUri.ToString(), func, out message, false)) + return false; + + if (!_uri.IsAbsoluteUri && requestUri.IsAbsoluteUri) + if (_uri.ToString().NotEqualsDo(requestUri.PathAndQuery, func, out message, false)) + return false; + + if (!_uri.IsAbsoluteUri && !requestUri.IsAbsoluteUri) + if (_uri.ToString().NotEqualsDo(requestUri.ToString(), func, out message, false)) + return false; + + message = String.Empty; + return true; + } + private bool isValidResponse(ResponseHandshake response, out string message) { if (!response.IsWebSocketResponse) { - message = "Not WebSocket response."; + message = "Invalid WebSocket response."; return false; } @@ -719,7 +783,7 @@ namespace WebSocketSharp else { #if DEBUG - Console.WriteLine("WS: Info@receive: Client starts closing handshake."); + Console.WriteLine("WS: Info@receive: Start closing handshake."); #endif close(CloseStatusCode.INCORRECT_DATA, String.Empty); return null; @@ -733,7 +797,7 @@ namespace WebSocketSharp else if (frame.Opcode == Opcode.CLOSE) {// FINAL & CLOSE #if DEBUG - Console.WriteLine("WS: Info@receive: Server starts closing handshake."); + Console.WriteLine("WS: Info@receive: Start closing handshake."); #endif close(frame.PayloadData); return null; @@ -753,7 +817,7 @@ namespace WebSocketSharp else {// FINAL & (TEXT | BINARY) #if DEBUG - Console.WriteLine("WS: Info@receive: Client starts closing handshake."); + Console.WriteLine("WS: Info@receive: Start closing handshake."); #endif close(CloseStatusCode.INCORRECT_DATA, String.Empty); return null; @@ -773,7 +837,7 @@ namespace WebSocketSharp goto default; case Opcode.CLOSE: #if DEBUG - Console.WriteLine("WS: Info@receive: Server starts closing handshake."); + Console.WriteLine("WS: Info@receive: Start closing handshake."); #endif close(payloadData); break; @@ -796,6 +860,9 @@ namespace WebSocketSharp private RequestHandshake receiveOpeningHandshake() { + if (_context != null) + return RequestHandshake.Parse(_context); + return RequestHandshake.Parse(readHandshake()); } diff --git a/websocket-sharp/bin/Debug/websocket-sharp.dll b/websocket-sharp/bin/Debug/websocket-sharp.dll index 0901b46b..3950c3fe 100755 Binary files a/websocket-sharp/bin/Debug/websocket-sharp.dll and b/websocket-sharp/bin/Debug/websocket-sharp.dll differ diff --git a/websocket-sharp/bin/Debug/websocket-sharp.dll.mdb b/websocket-sharp/bin/Debug/websocket-sharp.dll.mdb index 615a3a20..f3c03973 100644 Binary files a/websocket-sharp/bin/Debug/websocket-sharp.dll.mdb and b/websocket-sharp/bin/Debug/websocket-sharp.dll.mdb differ diff --git a/websocket-sharp/bin/Debug_Ubuntu/websocket-sharp.dll b/websocket-sharp/bin/Debug_Ubuntu/websocket-sharp.dll index 764c252c..b2c76b4d 100755 Binary files a/websocket-sharp/bin/Debug_Ubuntu/websocket-sharp.dll and b/websocket-sharp/bin/Debug_Ubuntu/websocket-sharp.dll differ diff --git a/websocket-sharp/bin/Debug_Ubuntu/websocket-sharp.dll.mdb b/websocket-sharp/bin/Debug_Ubuntu/websocket-sharp.dll.mdb index f63a5858..86949826 100644 Binary files a/websocket-sharp/bin/Debug_Ubuntu/websocket-sharp.dll.mdb and b/websocket-sharp/bin/Debug_Ubuntu/websocket-sharp.dll.mdb differ diff --git a/websocket-sharp/bin/Release/websocket-sharp.dll b/websocket-sharp/bin/Release/websocket-sharp.dll index cae2760e..d33d5220 100755 Binary files a/websocket-sharp/bin/Release/websocket-sharp.dll and b/websocket-sharp/bin/Release/websocket-sharp.dll differ diff --git a/websocket-sharp/bin/Release_Ubuntu/websocket-sharp.dll b/websocket-sharp/bin/Release_Ubuntu/websocket-sharp.dll index d8d1e088..4160d674 100755 Binary files a/websocket-sharp/bin/Release_Ubuntu/websocket-sharp.dll and b/websocket-sharp/bin/Release_Ubuntu/websocket-sharp.dll differ diff --git a/websocket-sharp/websocket-sharp.csproj b/websocket-sharp/websocket-sharp.csproj index 1d6f607e..26785bd7 100644 --- a/websocket-sharp/websocket-sharp.csproj +++ b/websocket-sharp/websocket-sharp.csproj @@ -79,10 +79,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/websocket-sharp/websocket-sharp.pidb b/websocket-sharp/websocket-sharp.pidb index 7f9fb8fb..22781a1e 100644 Binary files a/websocket-sharp/websocket-sharp.pidb and b/websocket-sharp/websocket-sharp.pidb differ