├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── WebSocketProxy.Sample ├── App.config ├── HelloWorldController.cs ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── Resources │ └── Content │ │ └── index.html ├── WebSocketProxy.Sample.csproj └── packages.config ├── WebSocketProxy.sln └── WebSocketProxy ├── Host.cs ├── HttpPacket.cs ├── HttpPacketBuilder.cs ├── Logger.cs ├── Properties └── AssemblyInfo.cs ├── TcpConnectionManager.cs ├── TcpHost.cs ├── TcpProxyConfiguration.cs ├── TcpProxyServer.cs ├── TcpRoute.cs ├── TcpStateContainer.cs ├── WebSocketHeaders.cs ├── WebSocketProxy.csproj └── WebSocketProxy.nuspec /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | *_i.c 39 | *_p.c 40 | *_i.h 41 | *.ilk 42 | *.meta 43 | *.obj 44 | *.pch 45 | *.pdb 46 | *.pgc 47 | *.pgd 48 | *.rsp 49 | *.sbr 50 | *.tlb 51 | *.tli 52 | *.tlh 53 | *.tmp 54 | *.tmp_proj 55 | *.log 56 | *.vspscc 57 | *.vssscc 58 | .builds 59 | *.pidb 60 | *.svclog 61 | *.scc 62 | 63 | # Visual Studio profiler 64 | *.psess 65 | *.vsp 66 | *.vspx 67 | *.sap 68 | 69 | # TFS 2012 Local Workspace 70 | $tf/ 71 | 72 | # Guidance Automation Toolkit 73 | *.gpState 74 | 75 | # ReSharper is a .NET coding add-in 76 | _ReSharper*/ 77 | *.[Rr]e[Ss]harper 78 | *.DotSettings.user 79 | 80 | # JustCode is a .NET coding add-in 81 | .JustCode 82 | 83 | # TeamCity is a build add-in 84 | _TeamCity* 85 | 86 | # DotCover is a Code Coverage Tool 87 | *.dotCover 88 | 89 | # NCrunch 90 | _NCrunch_* 91 | .*crunch*.local.xml 92 | nCrunchTemp_* 93 | 94 | # MightyMoose 95 | *.mm.* 96 | AutoTest.Net/ 97 | 98 | # Web workbench (sass) 99 | .sass-cache/ 100 | 101 | # Installshield output folder 102 | [Ee]xpress/ 103 | 104 | # DocProject is a documentation generator add-in 105 | DocProject/buildhelp/ 106 | DocProject/Help/*.HxT 107 | DocProject/Help/*.HxC 108 | DocProject/Help/*.hhc 109 | DocProject/Help/*.hhk 110 | DocProject/Help/*.hhp 111 | DocProject/Help/Html2 112 | DocProject/Help/html 113 | 114 | # Click-Once directory 115 | publish/ 116 | 117 | # Publish Web Output 118 | *.[Pp]ublish.xml 119 | *.azurePubxml 120 | # TODO: Comment the next line if you want to checkin your web deploy settings 121 | # but database connection strings (with potential passwords) will be unencrypted 122 | *.pubxml 123 | *.publishproj 124 | 125 | # NuGet Packages 126 | *.nupkg 127 | # The packages folder can be ignored because of Package Restore 128 | **/packages/* 129 | # except build/, which is used as an MSBuild target. 130 | !**/packages/build/ 131 | # Uncomment if necessary however generally it will be regenerated when needed 132 | #!**/packages/repositories.config 133 | 134 | # Windows Azure Build Output 135 | csx/ 136 | *.build.csdef 137 | 138 | # Windows Store app package directory 139 | AppPackages/ 140 | 141 | # Visual Studio cache files 142 | # files ending in .cache can be ignored 143 | *.[Cc]ache 144 | # but keep track of directories ending in .cache 145 | !*.[Cc]ache/ 146 | 147 | # Others 148 | ClientBin/ 149 | [Ss]tyle[Cc]op.* 150 | ~$* 151 | *~ 152 | *.dbmdl 153 | *.dbproj.schemaview 154 | *.pfx 155 | *.publishsettings 156 | node_modules/ 157 | orleans.codegen.cs 158 | 159 | # RIA/Silverlight projects 160 | Generated_Code/ 161 | 162 | # Backup & report files from converting an old project file 163 | # to a newer Visual Studio version. Backup files are not needed, 164 | # because we have git ;-) 165 | _UpgradeReport_Files/ 166 | Backup*/ 167 | UpgradeLog*.XML 168 | UpgradeLog*.htm 169 | 170 | # SQL Server files 171 | *.mdf 172 | *.ldf 173 | 174 | # Business Intelligence projects 175 | *.rdl.data 176 | *.bim.layout 177 | *.bim_*.settings 178 | 179 | # Microsoft Fakes 180 | FakesAssemblies/ 181 | 182 | # Node.js Tools for Visual Studio 183 | .ntvs_analysis.dat 184 | 185 | # Visual Studio 6 build log 186 | *.plg 187 | 188 | # Visual Studio 6 workspace options file 189 | *.opt 190 | 191 | # Visual Studio LightSwitch build output 192 | **/*.HTMLClient/GeneratedArtifacts 193 | **/*.DesktopClient/GeneratedArtifacts 194 | **/*.DesktopClient/ModelManifest.xml 195 | **/*.Server/GeneratedArtifacts 196 | **/*.Server/ModelManifest.xml 197 | _Pvt_Extensions 198 | 199 | # Application Specific 200 | WebSocketProxy.NuGetPackage 201 | 202 | # Ignore all bin and obj directories 203 | /**/bin/* 204 | /**/obj/* 205 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | solution: WebSocketProxy.sln 3 | sudo: required 4 | script: 5 | - xbuild /p:Configuration=Release WebSocketProxy.sln 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Life Emotions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WebSocketProxy ![build status](https://travis-ci.org/lifeemotions/websocketproxy.svg?branch=master) [![NuGet](https://img.shields.io/nuget/v/WebSocketProxy.svg?maxAge=2592000)]() 2 | ======= 3 | 4 | WebSocketProxy is a lightweight C# library which allows you to connect to an HTTP server and websocket server through the same TCP port. 5 | This library is independent on which web frameworks are used for handling websocket and http requests. 6 | 7 | # Installation 8 | 9 | Installation can be done using NuGet. 10 | 11 | ``` 12 | Install-Package WebSocketProxy 13 | ``` 14 | 15 | # Sample Usage 16 | The project "WebSocket.Sample" contains a sample usage using [NancyFX](https://github.com/NancyFx/Nancy) and [Fleck](https://github.com/statianzo/Fleck). 17 | 18 | The first step is to build the configuration object in which you set the endpoints that are listening for HTTP (Nancy) and WebSocket requests (Fleck) and the public endpoint which will be listening to both kinds of requests. 19 | ```csharp 20 | TcpProxyConfiguration configuration = new TcpProxyConfiguration() 21 | { 22 | PublicHost = new Host() 23 | { 24 | IpAddress = IPAddress.Parse("0.0.0.0"), 25 | Port = 8080 26 | }, 27 | HttpHost = new Host() 28 | { 29 | IpAddress = IPAddress.Loopback, 30 | Port = 8081 31 | }, 32 | WebSocketHost = new Host() 33 | { 34 | 35 | IpAddress = IPAddress.Loopback, 36 | Port = 8082 37 | } 38 | }; 39 | 40 | ``` 41 | Then, initialize Nancy and Fleck, followed by the WebSocketServer. 42 | 43 | ```csharp 44 | using (var nancyHost = new NancyHost(new Uri("http://localhost:8081"))) 45 | using (var websocketServer = new WebSocketServer("ws://0.0.0.0:8082")) 46 | using (var tcpProxy = new TcpProxyServer(configuration)) 47 | { 48 | // Initialize Nancy 49 | nancyHost.Start(); 50 | 51 | // Initialize Fleck 52 | websocketServer.Start(connection => 53 | { 54 | connection.OnOpen = () => Console.WriteLine("Connection on open"); 55 | connection.OnClose = () => Console.WriteLine("Connection on close"); 56 | connection.OnMessage = message => Console.WriteLine("Message: " + message); 57 | }); 58 | 59 | // Initialize the proxy 60 | tcpProxy.Start(); 61 | 62 | Console.WriteLine("Press [Enter] to stop"); 63 | Console.ReadLine(); 64 | } 65 | ``` 66 | By pointing the web browser to the port 8080, you will receive an html page which initializes a websocket connection. 67 | 68 | # SSL Support 69 | This library supports HTTPS and WSS. Simply pass the certificate path and password in the configuration object and all incoming requests will be unencrypted and routed to the corresponding library. 70 | 71 | -------------------------------------------------------------------------------- /WebSocketProxy.Sample/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /WebSocketProxy.Sample/HelloWorldController.cs: -------------------------------------------------------------------------------- 1 | using Nancy; 2 | 3 | namespace WebSocketProxy.Sample 4 | { 5 | public class HelloWorldController : NancyModule 6 | { 7 | public HelloWorldController() 8 | { 9 | Get["/"] = v => View["Content/index.html"]; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /WebSocketProxy.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using Fleck; 4 | using Nancy.Hosting.Self; 5 | 6 | namespace WebSocketProxy.Sample 7 | { 8 | class Program 9 | { 10 | static void Main(string[] args) 11 | { 12 | TcpProxyConfiguration configuration = new TcpProxyConfiguration() 13 | { 14 | PublicHost = new Host() 15 | { 16 | IpAddress = IPAddress.Parse("0.0.0.0"), 17 | Port = 8080 18 | }, 19 | HttpHost = new Host() 20 | { 21 | IpAddress = IPAddress.Loopback, 22 | Port = 8081 23 | }, 24 | WebSocketHost = new Host() 25 | { 26 | 27 | IpAddress = IPAddress.Loopback, 28 | Port = 8082 29 | } 30 | }; 31 | 32 | 33 | using (var nancyHost = new NancyHost(new Uri("http://localhost:8081"))) 34 | using (var websocketServer = new WebSocketServer("ws://0.0.0.0:8082")) 35 | using (var tcpProxy = new TcpProxyServer(configuration)) 36 | { 37 | nancyHost.Start(); 38 | websocketServer.Start(connection => 39 | { 40 | connection.OnOpen = () => Console.WriteLine("COnnection on open"); 41 | connection.OnClose = () => Console.WriteLine("Connection on close"); 42 | connection.OnMessage = message => Console.WriteLine("Message: " + message); 43 | }); 44 | 45 | tcpProxy.Start(); 46 | 47 | Console.WriteLine("Press [Enter] to stop"); 48 | Console.ReadLine(); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /WebSocketProxy.Sample/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("WebSocketProxy.Sample")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("WebSocketProxy.Sample")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("dd602a26-f8ca-407c-ba30-7e1fd01c6ec7")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /WebSocketProxy.Sample/Resources/Content/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebSocketProxy Hello World 6 | 7 | 8 | 9 |

This is the WebSocket Proxy Hello World Page

10 | 11 | 12 | 33 | 34 | -------------------------------------------------------------------------------- /WebSocketProxy.Sample/WebSocketProxy.Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {DD602A26-F8CA-407C-BA30-7E1FD01C6EC7} 8 | Exe 9 | Properties 10 | WebSocketProxy.Sample 11 | WebSocketProxy.Sample 12 | v4.5 13 | 512 14 | true 15 | 16 | 17 | 18 | AnyCPU 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | AnyCPU 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | Always 38 | 39 | 40 | 41 | ..\packages\Fleck.0.13.0.57\lib\net40\Fleck.dll 42 | True 43 | 44 | 45 | ..\packages\Nancy.1.2.0\lib\net40\Nancy.dll 46 | True 47 | 48 | 49 | ..\packages\Nancy.Hosting.Self.1.2.0\lib\net40\Nancy.Hosting.Self.dll 50 | True 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | {BDA44986-B4D2-43E2-9486-CAC4F62EEB4F} 73 | WebSocketProxy 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 91 | -------------------------------------------------------------------------------- /WebSocketProxy.Sample/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /WebSocketProxy.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.23107.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSocketProxy", "WebSocketProxy\WebSocketProxy.csproj", "{BDA44986-B4D2-43E2-9486-CAC4F62EEB4F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSocketProxy.Sample", "WebSocketProxy.Sample\WebSocketProxy.Sample.csproj", "{DD602A26-F8CA-407C-BA30-7E1FD01C6EC7}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {BDA44986-B4D2-43E2-9486-CAC4F62EEB4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {BDA44986-B4D2-43E2-9486-CAC4F62EEB4F}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {BDA44986-B4D2-43E2-9486-CAC4F62EEB4F}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {BDA44986-B4D2-43E2-9486-CAC4F62EEB4F}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {DD602A26-F8CA-407C-BA30-7E1FD01C6EC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {DD602A26-F8CA-407C-BA30-7E1FD01C6EC7}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {DD602A26-F8CA-407C-BA30-7E1FD01C6EC7}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {DD602A26-F8CA-407C-BA30-7E1FD01C6EC7}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /WebSocketProxy/Host.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace WebSocketProxy 4 | { 5 | public class Host 6 | { 7 | public Host() 8 | { 9 | Port = -1; 10 | } 11 | 12 | public IPAddress IpAddress { get; set; } 13 | 14 | public int Port { get; set; } 15 | 16 | public bool IsSpecified 17 | { 18 | get 19 | { 20 | return Port != -1 && IpAddress != null; 21 | } 22 | } 23 | 24 | public override string ToString() 25 | { 26 | return IpAddress + ":" + Port; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /WebSocketProxy/HttpPacket.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace WebSocketProxy 6 | { 7 | /// 8 | /// Simplified HttpPacket representation 9 | /// 10 | internal class HttpPacket 11 | { 12 | private readonly IDictionary _headers; 13 | 14 | public HttpPacket() 15 | { 16 | _headers = new Dictionary(); 17 | } 18 | 19 | public void AddHeader(string fieldName, string fieldValue) 20 | { 21 | if (string.IsNullOrWhiteSpace(fieldName) || string.IsNullOrWhiteSpace(fieldValue)) 22 | return; 23 | 24 | // Http Headers can be repeated per RFC2616 25 | if (_headers.ContainsKey(fieldName)) 26 | { 27 | _headers[fieldName] += ',' + fieldValue; 28 | } 29 | else 30 | { 31 | _headers.Add(fieldName, fieldValue); 32 | } 33 | } 34 | 35 | public string GetHeaderOrDefault(string fieldName, string defaultValue) 36 | { 37 | if (string.IsNullOrWhiteSpace(fieldName)) return defaultValue; 38 | 39 | string result; 40 | return !_headers.TryGetValue(fieldName, out result) ? defaultValue : result; 41 | } 42 | 43 | public bool IsWebSocketPacket 44 | { 45 | get { return _headers.Keys.Any(headerName => WebSocketHeaders.ClientHeaders.Contains(headerName.ToLowerInvariant().Trim())); } 46 | } 47 | 48 | public override string ToString() 49 | { 50 | StringBuilder sb = new StringBuilder(); 51 | foreach (string key in _headers.Keys) 52 | { 53 | sb.Append(key); 54 | sb.Append(" : "); 55 | sb.Append(_headers[key]); 56 | sb.Append("\n"); 57 | } 58 | 59 | return string.Format("HttpPacket: \n{0}\n", sb); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /WebSocketProxy/HttpPacketBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebSocketProxy 4 | { 5 | internal class HttpPacketBuilder 6 | { 7 | public static HttpPacket BuildPacket(string data) 8 | { 9 | if (string.IsNullOrWhiteSpace(data)) 10 | return null; 11 | 12 | try 13 | { 14 | HttpPacket result = new HttpPacket(); 15 | 16 | string[] splitted = data.Split('\n'); 17 | 18 | // There are no headers... nothing to do here... 19 | if (splitted.Length < 2) 20 | return null; 21 | 22 | for (int i = 1; i < splitted.Length; i++) 23 | { 24 | string header = splitted[i]; 25 | 26 | // End of HTTP headers (newline) 27 | if (string.IsNullOrWhiteSpace(header)) 28 | break; 29 | 30 | string[] splittedHeader = header.Split(':'); 31 | 32 | if (splittedHeader.Length < 2) 33 | continue; 34 | 35 | // header name 36 | string fieldName = splittedHeader[0].Trim().ToLowerInvariant(); 37 | 38 | // header value 39 | string value = string.Empty; 40 | for (int j = 1; j < splittedHeader.Length; j++) 41 | { 42 | value = value + splittedHeader[j]; 43 | } 44 | 45 | result.AddHeader(fieldName, value.Trim()); 46 | } 47 | 48 | return result; 49 | 50 | } 51 | catch (Exception) 52 | { 53 | return null; 54 | } 55 | } 56 | 57 | } 58 | } -------------------------------------------------------------------------------- /WebSocketProxy/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebSocketProxy 4 | { 5 | public class Logger 6 | { 7 | public Action Info { get; set; } 8 | 9 | public Action Error { get; set; } 10 | 11 | public Logger() 12 | { 13 | Info = (message) => Console.WriteLine("{0}:{1}", DateTime.Now, message); 14 | Error = (message, error) => Console.WriteLine("{0}:{1} - {2}", 15 | DateTime.Now, message, error != null ? error.Message : string.Empty); 16 | } 17 | 18 | public void Log(string message) 19 | { 20 | if (Info != null && !string.IsNullOrWhiteSpace(message)) 21 | { 22 | Info(message); 23 | } 24 | } 25 | 26 | public void Warn(string message, Exception e) 27 | { 28 | if (Error != null && !string.IsNullOrWhiteSpace(message)) 29 | { 30 | Error(message, e); 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /WebSocketProxy/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("WebSocketProxy")] 8 | [assembly: AssemblyDescription("WebSocket Proxy acts as gateway for HTTP and WebSocket servers")] 9 | [assembly: AssemblyCompany("life.emotions")] 10 | [assembly: AssemblyProduct("SwaggerWcf")] 11 | [assembly: AssemblyCopyright("Copyright © 2015")] 12 | [assembly: AssemblyTrademark("")] 13 | [assembly: AssemblyCulture("")] 14 | 15 | #if DEBUG 16 | [assembly: AssemblyConfiguration("Debug")] 17 | #else 18 | [assembly: AssemblyConfiguration("Release")] 19 | #endif 20 | 21 | // Setting ComVisible to false makes the types in this assembly not visible 22 | // to COM components. If you need to access a type in this assembly from 23 | // COM, set the ComVisible attribute to true on that type. 24 | [assembly: ComVisible(false)] 25 | 26 | // The following GUID is for the ID of the typelib if this project is exposed to COM 27 | [assembly: Guid("54d57407-92ae-4f22-b327-65e4c79ae515")] 28 | 29 | // Version information for an assembly consists of the following four values: 30 | // 31 | // Major Version 32 | // Minor Version 33 | // Build Number 34 | // Revision 35 | // 36 | // You can specify all the values or you can default the Build and Revision Numbers 37 | // by using the '*' as shown below: 38 | // [assembly: AssemblyVersion("1.0.*")] 39 | [assembly: AssemblyVersion("1.0.2")] 40 | [assembly: AssemblyFileVersion("1.0.2")] 41 | [assembly: AssemblyInformationalVersion("1.0.2")] 42 | -------------------------------------------------------------------------------- /WebSocketProxy/TcpConnectionManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace WebSocketProxy 4 | { 5 | internal class TcpConnectionManager 6 | { 7 | private readonly ICollection _connections; 8 | 9 | public TcpConnectionManager() 10 | { 11 | _connections = new List(); 12 | } 13 | 14 | public int ConnectionCount 15 | { 16 | get 17 | { 18 | lock(_connections) 19 | { 20 | return _connections.Count; 21 | } 22 | } 23 | } 24 | 25 | public void AddRoute(TcpRoute route) 26 | { 27 | if (route == null || !route.Connected) return; 28 | 29 | lock (_connections) 30 | { 31 | route.Disconnected += route_Disconnected; 32 | _connections.Add(route); 33 | 34 | route.Start(); 35 | } 36 | } 37 | 38 | void route_Disconnected(TcpRoute route) 39 | { 40 | if (route == null) return; 41 | 42 | lock (_connections) 43 | { 44 | route.Disconnected -= route_Disconnected; 45 | _connections.Remove(route); 46 | } 47 | } 48 | 49 | } 50 | } -------------------------------------------------------------------------------- /WebSocketProxy/TcpHost.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Net.Security; 5 | using System.Net.Sockets; 6 | using System.Security.Authentication; 7 | using System.Security.Cryptography.X509Certificates; 8 | using System.Threading.Tasks; 9 | 10 | namespace WebSocketProxy 11 | { 12 | internal class TcpHost : IDisposable 13 | { 14 | private bool _closed; 15 | 16 | #region Events 17 | 18 | public delegate void DataAvailableDelegate(TcpHost host, byte[] data, int length); 19 | 20 | public event DataAvailableDelegate DataAvailable; 21 | 22 | protected void OnDataAvailable(byte[] data, int length) 23 | { 24 | if (DataAvailable != null) 25 | { 26 | DataAvailable(this, data, length); 27 | } 28 | } 29 | 30 | public delegate void DisconnectedDelegate(TcpHost host); 31 | 32 | public event DisconnectedDelegate Disconnected; 33 | 34 | protected void OnDisconnected() 35 | { 36 | if (Disconnected != null) 37 | { 38 | Disconnected(this); 39 | } 40 | } 41 | 42 | #endregion 43 | 44 | private const int DefaultBufferLength = 4 * 1024; 45 | 46 | private readonly TcpClient _tcpClient; 47 | private readonly byte[] _buffer; 48 | 49 | public Stream Stream { get; private set; } 50 | 51 | public bool Connected 52 | { 53 | get { return _tcpClient.Connected; } 54 | } 55 | 56 | #region Constructors 57 | 58 | public static TcpHost ManufactureDefault(IPAddress address, int port) 59 | { 60 | return new TcpHost(new TcpClient(address.ToString(), port)); 61 | } 62 | 63 | public TcpHost(TcpClient tcpClient) 64 | { 65 | _buffer = new byte[DefaultBufferLength]; 66 | _tcpClient = tcpClient; 67 | Stream = _tcpClient.GetStream(); 68 | } 69 | 70 | 71 | #endregion 72 | 73 | 74 | public async Task AuthenticateAsync(X509Certificate2 certificate) 75 | { 76 | if (certificate == null) 77 | return; 78 | 79 | SslStream sslStream = new SslStream(Stream, false); 80 | Stream = sslStream; 81 | await sslStream.AuthenticateAsServerAsync(certificate); 82 | } 83 | 84 | public void BeginAuthenticationAsServer(X509Certificate2 certificate, SslProtocols protocols, AsyncCallback callback, object state) 85 | { 86 | SslStream sslStream = new SslStream(Stream, false); 87 | Stream = sslStream; 88 | sslStream.BeginAuthenticateAsServer(certificate, false, protocols, false, callback, state); 89 | } 90 | 91 | public void EndAuthenticationAsServer(IAsyncResult asyncResult) 92 | { 93 | SslStream sslStream = Stream as SslStream; 94 | if (sslStream != null) 95 | { 96 | sslStream.EndAuthenticateAsServer(asyncResult); 97 | } 98 | 99 | Stream = sslStream; 100 | } 101 | 102 | public void Send(byte[] data, int length) 103 | { 104 | try 105 | { 106 | if (!_closed) 107 | { 108 | Stream.Write(data, 0, length); 109 | } 110 | 111 | } 112 | catch (Exception) 113 | { 114 | OnDisconnected(); 115 | } 116 | 117 | } 118 | 119 | public void StartReading() 120 | { 121 | try 122 | { 123 | if (!_closed) 124 | { 125 | Stream.BeginRead(_buffer, 0, _buffer.Length, ReadAsyncCallback, Stream); 126 | } 127 | } 128 | catch (Exception) 129 | { 130 | OnDisconnected(); 131 | } 132 | 133 | } 134 | 135 | private void ReadAsyncCallback(IAsyncResult asyncResult) 136 | { 137 | if (_tcpClient.Connected && !_closed) 138 | { 139 | try 140 | { 141 | int numberOfBytesRead = Stream.EndRead(asyncResult); 142 | 143 | // No more data to read. Close the connection 144 | if (numberOfBytesRead > 0) 145 | { 146 | OnDataAvailable(_buffer, numberOfBytesRead); 147 | StartReading(); 148 | } 149 | else 150 | { 151 | Close(); 152 | OnDisconnected(); 153 | } 154 | 155 | 156 | } 157 | catch (IOException) 158 | { 159 | OnDisconnected(); 160 | } 161 | catch (ObjectDisposedException) 162 | { 163 | // 164 | } 165 | } 166 | else 167 | { 168 | OnDisconnected(); 169 | } 170 | } 171 | 172 | public void Close() 173 | { 174 | try 175 | { 176 | if (_tcpClient == null || !_tcpClient.Connected) return; 177 | 178 | _closed = true; 179 | _tcpClient.Close(); 180 | } 181 | catch (Exception) 182 | { 183 | // 184 | } 185 | 186 | } 187 | 188 | public void Dispose() 189 | { 190 | Close(); 191 | } 192 | } 193 | } -------------------------------------------------------------------------------- /WebSocketProxy/TcpProxyConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Authentication; 2 | 3 | namespace WebSocketProxy 4 | { 5 | public class TcpProxyConfiguration 6 | { 7 | private const SslProtocols DefaultProtocols = SslProtocols.Ssl2 | SslProtocols.Ssl3 | SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12; 8 | 9 | #region Constructors 10 | 11 | public TcpProxyConfiguration() : this(DefaultProtocols) 12 | { 13 | // 14 | } 15 | 16 | public TcpProxyConfiguration(SslProtocols protocols) 17 | { 18 | SslProtocols = protocols; 19 | } 20 | 21 | #endregion 22 | 23 | 24 | /// 25 | /// Public interface in which the proxy will be listening 26 | /// 27 | public Host PublicHost { get; set; } 28 | 29 | /// 30 | /// Host which, when specified, will redirect the WebSocket packets 31 | /// 32 | public Host WebSocketHost { get; set; } 33 | 34 | /// 35 | /// Host which, when specified, will redirect plain HTTP packets 36 | /// 37 | public Host HttpHost { get; set; } 38 | 39 | /// 40 | /// If true, all packets will be decrypted using the SSL certificate specified in 41 | /// 42 | public bool EnableSslCertificate 43 | { 44 | get { return SslCertificatePath != null; } 45 | } 46 | 47 | /// 48 | /// FileSystem path to the SSL certificate 49 | /// 50 | public string SslCertificatePath { get; set; } 51 | 52 | /// 53 | /// Instalation Password of the certificate specified in 54 | /// 55 | public string SslCertificatePassword { get; set; } 56 | 57 | /// 58 | /// Enabled SSL protocols, in case EnableSslCertificate is set to true 59 | /// 60 | public SslProtocols SslProtocols { get; set; } 61 | } 62 | } -------------------------------------------------------------------------------- /WebSocketProxy/TcpProxyServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Sockets; 4 | using System.Security.Cryptography.X509Certificates; 5 | using System.Text; 6 | 7 | namespace WebSocketProxy 8 | { 9 | public class TcpProxyServer : IDisposable 10 | { 11 | private readonly TcpListener _tcpListener; 12 | private readonly TcpProxyConfiguration _configuration; 13 | private X509Certificate2 _certificate; 14 | private readonly TcpConnectionManager _tcpConnectionManager; 15 | private bool _closing; 16 | 17 | private readonly Logger _logger; 18 | 19 | public Logger Log 20 | { 21 | get { return _logger; } 22 | } 23 | 24 | 25 | public int ConnectionCount 26 | { 27 | get { return _tcpConnectionManager.ConnectionCount; } 28 | } 29 | 30 | #region Constructors 31 | 32 | public TcpProxyServer(TcpProxyConfiguration configuration) 33 | { 34 | _logger = new Logger(); 35 | _tcpConnectionManager = new TcpConnectionManager(); 36 | _configuration = configuration; 37 | _tcpListener = new TcpListener(_configuration.PublicHost.IpAddress, _configuration.PublicHost.Port); 38 | } 39 | 40 | #endregion 41 | 42 | public void Start() 43 | { 44 | if (_configuration.EnableSslCertificate) 45 | { 46 | _certificate = _configuration.SslCertificatePassword != null 47 | ? new X509Certificate2(_configuration.SslCertificatePath, _configuration.SslCertificatePassword) 48 | : new X509Certificate2(_configuration.SslCertificatePath); 49 | } 50 | 51 | _tcpListener.Start(); 52 | DoBeginListenForClients(); 53 | 54 | Log.Info(string.Format("Proxy Server started at {0}", _configuration.PublicHost)); 55 | } 56 | 57 | private void DoBeginListenForClients() 58 | { 59 | try 60 | { 61 | _tcpListener.BeginAcceptTcpClient(TcpClientAcceptCallback, _tcpListener); 62 | } 63 | catch (InvalidOperationException) 64 | { 65 | // socket has been closed 66 | } 67 | catch (IOException) 68 | { 69 | // 70 | } 71 | } 72 | 73 | private void TcpClientAcceptCallback(IAsyncResult asyncResult) 74 | { 75 | if (_closing) 76 | return; 77 | 78 | DoBeginListenForClients(); 79 | 80 | try 81 | { 82 | TcpClient client = _tcpListener.EndAcceptTcpClient(asyncResult); 83 | 84 | TcpHost host = new TcpHost(client); 85 | if (_certificate != null) 86 | { 87 | host.BeginAuthenticationAsServer(_certificate, _configuration.SslProtocols, AuthenticationCallback, 88 | host); 89 | } 90 | else 91 | { 92 | ReadFirstPacket(host); 93 | } 94 | } 95 | catch (IOException) 96 | { 97 | // TCP Listener may not be valid anymore 98 | } 99 | } 100 | 101 | private void AuthenticationCallback(IAsyncResult asyncResult) 102 | { 103 | if (asyncResult == null) return; 104 | 105 | TcpHost host = asyncResult.AsyncState as TcpHost; 106 | if (host == null) 107 | return; 108 | 109 | try 110 | { 111 | host.EndAuthenticationAsServer(asyncResult); 112 | ReadFirstPacket(host); 113 | } 114 | catch (IOException) 115 | { 116 | // Somehow, the authentication failed... Close the connection 117 | host.Close(); 118 | } 119 | } 120 | 121 | private void ReadFirstPacket(TcpHost clientMachine) 122 | { 123 | byte[] data = new byte[4 * 1024]; 124 | 125 | try 126 | { 127 | TcpStateContainer state = new TcpStateContainer(clientMachine, data); 128 | clientMachine.Stream.BeginRead(data, 0, data.Length, FirstPacketReadCallback, state); 129 | } 130 | catch (IOException) 131 | { 132 | clientMachine.Close(); 133 | } 134 | } 135 | 136 | private void FirstPacketReadCallback(IAsyncResult asyncResult) 137 | { 138 | if (asyncResult == null) return; 139 | 140 | TcpStateContainer asyncState = asyncResult.AsyncState as TcpStateContainer; 141 | if (asyncState == null) 142 | return; 143 | 144 | TcpHost clientMachine = asyncState.Client; 145 | byte[] data = asyncState.DataBuffer; 146 | 147 | try 148 | { 149 | int numBytes = clientMachine.Stream.EndRead(asyncResult); 150 | string stringData = Encoding.UTF8.GetString(data, 0, numBytes); 151 | 152 | HttpPacket packet = HttpPacketBuilder.BuildPacket(stringData); 153 | 154 | if (packet == null) 155 | { 156 | clientMachine.Close(); 157 | return; 158 | } 159 | 160 | Host serverMachineHost = packet.IsWebSocketPacket 161 | ? _configuration.WebSocketHost 162 | : _configuration.HttpHost; 163 | 164 | if (serverMachineHost != null && serverMachineHost.IsSpecified) 165 | { 166 | TcpHost serverMachine = TcpHost.ManufactureDefault(serverMachineHost.IpAddress, 167 | serverMachineHost.Port); 168 | 169 | serverMachine.Send(data, numBytes); 170 | 171 | TcpRoute route = new TcpRoute(clientMachine, serverMachine); 172 | _tcpConnectionManager.AddRoute(route); 173 | } 174 | else 175 | { 176 | clientMachine.Close(); 177 | } 178 | } 179 | catch (IOException) {} 180 | } 181 | 182 | public void Dispose() 183 | { 184 | if (_tcpListener == null) 185 | return; 186 | 187 | _closing = true; 188 | 189 | try 190 | { 191 | _tcpListener.Stop(); 192 | } 193 | catch (IOException) 194 | { 195 | // In case the listener is not listening anymore 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /WebSocketProxy/TcpRoute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebSocketProxy 4 | { 5 | internal class TcpRoute : IDisposable 6 | { 7 | private readonly TcpHost _clientMachine; 8 | private readonly TcpHost _serverMachine; 9 | 10 | #region Events 11 | 12 | public delegate void DisconnectedDelegate(TcpRoute route); 13 | 14 | public event DisconnectedDelegate Disconnected; 15 | 16 | protected void OnDisconnected() 17 | { 18 | if (Disconnected != null) 19 | { 20 | Disconnected(this); 21 | } 22 | } 23 | 24 | #endregion 25 | 26 | public bool Connected 27 | { 28 | get { return _clientMachine.Connected && _serverMachine.Connected; } 29 | } 30 | 31 | public TcpRoute(TcpHost clientMachine, TcpHost serverMachine) 32 | { 33 | _clientMachine = clientMachine; 34 | _serverMachine = serverMachine; 35 | } 36 | 37 | public void Start() 38 | { 39 | RegisterHostEvents(); 40 | _clientMachine.StartReading(); 41 | _serverMachine.StartReading(); 42 | } 43 | 44 | void _serverMachine_DataAvailable(TcpHost host, byte[] data, int length) 45 | { 46 | _clientMachine.Send(data, length); 47 | } 48 | 49 | void _clientMachine_DataAvailable(TcpHost host, byte[] data, int length) 50 | { 51 | _serverMachine.Send(data, length); 52 | } 53 | 54 | void _serverMachine_Disconnected(TcpHost host) 55 | { 56 | Stop(); 57 | } 58 | 59 | void _clientMachine_Disconnected(TcpHost host) 60 | { 61 | Stop(); 62 | } 63 | 64 | private void RegisterHostEvents() 65 | { 66 | _clientMachine.Disconnected += _clientMachine_Disconnected; 67 | _serverMachine.Disconnected += _serverMachine_Disconnected; 68 | 69 | _clientMachine.DataAvailable += _clientMachine_DataAvailable; 70 | _serverMachine.DataAvailable += _serverMachine_DataAvailable; 71 | } 72 | 73 | 74 | public void Stop() 75 | { 76 | _clientMachine.Close(); 77 | _serverMachine.Close(); 78 | 79 | OnDisconnected(); 80 | } 81 | 82 | 83 | public void Dispose() 84 | { 85 | Stop(); 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /WebSocketProxy/TcpStateContainer.cs: -------------------------------------------------------------------------------- 1 | namespace WebSocketProxy 2 | { 3 | /// 4 | /// Helper class for managing the state in a Stream.BeginRead operation 5 | /// 6 | internal class TcpStateContainer 7 | { 8 | public TcpStateContainer(TcpHost client, byte[] dataBuffer) 9 | { 10 | Client = client; 11 | DataBuffer = dataBuffer; 12 | } 13 | 14 | public TcpHost Client { get; set; } 15 | 16 | public byte[] DataBuffer { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /WebSocketProxy/WebSocketHeaders.cs: -------------------------------------------------------------------------------- 1 | namespace WebSocketProxy 2 | { 3 | internal class WebSocketHeaders 4 | { 5 | public static readonly string[] ClientHeaders = 6 | { 7 | "sec-websocket-key", 8 | "sec-websocket-extensions", 9 | "sec-websocket-protocol", 10 | "sec-websocket-protocol" 11 | }; 12 | } 13 | } -------------------------------------------------------------------------------- /WebSocketProxy/WebSocketProxy.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {BDA44986-B4D2-43E2-9486-CAC4F62EEB4F} 8 | Library 9 | Properties 10 | WebSocketProxy 11 | WebSocketProxy 12 | v4.5 13 | 512 14 | 15 | 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | false 26 | 5 27 | 28 | 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | false 36 | 5 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 72 | -------------------------------------------------------------------------------- /WebSocketProxy/WebSocketProxy.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $title$ 5 | $version$ 6 | $title$ 7 | $author$ 8 | $author$ 9 | https://raw.githubusercontent.com/lifeemotions/websocketproxy/master/LICENSE 10 | https://github.com/lifeemotions/websocketproxy 11 | false 12 | $description$ 13 | fix to avoid excepion when stopping 14 | Copyright © 2015 15 | .net websocket http ssl proxy 16 | 17 | --------------------------------------------------------------------------------