├── .gitignore
├── LICENSE.txt
├── README.md
├── WebSockets.sln
├── WebSockets.zip
├── WebSockets
├── Client
│ └── WebSocketClient.cs
├── Common
│ ├── BinaryReaderWriter.cs
│ ├── IWebSocketLogger.cs
│ ├── WebSocketBase.cs
│ ├── WebSocketCloseCode.cs
│ ├── WebSocketFrame.cs
│ ├── WebSocketFrameReader.cs
│ ├── WebSocketFrameWriter.cs
│ └── WebSocketOpCode.cs
├── Events
│ ├── BinaryFrameEventArgs.cs
│ ├── BinaryMultiFrameEventArgs.cs
│ ├── ConnectionCloseEventArgs.cs
│ ├── PingEventArgs.cs
│ ├── PongEventArgs.cs
│ ├── TextFrameEventArgs.cs
│ └── TextMultiFrameEventArgs.cs
├── Exceptions
│ ├── EntityTooLargeException.cs
│ ├── ServerListenerSocketException.cs
│ ├── TODO.txt
│ ├── WebSocketHandshakeFailedException.cs
│ └── WebSocketVersionNotSupportedException.cs
├── MimeTypes.config
├── Properties
│ └── AssemblyInfo.cs
├── README.txt
├── Server
│ ├── ConnectionDetails.cs
│ ├── ConnectionType.cs
│ ├── Http
│ │ ├── BadRequestService.cs
│ │ ├── HttpHelper.cs
│ │ ├── HttpService.cs
│ │ ├── MimeTypes.cs
│ │ └── MimeTypesFactory.cs
│ ├── IService.cs
│ ├── IServiceFactory.cs
│ ├── WebServer.cs
│ └── WebSocket
│ │ └── WebSocketService.cs
└── WebSockets.csproj
├── WebSocketsCmd
├── App.config
├── Client
│ └── ChatWebSocketClient.cs
├── CustomConsoleTraceListener.cs
├── Program.cs
├── Properties
│ ├── AssemblyInfo.cs
│ ├── Settings.Designer.cs
│ └── Settings.settings
├── README.md
├── Server
│ ├── ChatWebSocketService.cs
│ └── ServiceFactory.cs
├── WebSocketLogger.cs
├── WebSocketsCmd.csproj
├── client.html
└── favicon.ico
└── gitpublish.bat
/.gitignore:
--------------------------------------------------------------------------------
1 | *.suo
2 | *.user
3 | _ReSharper.*
4 | bin
5 | obj
6 | packages
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 David Haig
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
WebSocket Server in c#
2 |
3 | NOTE: This is no longer maintained. See https://github.com/ninjasource/Ninja.WebSockets
4 | Set WebSockets.Cmd
as the startup project
5 |
6 | License
7 |
8 | The MIT License (MIT)
9 |
See LICENCE.txt
10 |
11 | Introduction
12 |
13 | A lot of the Web Socket examples out there are for old Web Socket versions and included complicated code (and external libraries) for fall back communication. All modern browsers that anyone cares about (including safari on an iphone) support at least version 13 of the Web Socket protocol so I'd rather not complicate things. This is a bare bones implementation of the web socket protocol in C# with no external libraries involved. You can connect using standard HTML5 JavaScript.
14 |
15 | This application serves up basic html pages as well as handling WebSocket connections. This may seem confusing but it allows you to send the client the html they need to make a web socket connection and also allows you to share the same port. However, the HttpConnection
is very rudimentary. I'm sure it has some glaring security problems. It was just made to make this demo easier to run. Replace it with your own or don't use it.
16 |
17 | Background
18 |
19 | There is nothing magical about Web Sockets. The spec is easy to follow and there is no need to use special libraries. At one point, I was even considering somehow communicating with Node.js but that is not necessary. The spec can be a bit fiddly with bits and bytes but this was probably done to keep the overheads low. This is my first CodeProject article and I hope you will find it easy to follow. The following links offer some great advice:
20 |
21 | Step by step guide
22 |
23 |
26 |
27 | The official Web Socket spec
28 |
29 |
32 |
33 | Some useful stuff in C#
34 |
35 |
38 |
39 | Using the Code
40 |
41 | NOTE You will get a firewall warning because you are listening on a port. This is normal for any socket based server.
42 |
A good place to put a breakpoint is in the WebServer
class in the HandleAsyncConnection
function. Note that this is a multithreaded server so you may want to freeze threads if this gets confusing. The console output prints the thread id to make things easier. If you want to skip past all the plumbing, then another good place to start is the Respond
function in the WebSocketConnection
class. If you are not interested in the inner workings of Web Sockets and just want to use them, then take a look at the OnTextFrame
in the ChatWebSocketConnection
class. See below.
43 |
44 | Implementation of a chat web socket connection is as follows:
45 |
46 |
47 | internal class ChatWebSocketService : WebSocketService
48 | {
49 | private readonly IWebSocketLogger _logger;
50 |
51 | public ChatWebSocketService(NetworkStream networkStream, TcpClient tcpClient, string header, IWebSocketLogger logger)
52 | : base(networkStream, tcpClient, header, true, logger)
53 | {
54 | _logger = logger;
55 | }
56 |
57 | protected override void OnTextFrame(string text)
58 | {
59 | string response = "ServerABC: " + text;
60 | base.Send(response);
61 | _logger.Information(this.GetType(), response);
62 | }
63 | }
64 |
65 | The factory used to create the connection is as follows:
66 |
67 |
68 | internal class ServiceFactory : IServiceFactory
69 | {
70 | public ServiceFactory(string webRoot, IWebSocketLogger logger)
71 | {
72 | _logger = logger;
73 | _webRoot = webRoot;
74 | }
75 |
76 | public IService CreateInstance(ConnectionDetails connectionDetails)
77 | {
78 | switch (connectionDetails.ConnectionType)
79 | {
80 | case ConnectionType.WebSocket:
81 | // you can support different kinds of web socket connections using a different path
82 | if (connectionDetails.Path == "/chat")
83 | {
84 | return new ChatWebSocketService(connectionDetails.NetworkStream, connectionDetails.TcpClient, connectionDetails.Header, _logger);
85 | }
86 | break;
87 | case ConnectionType.Http:
88 | // this path actually refers to the reletive location of some html file or image
89 | return new HttpService(connectionDetails.NetworkStream, connectionDetails.Path, _webRoot, _logger);
90 | }
91 |
92 | return new BadRequestService(connectionDetails.NetworkStream, connectionDetails.Header, _logger);
93 | }
94 | }
95 |
96 | HTML5 JavaScript used to connect:
97 |
98 |
99 | // open the connection to the Web Socket server
100 | var CONNECTION = new WebSocket('ws://localhost/chat');
101 |
102 | // Log messages from the server
103 | CONNECTION.onmessage = function (e) {
104 | console.log(e.data);
105 | };
106 |
107 | CONNECTION.send('Hellow World');
108 |
109 | Starting the server and the test client:
110 |
111 |
112 | private static void Main(string[] args)
113 | {
114 | IWebSocketLogger logger = new WebSocketLogger();
115 |
116 | try
117 | {
118 | string webRoot = Settings.Default.WebRoot;
119 | int port = Settings.Default.Port;
120 |
121 | // used to decide what to do with incoming connections
122 | ServiceFactory serviceFactory = new ServiceFactory(webRoot, logger);
123 |
124 | using (WebServer server = new WebServer(serviceFactory, logger))
125 | {
126 | server.Listen(port);
127 | Thread clientThread = new Thread(new ParameterizedThreadStart(TestClient));
128 | clientThread.IsBackground = false;
129 | clientThread.Start(logger);
130 | Console.ReadKey();
131 | }
132 | }
133 | catch (Exception ex)
134 | {
135 | logger.Error(null, ex);
136 | Console.ReadKey();
137 | }
138 | }
139 |
140 | The test client runs a short self test to make sure that everything is fine. Opening and closing handshakes are tested here.
141 |
142 | Web Socket Protocol
143 |
144 | The first thing to realize about the protocol is that it is, in essence, a basic duplex TCP/IP socket connection. The connection starts off with the client connecting to a remote server and sending http header text to that server. The header text asks the web server to upgrade the connection to a web socket connection. This is done as a handshake where the web server responds with an appropriate http text header and from then onwards, the client and server will talk the Web Socket language.
145 |
146 | Server Handshake
147 |
148 |
149 | Regex webSocketKeyRegex = new Regex("Sec-WebSocket-Key: (.*)");
150 | Regex webSocketVersionRegex = new Regex("Sec-WebSocket-Version: (.*)");
151 |
152 | // check the version. Support version 13 and above
153 | const int WebSocketVersion = 13;
154 | int secWebSocketVersion = Convert.ToInt32(webSocketVersionRegex.Match(header).Groups[1].Value.Trim());
155 | if (secWebSocketVersion < WebSocketVersion)
156 | {
157 | throw new WebSocketVersionNotSupportedException(string.Format("WebSocket Version {0} not suported. Must be {1} or above", secWebSocketVersion, WebSocketVersion));
158 | }
159 |
160 | string secWebSocketKey = webSocketKeyRegex.Match(header).Groups[1].Value.Trim();
161 | string setWebSocketAccept = base.ComputeSocketAcceptString(secWebSocketKey);
162 | string response = ("HTTP/1.1 101 Switching Protocols" + Environment.NewLine
163 | + "Connection: Upgrade" + Environment.NewLine
164 | + "Upgrade: websocket" + Environment.NewLine
165 | + "Sec-WebSocket-Accept: " + setWebSocketAccept);
166 |
167 | HttpHelper.WriteHttpHeader(response, networkStream);
168 |
169 | This computes the accept string
:
170 |
171 |
172 | /// <summary>
173 | /// Combines the key supplied by the client with a guid and returns the sha1 hash of the combination
174 | /// </summary>
175 | public static string ComputeSocketAcceptString(string secWebSocketKey)
176 | {
177 | // this is a guid as per the web socket spec
178 | const string webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
179 |
180 | string concatenated = secWebSocketKey + webSocketGuid;
181 | byte[] concatenatedAsBytes = Encoding.UTF8.GetBytes(concatenated);
182 | byte[] sha1Hash = SHA1.Create().ComputeHash(concatenatedAsBytes);
183 | string secWebSocketAccept = Convert.ToBase64String(sha1Hash);
184 | return secWebSocketAccept;
185 | }
186 |
187 | Client Handshake
188 |
189 | Uri uri = _uri;
190 | WebSocketFrameReader reader = new WebSocketFrameReader();
191 | Random rand = new Random();
192 | byte[] keyAsBytes = new byte[16];
193 | rand.NextBytes(keyAsBytes);
194 | string secWebSocketKey = Convert.ToBase64String(keyAsBytes);
195 |
196 | string handshakeHttpRequestTemplate = @"GET {0} HTTP/1.1{4}" +
197 | "Host: {1}:{2}{4}" +
198 | "Upgrade: websocket{4}" +
199 | "Connection: Upgrade{4}" +
200 | "Sec-WebSocket-Key: {3}{4}" +
201 | "Sec-WebSocket-Version: 13{4}{4}";
202 |
203 | string handshakeHttpRequest = string.Format(handshakeHttpRequestTemplate, uri.PathAndQuery, uri.Host, uri.Port, secWebSocketKey, Environment.NewLine);
204 | byte[] httpRequest = Encoding.UTF8.GetBytes(handshakeHttpRequest);
205 | networkStream.Write(httpRequest, 0, httpRequest.Length);
206 |
207 | Reading and Writing
208 |
209 | After the handshake as been performed, the server goes into a read
loop. The following two class convert a stream of bytes to a web socket frame and visa versa: WebSocketFrameReader
and WebSocketFrameWriter
.
210 |
211 |
212 | // from WebSocketFrameReader class
213 | public WebSocketFrame Read(NetworkStream stream, Socket socket)
214 | {
215 | byte byte1;
216 |
217 | try
218 | {
219 | byte1 = (byte) stream.ReadByte();
220 | }
221 | catch (IOException)
222 | {
223 | if (socket.Connected)
224 | {
225 | throw;
226 | }
227 | else
228 | {
229 | return null;
230 | }
231 | }
232 |
233 | // process first byte
234 | byte finBitFlag = 0x80;
235 | byte opCodeFlag = 0x0F;
236 | bool isFinBitSet = (byte1 & finBitFlag) == finBitFlag;
237 | WebSocketOpCode opCode = (WebSocketOpCode) (byte1 & opCodeFlag);
238 |
239 | // read and process second byte
240 | byte byte2 = (byte) stream.ReadByte();
241 | byte maskFlag = 0x80;
242 | bool isMaskBitSet = (byte2 & maskFlag) == maskFlag;
243 | uint len = ReadLength(byte2, stream);
244 | byte[] decodedPayload;
245 |
246 | // use the masking key to decode the data if needed
247 | if (isMaskBitSet)
248 | {
249 | const int maskKeyLen = 4;
250 | byte[] maskKey = BinaryReaderWriter.ReadExactly(maskKeyLen, stream);
251 | byte[] encodedPayload = BinaryReaderWriter.ReadExactly((int) len, stream);
252 | decodedPayload = new byte[len];
253 |
254 | // apply the mask key
255 | for (int i = 0; i < encodedPayload.Length; i++)
256 | {
257 | decodedPayload[i] = (Byte) (encodedPayload[i] ^ maskKey[i%maskKeyLen]);
258 | }
259 | }
260 | else
261 | {
262 | decodedPayload = BinaryReaderWriter.ReadExactly((int) len, stream);
263 | }
264 |
265 | WebSocketFrame frame = new WebSocketFrame(isFinBitSet, opCode, decodedPayload, true);
266 | return frame;
267 | }
268 |
269 |
270 | // from WebSocketFrameWriter class
271 | public void Write(WebSocketOpCode opCode, byte[] payload, bool isLastFrame)
272 | {
273 | // best to write everything to a memory stream before we push it onto the wire
274 | // not really necessary but I like it this way
275 | using (MemoryStream memoryStream = new MemoryStream())
276 | {
277 | byte finBitSetAsByte = isLastFrame ? (byte)0x80 : (byte)0x00;
278 | byte byte1 = (byte)(finBitSetAsByte | (byte)opCode);
279 | memoryStream.WriteByte(byte1);
280 |
281 | // NB, dont set the mask flag. No need to mask data from server to client
282 | // depending on the size of the length we want to write it as a byte, ushort or ulong
283 | if (payload.Length < 126)
284 | {
285 | byte byte2 = (byte)payload.Length;
286 | memoryStream.WriteByte(byte2);
287 | }
288 | else if (payload.Length <= ushort.MaxValue)
289 | {
290 | byte byte2 = 126;
291 | memoryStream.WriteByte(byte2);
292 | BinaryReaderWriter.WriteUShort((ushort)payload.Length, memoryStream, false);
293 | }
294 | else
295 | {
296 | byte byte2 = 127;
297 | memoryStream.WriteByte(byte2);
298 | BinaryReaderWriter.WriteULong((ulong)payload.Length, memoryStream, false);
299 | }
300 |
301 | memoryStream.Write(payload, 0, payload.Length);
302 | byte[] buffer = memoryStream.ToArray();
303 | _stream.Write(buffer, 0, buffer.Length);
304 | }
305 | }
306 |
307 |
308 |
309 | Points of Interest
310 |
311 | Problems with Proxy Servers:
312 | Proxy servers which have not been configured to support Web sockets will not work well with them.
313 | I suggest that you use transport layer security if you want this to work across the wider internet especially from within a corporation.
314 |
315 | History
316 |
317 |
318 | - Version 1.01 WebSocket
319 | - Version 1.02 Fixed endianness bug with length field
320 | - Version 1.03 Major refactor and added support for c# Client
321 |
322 |
--------------------------------------------------------------------------------
/WebSockets.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 11.00
3 | # Visual Studio 2010
4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSockets", "WebSockets\WebSockets.csproj", "{D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}"
5 | EndProject
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSocketsCmd", "WebSocketsCmd\WebSocketsCmd.csproj", "{8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Debug|Mixed Platforms = Debug|Mixed Platforms
12 | Debug|x86 = Debug|x86
13 | Release|Any CPU = Release|Any CPU
14 | Release|Mixed Platforms = Release|Mixed Platforms
15 | Release|x86 = Release|x86
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
21 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
22 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Debug|x86.ActiveCfg = Debug|Any CPU
23 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
24 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Release|Any CPU.Build.0 = Release|Any CPU
25 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
26 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
27 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}.Release|x86.ActiveCfg = Release|Any CPU
28 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Debug|Any CPU.ActiveCfg = Debug|x86
29 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
30 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Debug|Mixed Platforms.Build.0 = Debug|x86
31 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Debug|x86.ActiveCfg = Debug|x86
32 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Debug|x86.Build.0 = Debug|x86
33 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Release|Any CPU.ActiveCfg = Release|x86
34 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Release|Mixed Platforms.ActiveCfg = Release|x86
35 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Release|Mixed Platforms.Build.0 = Release|x86
36 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Release|x86.ActiveCfg = Release|x86
37 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}.Release|x86.Build.0 = Release|x86
38 | EndGlobalSection
39 | GlobalSection(SolutionProperties) = preSolution
40 | HideSolutionNode = FALSE
41 | EndGlobalSection
42 | EndGlobal
43 |
--------------------------------------------------------------------------------
/WebSockets.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ninjasource/websocket-server/3fc27cf05077235c9f764728d3ec9e7139845275/WebSockets.zip
--------------------------------------------------------------------------------
/WebSockets/Client/WebSocketClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Net.Security;
7 | using System.Net.Sockets;
8 | using System.Text;
9 | using System.Text.RegularExpressions;
10 | using WebSockets.Common;
11 | using System.Diagnostics;
12 | using System.Security.Policy;
13 | using WebSockets.Exceptions;
14 | using WebSockets.Server.WebSocket;
15 | using WebSockets.Server.Http;
16 | using System.Threading;
17 | using WebSockets.Events;
18 | using System.Threading.Tasks;
19 | using System.Security.Cryptography.X509Certificates;
20 |
21 | namespace WebSockets.Client
22 | {
23 | public class WebSocketClient : WebSocketBase, IDisposable
24 | {
25 | private readonly bool _noDelay;
26 | private readonly IWebSocketLogger _logger;
27 | private TcpClient _tcpClient;
28 | private Stream _stream;
29 | private Uri _uri;
30 | private ManualResetEvent _conectionCloseWait;
31 |
32 | public WebSocketClient(bool noDelay, IWebSocketLogger logger)
33 | : base(logger)
34 | {
35 | _noDelay = noDelay;
36 | _logger = logger;
37 | _conectionCloseWait = new ManualResetEvent(false);
38 | }
39 |
40 | // The following method is invoked by the RemoteCertificateValidationDelegate.
41 | public static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
42 | {
43 | if (sslPolicyErrors == SslPolicyErrors.None)
44 | {
45 | return true;
46 | }
47 |
48 | Console.WriteLine("Certificate error: {0}", sslPolicyErrors);
49 |
50 | // Do not allow this client to communicate with unauthenticated servers.
51 | return false;
52 | }
53 |
54 | private Stream GetStream(TcpClient tcpClient, bool isSecure)
55 | {
56 | if (isSecure)
57 | {
58 | SslStream sslStream = new SslStream(tcpClient.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null);
59 | _logger.Information(this.GetType(), "Attempting to secure connection...");
60 | sslStream.AuthenticateAsClient("clusteredanalytics.com");
61 | _logger.Information(this.GetType(), "Connection successfully secured.");
62 | return sslStream;
63 | }
64 | else
65 | {
66 | _logger.Information(this.GetType(), "Connection not secure");
67 | return tcpClient.GetStream();
68 | }
69 | }
70 |
71 | public virtual void OpenBlocking(Uri uri)
72 | {
73 | if (!_isOpen)
74 | {
75 | string host = uri.Host;
76 | int port = uri.Port;
77 | _tcpClient = new TcpClient();
78 | _tcpClient.NoDelay = _noDelay;
79 |
80 | IPAddress ipAddress;
81 | if (IPAddress.TryParse(host, out ipAddress))
82 | {
83 | _tcpClient.Connect(ipAddress, port);
84 | }
85 | else
86 | {
87 | _tcpClient.Connect(host, port);
88 | }
89 |
90 | bool isSecure = port == 443;
91 | _stream = GetStream(_tcpClient, isSecure);
92 |
93 | _uri = uri;
94 | _isOpen = true;
95 | base.OpenBlocking(_stream, _tcpClient.Client);
96 | _isOpen = false;
97 | }
98 | }
99 |
100 | protected override void PerformHandshake(Stream stream)
101 | {
102 | Uri uri = _uri;
103 | WebSocketFrameReader reader = new WebSocketFrameReader();
104 | Random rand = new Random();
105 | byte[] keyAsBytes = new byte[16];
106 | rand.NextBytes(keyAsBytes);
107 | string secWebSocketKey = Convert.ToBase64String(keyAsBytes);
108 |
109 | string handshakeHttpRequestTemplate = @"GET {0} HTTP/1.1{4}" +
110 | "Host: {1}:{2}{4}" +
111 | "Upgrade: websocket{4}" +
112 | "Connection: Upgrade{4}" +
113 | "Sec-WebSocket-Key: {3}{4}" +
114 | "Sec-WebSocket-Version: 13{4}{4}";
115 |
116 | string handshakeHttpRequest = string.Format(handshakeHttpRequestTemplate, uri.PathAndQuery, uri.Host, uri.Port, secWebSocketKey, Environment.NewLine);
117 | byte[] httpRequest = Encoding.UTF8.GetBytes(handshakeHttpRequest);
118 | stream.Write(httpRequest, 0, httpRequest.Length);
119 | _logger.Information(this.GetType(), "Handshake sent. Waiting for response.");
120 |
121 | // make sure we escape the accept string which could contain special regex characters
122 | string regexPattern = "Sec-WebSocket-Accept: (.*)";
123 | Regex regex = new Regex(regexPattern);
124 |
125 | string response = string.Empty;
126 |
127 | try
128 | {
129 | response = HttpHelper.ReadHttpHeader(stream);
130 | }
131 | catch (Exception ex)
132 | {
133 | throw new WebSocketHandshakeFailedException("Handshake unexpected failure", ex);
134 | }
135 |
136 | // check the accept string
137 | string expectedAcceptString = base.ComputeSocketAcceptString(secWebSocketKey);
138 | string actualAcceptString = regex.Match(response).Groups[1].Value.Trim();
139 | if (expectedAcceptString != actualAcceptString)
140 | {
141 | throw new WebSocketHandshakeFailedException(string.Format("Handshake failed because the accept string from the server '{0}' was not the expected string '{1}'", expectedAcceptString, actualAcceptString));
142 | }
143 | else
144 | {
145 | _logger.Information(this.GetType(), "Handshake response received. Connection upgraded to WebSocket protocol.");
146 | }
147 | }
148 |
149 | public virtual void Dispose()
150 | {
151 | if (_isOpen)
152 | {
153 | using (MemoryStream stream = new MemoryStream())
154 | {
155 | // set the close reason to GoingAway
156 | BinaryReaderWriter.WriteUShort((ushort) WebSocketCloseCode.GoingAway, stream, false);
157 |
158 | // send close message to server to begin the close handshake
159 | Send(WebSocketOpCode.ConnectionClose, stream.ToArray());
160 | _logger.Information(this.GetType(), "Sent websocket close message to server. Reason: GoingAway");
161 | }
162 |
163 | // this needs to run on a worker thread so that the read loop (in the base class) is not blocked
164 | Task.Factory.StartNew(WaitForServerCloseMessage);
165 | }
166 | }
167 |
168 | private void WaitForServerCloseMessage()
169 | {
170 | // as per the websocket spec, the server must close the connection, not the client.
171 | // The client is free to close the connection after a timeout period if the server fails to do so
172 | _conectionCloseWait.WaitOne(TimeSpan.FromSeconds(10));
173 |
174 | // this will only happen if the server has failed to reply with a close response
175 | if (_isOpen)
176 | {
177 | _logger.Warning(this.GetType(), "Server failed to respond with a close response. Closing the connection from the client side.");
178 |
179 | // wait for data to be sent before we close the stream and client
180 | _tcpClient.Client.Shutdown(SocketShutdown.Both);
181 | _stream.Close();
182 | _tcpClient.Close();
183 | }
184 |
185 | _logger.Information(this.GetType(), "Client: Connection closed");
186 | }
187 |
188 | protected override void OnConnectionClose(byte[] payload)
189 | {
190 | // server has either responded to a client close request or closed the connection for its own reasons
191 | // the server will close the tcp connection so the client will not have to do it
192 | _isOpen = false;
193 | _conectionCloseWait.Set();
194 | base.OnConnectionClose(payload);
195 | }
196 |
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/WebSockets/Common/BinaryReaderWriter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text;
6 |
7 | namespace WebSockets.Common
8 | {
9 | internal class BinaryReaderWriter
10 | {
11 | public static byte[] ReadExactly(int length, Stream stream)
12 | {
13 | byte[] buffer = new byte[length];
14 | if (length == 0)
15 | {
16 | return buffer;
17 | }
18 |
19 | int offset = 0;
20 | do
21 | {
22 | int bytesRead = stream.Read(buffer, offset, length - offset);
23 | if (bytesRead == 0)
24 | {
25 | throw new EndOfStreamException(string.Format("Unexpected end of stream encountered whilst attempting to read {0:#,##0} bytes", length));
26 | }
27 |
28 | offset += bytesRead;
29 | } while (offset < length);
30 |
31 | return buffer;
32 | }
33 |
34 | public static ushort ReadUShortExactly(Stream stream, bool isLittleEndian)
35 | {
36 | byte[] lenBuffer = BinaryReaderWriter.ReadExactly(2, stream);
37 |
38 | if (!isLittleEndian)
39 | {
40 | Array.Reverse(lenBuffer); // big endian
41 | }
42 |
43 | return BitConverter.ToUInt16(lenBuffer, 0);
44 | }
45 |
46 | public static ulong ReadULongExactly(Stream stream, bool isLittleEndian)
47 | {
48 | byte[] lenBuffer = BinaryReaderWriter.ReadExactly(8, stream);
49 |
50 | if (!isLittleEndian)
51 | {
52 | Array.Reverse(lenBuffer); // big endian
53 | }
54 |
55 | return BitConverter.ToUInt64(lenBuffer, 0);
56 | }
57 |
58 | public static long ReadLongExactly(Stream stream, bool isLittleEndian)
59 | {
60 | byte[] lenBuffer = BinaryReaderWriter.ReadExactly(8, stream);
61 |
62 | if (!isLittleEndian)
63 | {
64 | Array.Reverse(lenBuffer); // big endian
65 | }
66 |
67 | return BitConverter.ToInt64(lenBuffer, 0);
68 | }
69 |
70 | public static void WriteULong(ulong value, Stream stream, bool isLittleEndian)
71 | {
72 | byte[] buffer = BitConverter.GetBytes(value);
73 | if (BitConverter.IsLittleEndian && ! isLittleEndian)
74 | {
75 | Array.Reverse(buffer);
76 | }
77 |
78 | stream.Write(buffer, 0, buffer.Length);
79 | }
80 |
81 | public static void WriteLong(long value, Stream stream, bool isLittleEndian)
82 | {
83 | byte[] buffer = BitConverter.GetBytes(value);
84 | if (BitConverter.IsLittleEndian && !isLittleEndian)
85 | {
86 | Array.Reverse(buffer);
87 | }
88 |
89 | stream.Write(buffer, 0, buffer.Length);
90 | }
91 |
92 | public static void WriteUShort(ushort value, Stream stream, bool isLittleEndian)
93 | {
94 | byte[] buffer = BitConverter.GetBytes(value);
95 | if (BitConverter.IsLittleEndian && !isLittleEndian)
96 | {
97 | Array.Reverse(buffer);
98 | }
99 |
100 | stream.Write(buffer, 0, buffer.Length);
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/WebSockets/Common/IWebSocketLogger.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace WebSockets.Common
7 | {
8 | public interface IWebSocketLogger
9 | {
10 | void Information(Type type, string format, params object[] args);
11 | void Warning(Type type, string format, params object[] args);
12 | void Error(Type type, string format, params object[] args);
13 | void Error(Type type, Exception exception);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/WebSockets/Common/WebSocketBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Net.Sockets;
6 | using System.Security.Cryptography;
7 | using System.Text;
8 | using System.IO;
9 | using WebSockets.Events;
10 |
11 | namespace WebSockets.Common
12 | {
13 | public abstract class WebSocketBase
14 | {
15 | private readonly IWebSocketLogger _logger;
16 | private readonly object _sendLocker;
17 | private Stream _stream;
18 | private WebSocketFrameWriter _writer;
19 | private WebSocketOpCode _multiFrameOpcode;
20 | private Socket _socket;
21 | protected bool _isOpen;
22 |
23 | public event EventHandler ConnectionOpened;
24 | public event EventHandler ConnectionClose;
25 | public event EventHandler Ping;
26 | public event EventHandler Pong;
27 | public event EventHandler TextFrame;
28 | public event EventHandler TextMultiFrame;
29 | public event EventHandler BinaryFrame;
30 | public event EventHandler BinaryMultiFrame;
31 |
32 | public WebSocketBase(IWebSocketLogger logger)
33 | {
34 | _logger = logger;
35 | _sendLocker = new object();
36 | _isOpen = false;
37 | }
38 |
39 | protected void OpenBlocking(Stream stream, Socket socket)
40 | {
41 | _socket = socket;
42 | _stream = stream;
43 | _writer = new WebSocketFrameWriter(stream);
44 | PerformHandshake(stream);
45 | _isOpen = true;
46 | MainReadLoop();
47 | }
48 |
49 | protected virtual void Send(WebSocketOpCode opCode, byte[] toSend, bool isLastFrame)
50 | {
51 | if (_isOpen)
52 | {
53 | lock (_sendLocker)
54 | {
55 | if (_isOpen)
56 | {
57 | _writer.Write(opCode, toSend, isLastFrame);
58 | }
59 | }
60 | }
61 | }
62 |
63 | protected virtual void Send(WebSocketOpCode opCode, byte[] toSend)
64 | {
65 | Send(opCode, toSend, true);
66 | }
67 |
68 | protected virtual void Send(byte[] toSend)
69 | {
70 | Send(WebSocketOpCode.BinaryFrame, toSend, true);
71 | }
72 |
73 | protected virtual void Send(string text)
74 | {
75 | byte[] bytes = Encoding.UTF8.GetBytes(text);
76 | Send(WebSocketOpCode.TextFrame, bytes, true);
77 | }
78 |
79 | protected virtual void OnConnectionOpened()
80 | {
81 | if (ConnectionOpened != null)
82 | {
83 | ConnectionOpened(this, new EventArgs());
84 | }
85 | }
86 |
87 | protected virtual void OnPing(byte[] payload)
88 | {
89 | Send(WebSocketOpCode.Pong, payload);
90 |
91 | if (Ping != null)
92 | {
93 | Ping(this, new PingEventArgs(payload));
94 | }
95 | }
96 |
97 | protected virtual void OnPong(byte[] payload)
98 | {
99 | if (Pong != null)
100 | {
101 | Pong(this, new PingEventArgs(payload));
102 | }
103 | }
104 |
105 | protected virtual void OnTextFrame(string text)
106 | {
107 | if (TextFrame != null)
108 | {
109 | TextFrame(this, new TextFrameEventArgs(text));
110 | }
111 | }
112 |
113 | protected virtual void OnTextMultiFrame(string text, bool isLastFrame)
114 | {
115 | if (TextMultiFrame != null)
116 | {
117 | TextMultiFrame(this, new TextMultiFrameEventArgs(text, isLastFrame));
118 | }
119 | }
120 |
121 | protected virtual void OnBinaryFrame(byte[] payload)
122 | {
123 | if (BinaryFrame != null)
124 | {
125 | BinaryFrame(this, new BinaryFrameEventArgs(payload));
126 | }
127 | }
128 |
129 | protected virtual void OnBinaryMultiFrame(byte[] payload, bool isLastFrame)
130 | {
131 | if (BinaryMultiFrame != null)
132 | {
133 | BinaryMultiFrame(this, new BinaryMultiFrameEventArgs(payload, isLastFrame));
134 | }
135 | }
136 |
137 | protected virtual void OnConnectionClose(byte[] payload)
138 | {
139 | ConnectionCloseEventArgs args = GetConnectionCloseEventArgsFromPayload(payload);
140 |
141 | if (args.Reason == null)
142 | {
143 | _logger.Information(this.GetType(), "Received web socket close message: {0}", args.Code);
144 | }
145 | else
146 | {
147 | _logger.Information(this.GetType(), "Received web socket close message: Code '{0}' Reason '{1}'", args.Code, args.Reason);
148 | }
149 |
150 | if (ConnectionClose != null)
151 | {
152 | ConnectionClose(this, args);
153 | }
154 | }
155 |
156 | protected abstract void PerformHandshake(Stream stream);
157 |
158 | ///
159 | /// Combines the key supplied by the client with a guid and returns the sha1 hash of the combination
160 | ///
161 | protected string ComputeSocketAcceptString(string secWebSocketKey)
162 | {
163 | // this is a guid as per the web socket spec
164 | const string webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
165 |
166 | string concatenated = secWebSocketKey + webSocketGuid;
167 | byte[] concatenatedAsBytes = Encoding.UTF8.GetBytes(concatenated);
168 | byte[] sha1Hash = SHA1.Create().ComputeHash(concatenatedAsBytes);
169 | string secWebSocketAccept = Convert.ToBase64String(sha1Hash);
170 | return secWebSocketAccept;
171 | }
172 |
173 | protected ConnectionCloseEventArgs GetConnectionCloseEventArgsFromPayload(byte[] payload)
174 | {
175 | if (payload.Length >= 2)
176 | {
177 | using (MemoryStream stream = new MemoryStream(payload))
178 | {
179 | ushort code = BinaryReaderWriter.ReadUShortExactly(stream, false);
180 |
181 | try
182 | {
183 | WebSocketCloseCode closeCode = (WebSocketCloseCode)code;
184 |
185 | if (payload.Length > 2)
186 | {
187 | string reason = Encoding.UTF8.GetString(payload, 2, payload.Length - 2);
188 | return new ConnectionCloseEventArgs(closeCode, reason);
189 | }
190 | else
191 | {
192 | return new ConnectionCloseEventArgs(closeCode, null);
193 | }
194 | }
195 | catch (InvalidCastException)
196 | {
197 | _logger.Warning(this.GetType(), "Close code {0} not recognised", code);
198 | return new ConnectionCloseEventArgs(WebSocketCloseCode.Normal, null);
199 | }
200 | }
201 | }
202 |
203 | return new ConnectionCloseEventArgs(WebSocketCloseCode.Normal, null);
204 | }
205 |
206 | private void MainReadLoop()
207 | {
208 | Stream stream = _stream;
209 | OnConnectionOpened();
210 | WebSocketFrameReader reader = new WebSocketFrameReader();
211 | List fragmentedFrames = new List();
212 |
213 | while (true)
214 | {
215 | WebSocketFrame frame;
216 |
217 | try
218 | {
219 | frame = reader.Read(stream, _socket);
220 | if (frame == null)
221 | {
222 | return;
223 | }
224 | }
225 | catch (ObjectDisposedException)
226 | {
227 | return;
228 | }
229 |
230 | // if we have received unexpected data
231 | if (!frame.IsValid)
232 | {
233 | return;
234 | }
235 |
236 | if (frame.OpCode == WebSocketOpCode.ContinuationFrame)
237 | {
238 | switch (_multiFrameOpcode)
239 | {
240 | case WebSocketOpCode.TextFrame:
241 | String data = Encoding.UTF8.GetString(frame.DecodedPayload, 0, frame.DecodedPayload.Length);
242 | OnTextMultiFrame(data, frame.IsFinBitSet);
243 | break;
244 | case WebSocketOpCode.BinaryFrame:
245 | OnBinaryMultiFrame(frame.DecodedPayload, frame.IsFinBitSet);
246 | break;
247 | }
248 | }
249 | else
250 | {
251 | switch (frame.OpCode)
252 | {
253 | case WebSocketOpCode.ConnectionClose:
254 | OnConnectionClose(frame.DecodedPayload);
255 | return;
256 | case WebSocketOpCode.Ping:
257 | OnPing(frame.DecodedPayload);
258 | break;
259 | case WebSocketOpCode.Pong:
260 | OnPong(frame.DecodedPayload);
261 | break;
262 | case WebSocketOpCode.TextFrame:
263 | String data = Encoding.UTF8.GetString(frame.DecodedPayload, 0, frame.DecodedPayload.Length);
264 | if (frame.IsFinBitSet)
265 | {
266 | OnTextFrame(data);
267 | }
268 | else
269 | {
270 | _multiFrameOpcode = frame.OpCode;
271 | OnTextMultiFrame(data, frame.IsFinBitSet);
272 | }
273 | break;
274 | case WebSocketOpCode.BinaryFrame:
275 | if (frame.IsFinBitSet)
276 | {
277 | OnBinaryFrame(frame.DecodedPayload);
278 | }
279 | else
280 | {
281 | _multiFrameOpcode = frame.OpCode;
282 | OnBinaryMultiFrame(frame.DecodedPayload, frame.IsFinBitSet);
283 | }
284 | break;
285 | }
286 | }
287 | }
288 | }
289 | }
290 | }
291 |
--------------------------------------------------------------------------------
/WebSockets/Common/WebSocketCloseCode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace WebSockets.Common
7 | {
8 | public enum WebSocketCloseCode
9 | {
10 | Normal = 1000,
11 | GoingAway = 1001,
12 | ProtocolError = 1002,
13 | DataTypeNotSupported = 1003,
14 | Reserverd = 1004,
15 | ReserverdNoStatusCode = 1005,
16 | ReserverdAbnormalClosure = 1006,
17 | MismatchDataNonUTF8 = 1007,
18 | ViolationOfPolicy = 1008,
19 | MessageTooLarge = 1009,
20 | EnpointExpectsExtension = 1010,
21 | ServerUnexpectedCondition = 1011,
22 | ServerRegectTlsHandshake = 1015,
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/WebSockets/Common/WebSocketFrame.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace WebSockets.Common
7 | {
8 | public class WebSocketFrame
9 | {
10 | public bool IsFinBitSet { get; private set; }
11 | public WebSocketOpCode OpCode { get; private set; }
12 | public byte[] DecodedPayload { get; private set; }
13 | public bool IsValid { get; private set; }
14 |
15 | public WebSocketFrame(bool isFinBitSet, WebSocketOpCode webSocketOpCode, byte[] decodedPayload, bool isValid)
16 | {
17 | IsFinBitSet = isFinBitSet;
18 | OpCode = webSocketOpCode;
19 | DecodedPayload = decodedPayload;
20 | IsValid = isValid;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/WebSockets/Common/WebSocketFrameReader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.IO;
6 | using System.Net.Sockets;
7 | using System.Diagnostics;
8 |
9 | namespace WebSockets.Common
10 | {
11 | // see http://tools.ietf.org/html/rfc6455 for specification
12 |
13 | public class WebSocketFrameReader
14 | {
15 | private byte[] _buffer;
16 |
17 | public WebSocketFrameReader()
18 | {
19 | _buffer = new byte[1024*64];
20 | }
21 |
22 | public WebSocketFrame Read(Stream stream, Socket socket)
23 | {
24 | byte byte1;
25 |
26 | try
27 | {
28 | byte1 = (byte) stream.ReadByte();
29 | }
30 | catch (IOException)
31 | {
32 | if (socket.Connected)
33 | {
34 | throw;
35 | }
36 | else
37 | {
38 | return null;
39 | }
40 | }
41 |
42 | // process first byte
43 | byte finBitFlag = 0x80;
44 | byte opCodeFlag = 0x0F;
45 | bool isFinBitSet = (byte1 & finBitFlag) == finBitFlag;
46 | WebSocketOpCode opCode = (WebSocketOpCode) (byte1 & opCodeFlag);
47 |
48 | // read and process second byte
49 | byte byte2 = (byte) stream.ReadByte();
50 | byte maskFlag = 0x80;
51 | bool isMaskBitSet = (byte2 & maskFlag) == maskFlag;
52 | uint len = ReadLength(byte2, stream);
53 | byte[] decodedPayload;
54 |
55 | // use the masking key to decode the data if needed
56 | if (isMaskBitSet)
57 | {
58 | const int maskKeyLen = 4;
59 | byte[] maskKey = BinaryReaderWriter.ReadExactly(maskKeyLen, stream);
60 | byte[] encodedPayload = BinaryReaderWriter.ReadExactly((int) len, stream);
61 | decodedPayload = new byte[len];
62 |
63 | // apply the mask key
64 | for (int i = 0; i < encodedPayload.Length; i++)
65 | {
66 | decodedPayload[i] = (Byte) (encodedPayload[i] ^ maskKey[i%maskKeyLen]);
67 | }
68 | }
69 | else
70 | {
71 | decodedPayload = BinaryReaderWriter.ReadExactly((int) len, stream);
72 | }
73 |
74 | WebSocketFrame frame = new WebSocketFrame(isFinBitSet, opCode, decodedPayload, true);
75 | return frame;
76 | }
77 |
78 | private static uint ReadLength(byte byte2, Stream stream)
79 | {
80 | byte payloadLenFlag = 0x7F;
81 | uint len = (uint) (byte2 & payloadLenFlag);
82 |
83 | // read a short length or a long length depending on the value of len
84 | if (len == 126)
85 | {
86 | len = BinaryReaderWriter.ReadUShortExactly(stream, false);
87 | }
88 | else if (len == 127)
89 | {
90 | len = (uint) BinaryReaderWriter.ReadULongExactly(stream, false);
91 | const uint maxLen = 2147483648; // 2GB
92 |
93 | // protect ourselves against bad data
94 | if (len > maxLen || len < 0)
95 | {
96 | throw new ArgumentOutOfRangeException(string.Format("Payload length out of range. Min 0 max 2GB. Actual {0:#,##0} bytes.", len));
97 | }
98 | }
99 |
100 | return len;
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/WebSockets/Common/WebSocketFrameWriter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.IO;
6 | using System.Net.Sockets;
7 |
8 | namespace WebSockets.Common
9 | {
10 | // see http://tools.ietf.org/html/rfc6455 for specification
11 | // see fragmentation section for sending multi part messages
12 | // EXAMPLE: For a text message sent as three fragments,
13 | // the first fragment would have an opcode of TextFrame and isLastFrame false,
14 | // the second fragment would have an opcode of ContinuationFrame and isLastFrame false,
15 | // the third fragment would have an opcode of ContinuationFrame and isLastFrame true.
16 |
17 | public class WebSocketFrameWriter
18 | {
19 | private readonly Stream _stream;
20 |
21 | public WebSocketFrameWriter(Stream stream)
22 | {
23 | _stream = stream;
24 | }
25 |
26 | public void Write(WebSocketOpCode opCode, byte[] payload, bool isLastFrame)
27 | {
28 | // best to write everything to a memory stream before we push it onto the wire
29 | // not really necessary but I like it this way
30 | using (MemoryStream memoryStream = new MemoryStream())
31 | {
32 | byte finBitSetAsByte = isLastFrame ? (byte) 0x80 : (byte) 0x00;
33 | byte byte1 = (byte) (finBitSetAsByte | (byte) opCode);
34 | memoryStream.WriteByte(byte1);
35 |
36 | // NB, dont set the mask flag. No need to mask data from server to client
37 | // depending on the size of the length we want to write it as a byte, ushort or ulong
38 | if (payload.Length < 126)
39 | {
40 | byte byte2 = (byte) payload.Length;
41 | memoryStream.WriteByte(byte2);
42 | }
43 | else if (payload.Length <= ushort.MaxValue)
44 | {
45 | byte byte2 = 126;
46 | memoryStream.WriteByte(byte2);
47 | BinaryReaderWriter.WriteUShort((ushort) payload.Length, memoryStream, false);
48 | }
49 | else
50 | {
51 | byte byte2 = 127;
52 | memoryStream.WriteByte(byte2);
53 | BinaryReaderWriter.WriteULong((ulong) payload.Length, memoryStream, false);
54 | }
55 |
56 | memoryStream.Write(payload, 0, payload.Length);
57 | byte[] buffer = memoryStream.ToArray();
58 | _stream.Write(buffer, 0, buffer.Length);
59 | }
60 | }
61 |
62 | public void Write(WebSocketOpCode opCode, byte[] payload)
63 | {
64 | Write(opCode, payload, true);
65 | }
66 |
67 | public void WriteText(string text)
68 | {
69 | byte[] responseBytes = Encoding.UTF8.GetBytes(text);
70 | Write(WebSocketOpCode.TextFrame, responseBytes);
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/WebSockets/Common/WebSocketOpCode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace WebSockets.Common
7 | {
8 | public enum WebSocketOpCode
9 | {
10 | ContinuationFrame = 0,
11 | TextFrame = 1,
12 | BinaryFrame = 2,
13 | ConnectionClose = 8,
14 | Ping = 9,
15 | Pong = 10
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/WebSockets/Events/BinaryFrameEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace WebSockets.Events
7 | {
8 | public class BinaryFrameEventArgs : EventArgs
9 | {
10 | public byte[] Payload { get; private set; }
11 |
12 | public BinaryFrameEventArgs(byte[] payload)
13 | {
14 | Payload = payload;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/WebSockets/Events/BinaryMultiFrameEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace WebSockets.Events
7 | {
8 | public class BinaryMultiFrameEventArgs : BinaryFrameEventArgs
9 | {
10 | public bool IsLastFrame { get; private set; }
11 |
12 | public BinaryMultiFrameEventArgs(byte[] payload, bool isLastFrame) : base(payload)
13 | {
14 | IsLastFrame = isLastFrame;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/WebSockets/Events/ConnectionCloseEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using WebSockets.Common;
6 |
7 | namespace WebSockets.Events
8 | {
9 | public class ConnectionCloseEventArgs : EventArgs
10 | {
11 | public WebSocketCloseCode Code { get; private set; }
12 | public string Reason { get; private set; }
13 |
14 | public ConnectionCloseEventArgs(WebSocketCloseCode code, string reason)
15 | {
16 | Code = code;
17 | Reason = reason;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/WebSockets/Events/PingEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace WebSockets.Events
7 | {
8 | public class PingEventArgs : EventArgs
9 | {
10 | public byte[] Payload { get; private set; }
11 |
12 | public PingEventArgs(byte[] payload)
13 | {
14 | Payload = payload;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/WebSockets/Events/PongEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace WebSockets.Events
7 | {
8 | public class PongEventArgs : EventArgs
9 | {
10 | public byte[] Payload { get; private set; }
11 |
12 | public PongEventArgs(byte[] payload)
13 | {
14 | Payload = payload;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/WebSockets/Events/TextFrameEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace WebSockets.Events
7 | {
8 | public class TextFrameEventArgs : EventArgs
9 | {
10 | public string Text { get; private set; }
11 |
12 | public TextFrameEventArgs(string text)
13 | {
14 | Text = text;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/WebSockets/Events/TextMultiFrameEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace WebSockets.Events
7 | {
8 | public class TextMultiFrameEventArgs : TextFrameEventArgs
9 | {
10 | public bool IsLastFrame { get; private set; }
11 |
12 | public TextMultiFrameEventArgs(string text, bool isLastFrame) : base(text)
13 | {
14 | IsLastFrame = isLastFrame;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/WebSockets/Exceptions/EntityTooLargeException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Runtime.Serialization;
5 | using System.Text;
6 |
7 | namespace WebSockets.Exceptions
8 | {
9 | [Serializable]
10 | public class EntityTooLargeException : Exception
11 | {
12 | public EntityTooLargeException() : base()
13 | {
14 |
15 | }
16 |
17 | ///
18 | /// Http header too large to fit in buffer
19 | ///
20 | public EntityTooLargeException(string message) : base(message)
21 | {
22 |
23 | }
24 |
25 | public EntityTooLargeException(string message, Exception inner) : base(message, inner)
26 | {
27 |
28 | }
29 |
30 | public EntityTooLargeException(SerializationInfo info, StreamingContext context) : base(info, context)
31 | {
32 |
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/WebSockets/Exceptions/ServerListenerSocketException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Runtime.Serialization;
5 | using System.Text;
6 | using System.Net.Sockets;
7 |
8 | namespace WebSockets.Exceptions
9 | {
10 | [Serializable]
11 | public class ServerListenerSocketException : Exception
12 | {
13 | public ServerListenerSocketException() : base()
14 | {
15 |
16 | }
17 |
18 | public ServerListenerSocketException(string message) : base(message)
19 | {
20 |
21 | }
22 |
23 | public ServerListenerSocketException(string message, Exception inner) : base(message, inner)
24 | {
25 |
26 | }
27 |
28 | public ServerListenerSocketException(SerializationInfo info, StreamingContext context) : base(info, context)
29 | {
30 |
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/WebSockets/Exceptions/TODO.txt:
--------------------------------------------------------------------------------
1 | Make sure that exceptions follow the microsoft standards
--------------------------------------------------------------------------------
/WebSockets/Exceptions/WebSocketHandshakeFailedException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Runtime.Serialization;
5 | using System.Text;
6 |
7 | namespace WebSockets.Exceptions
8 | {
9 | [Serializable]
10 | public class WebSocketHandshakeFailedException : Exception
11 | {
12 | public WebSocketHandshakeFailedException() : base()
13 | {
14 |
15 | }
16 |
17 | public WebSocketHandshakeFailedException(string message) : base(message)
18 | {
19 |
20 | }
21 |
22 | public WebSocketHandshakeFailedException(string message, Exception inner) : base(message, inner)
23 | {
24 |
25 | }
26 |
27 | public WebSocketHandshakeFailedException(SerializationInfo info, StreamingContext context) : base(info, context)
28 | {
29 |
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/WebSockets/Exceptions/WebSocketVersionNotSupportedException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Runtime.Serialization;
6 |
7 | namespace WebSockets.Exceptions
8 | {
9 | [Serializable]
10 | public class WebSocketVersionNotSupportedException : Exception
11 | {
12 | public WebSocketVersionNotSupportedException() : base()
13 | {
14 |
15 | }
16 |
17 | public WebSocketVersionNotSupportedException(string message) : base(message)
18 | {
19 |
20 | }
21 |
22 | public WebSocketVersionNotSupportedException(string message, Exception inner) : base(message, inner)
23 | {
24 |
25 | }
26 |
27 | public WebSocketVersionNotSupportedException(SerializationInfo info, StreamingContext context) : base(info, context)
28 | {
29 |
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/WebSockets/MimeTypes.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/WebSockets/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("WebSockets")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("WebSockets")]
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("f1b075ca-df2d-46ea-84ba-8592ae2e570c")]
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 |
--------------------------------------------------------------------------------
/WebSockets/README.txt:
--------------------------------------------------------------------------------
1 | Thanks to help from the following websites:
2 |
3 | Step by step guide
4 | https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
5 |
6 | The official Web Socket spec
7 | http://tools.ietf.org/html/rfc6455
8 |
9 | Some useful stuff in c#
10 | https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_server
11 |
12 | Web Socket Protocol 13 (and above) supported
13 |
14 | To run:
15 | Run the console application
16 | NOTE You will get a firewall warning because you are listening on a port. This is normal for any socket based server.
17 | The console application will run a self test so you can see if anything is wrong. The self test will open a connection, perform the open handshake, send a message, receive a response and start the close handshake and disconnect.
18 |
19 | If the self test is a success then the following should work too:
20 | Open a browser and enter: http://localhost/client.html
21 | The web server will then serve up the web page requested (client.html),
22 | The javascript in that webpage will execute and attempt to make a WebSocket connection to that same server and port (80)
23 | At this point the webserver will upgrade the connection to a web socket connection and you can chat with the server
24 | If you want to access this from another machine then make sure your firewall is not blocking the port
25 |
26 | Note:
27 | A lot of the Web Socket examples out there are for old Web Socket versions and included complicated code for fall back communication.
28 | All modern browsers (including safari on an iphone) support at least version 13 of the Web Socket protocol so I'd rather not complicate things.
29 | This application serves up basic html pages as well as handling WebSocket connections.
30 | This may seem confusing but it allows you to send the client the html they need to make a web socket connection and also allows you to share the same port
31 | However, the HttpConnection is very rudimentary. I'm sure it has some glaring security problems. It was just made to make this demo easier to run. Replace it with your own or dont use it.
32 |
33 | Debugging:
34 | A good place to put a breakpoint is in the WebServer class in the HandleAsyncConnection function.
35 | Note that this is a multithreaded server to you may want to freeze threads if this gets confusing. The console output prints the thread id to make things easier
36 | If you want to skip past all the plumbing then another good place to start is the Respond function in the WebSocketService class
37 | If you are not interested in the inner workings of Web Sockets and just want to use them then take a look at the OnTextFrame in the ChatWebSocketService class
38 |
39 | Problems with Proxy Servers:
40 | Proxy servers which have not been configured to support Web sockets will not work well with them.
41 | I suggest that you use transport layer security (SSL) if you want this to work across the wider internet especially from within a corporation
42 |
43 | Sub Folders:
44 | You can assign different web socket handlers depending on the folder attribute of the first line of the http request
45 | eg:
46 | GET /chat HTTP/1.1
47 | This folder has been setup to use the ChatWebSocketService class
48 |
49 | Change Log:
50 |
51 | 10 Apr 2016: SSL support
52 | 02 Apr 2016: c# Client
53 |
--------------------------------------------------------------------------------
/WebSockets/Server/ConnectionDetails.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Net.Sockets;
6 | using System.IO;
7 |
8 | namespace WebSockets.Server
9 | {
10 | public class ConnectionDetails
11 | {
12 | public Stream Stream { get; private set; }
13 | public TcpClient TcpClient { get; private set; }
14 | public ConnectionType ConnectionType { get; private set; }
15 | public string Header { get; private set; }
16 |
17 | // this is the path attribute in the first line of the http header
18 | public string Path { get; private set; }
19 |
20 | public ConnectionDetails (Stream stream, TcpClient tcpClient, string path, ConnectionType connectionType, string header)
21 | {
22 | Stream = stream;
23 | TcpClient = tcpClient;
24 | Path = path;
25 | ConnectionType = connectionType;
26 | Header = header;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/WebSockets/Server/ConnectionType.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace WebSockets.Server
7 | {
8 | public enum ConnectionType
9 | {
10 | Http,
11 | WebSocket,
12 | Unknown
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/WebSockets/Server/Http/BadRequestService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Net.Sockets;
7 | using WebSockets.Common;
8 | using System.IO;
9 |
10 | namespace WebSockets.Server.Http
11 | {
12 | public class BadRequestService : IService
13 | {
14 | private readonly Stream _stream;
15 | private readonly string _header;
16 | private readonly IWebSocketLogger _logger;
17 |
18 | public BadRequestService(Stream stream, string header, IWebSocketLogger logger)
19 | {
20 | _stream = stream;
21 | _header = header;
22 | _logger = logger;
23 | }
24 |
25 | public void Respond()
26 | {
27 | HttpHelper.WriteHttpHeader("HTTP/1.1 400 Bad Request", _stream);
28 |
29 | // limit what we log. Headers can be up to 16K in size
30 | string header = _header.Length > 255 ? _header.Substring(0,255) + "..." : _header;
31 | _logger.Warning(this.GetType(), "Bad request: '{0}'", header);
32 | }
33 |
34 | public void Dispose()
35 | {
36 | // do nothing
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/WebSockets/Server/Http/HttpHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.IO;
6 | using System.Net.Sockets;
7 | using System.Text.RegularExpressions;
8 | using System.Diagnostics;
9 | using System.Threading;
10 | using WebSockets.Exceptions;
11 |
12 | namespace WebSockets.Server.Http
13 | {
14 | public class HttpHelper
15 | {
16 | public static string ReadHttpHeader(Stream stream)
17 | {
18 | int length = 1024*16; // 16KB buffer more than enough for http header
19 | byte[] buffer = new byte[length];
20 | int offset = 0;
21 | int bytesRead = 0;
22 | do
23 | {
24 | if (offset >= length)
25 | {
26 | throw new EntityTooLargeException("Http header message too large to fit in buffer (16KB)");
27 | }
28 |
29 | bytesRead = stream.Read(buffer, offset, length - offset);
30 | offset += bytesRead;
31 | string header = Encoding.UTF8.GetString(buffer, 0, offset);
32 |
33 | // as per http specification, all headers should end this this
34 | if (header.Contains("\r\n\r\n"))
35 | {
36 | return header;
37 | }
38 |
39 | } while (bytesRead > 0);
40 |
41 | return string.Empty;
42 | }
43 |
44 | public static void WriteHttpHeader(string response, Stream stream)
45 | {
46 | response = response.Trim() + Environment.NewLine + Environment.NewLine;
47 | Byte[] bytes = Encoding.UTF8.GetBytes(response);
48 | stream.Write(bytes, 0, bytes.Length);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/WebSockets/Server/Http/HttpService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Net.Sockets;
6 | using System.Text;
7 | using System.IO;
8 | using System.Reflection;
9 | using System.Configuration;
10 | using System.Xml;
11 | using WebSockets.Common;
12 |
13 | namespace WebSockets.Server.Http
14 | {
15 | public class HttpService : IService
16 | {
17 | private readonly Stream _stream;
18 | private readonly string _path;
19 | private readonly string _webRoot;
20 | private readonly IWebSocketLogger _logger;
21 | private readonly MimeTypes _mimeTypes;
22 |
23 | public HttpService(Stream stream, string path, string webRoot, IWebSocketLogger logger)
24 | {
25 | _stream = stream;
26 | _path = path;
27 | _webRoot = webRoot;
28 | _logger = logger;
29 | _mimeTypes = MimeTypesFactory.GetMimeTypes(webRoot);
30 | }
31 |
32 | private static bool IsDirectory(string file)
33 | {
34 | if (Directory.Exists(file))
35 | {
36 | //detect whether its a directory or file
37 | FileAttributes attr = File.GetAttributes(file);
38 | return ((attr & FileAttributes.Directory) == FileAttributes.Directory);
39 | }
40 |
41 | return false;
42 | }
43 |
44 | public void Respond()
45 | {
46 | _logger.Information(this.GetType(), "Request: {0}", _path);
47 | string file = GetSafePath(_path);
48 |
49 | // default to index.html is path is supplied
50 | if (IsDirectory(file))
51 | {
52 | file += "index.html";
53 | }
54 |
55 | FileInfo fi = new FileInfo(file);
56 |
57 | if (fi.Exists)
58 | {
59 | string ext = fi.Extension.ToLower();
60 |
61 | string contentType;
62 | if (_mimeTypes.TryGetValue(ext, out contentType))
63 | {
64 | Byte[] bytes = File.ReadAllBytes(fi.FullName);
65 | RespondSuccess(contentType, bytes.Length);
66 | _stream.Write(bytes, 0, bytes.Length);
67 | _logger.Information(this.GetType(), "Served file: {0}", file);
68 | }
69 | else
70 | {
71 | RespondMimeTypeFailure(file);
72 | }
73 | }
74 | else
75 | {
76 | RespondNotFoundFailure(file);
77 | }
78 | }
79 |
80 | ///
81 | /// I am not convinced that this function is indeed safe from hacking file path tricks
82 | ///
83 | /// The relative path
84 | /// The file system path
85 | private string GetSafePath(string path)
86 | {
87 | path = path.Trim().Replace("/", "\\");
88 | if (path.Contains("..") || !path.StartsWith("\\") || path.Contains(":"))
89 | {
90 | return string.Empty;
91 | }
92 |
93 | string file = _webRoot + path;
94 | return file;
95 | }
96 |
97 | public void RespondMimeTypeFailure(string file)
98 | {
99 | HttpHelper.WriteHttpHeader("415 Unsupported Media Type", _stream);
100 | _logger.Warning(this.GetType(), "File extension not found MimeTypes.config: {0}", file);
101 | }
102 |
103 | public void RespondNotFoundFailure(string file)
104 | {
105 | HttpHelper.WriteHttpHeader("HTTP/1.1 404 Not Found", _stream);
106 | _logger.Information(this.GetType(), "File not found: {0}", file);
107 | }
108 |
109 | public void RespondSuccess(string contentType, int contentLength)
110 | {
111 | string response = "HTTP/1.1 200 OK" + Environment.NewLine +
112 | "Content-Type: " + contentType + Environment.NewLine +
113 | "Content-Length: " + contentLength + Environment.NewLine +
114 | "Connection: close";
115 | HttpHelper.WriteHttpHeader(response, _stream);
116 | }
117 |
118 | public void Dispose()
119 | {
120 | // do nothing. The network stream will be closed by the WebServer
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/WebSockets/Server/Http/MimeTypes.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Configuration;
4 | using System.Linq;
5 | using System.Text;
6 | using System.IO;
7 | using System.Reflection;
8 | using System.Xml;
9 |
10 | namespace WebSockets.Server.Http
11 | {
12 | class MimeTypes : Dictionary
13 | {
14 | public MimeTypes(string webRoot)
15 | {
16 | string configFileName = webRoot + @"\MimeTypes.config";
17 | if (!File.Exists(configFileName))
18 | {
19 | throw new FileNotFoundException("Mime Types config file not found: " + configFileName);
20 | }
21 |
22 | try
23 | {
24 | XmlDocument document = new XmlDocument();
25 | document.Load(configFileName);
26 | foreach (XmlNode node in document.SelectNodes("configuration/system.webServer/staticContent/mimeMap"))
27 | {
28 | string fileExtension = node.Attributes["fileExtension"].Value;
29 | string mimeType = node.Attributes["mimeType"].Value;
30 | this.Add(fileExtension, mimeType);
31 | }
32 | }
33 | catch (Exception ex)
34 | {
35 | throw new ConfigurationErrorsException("Invalid Mime Types configuration file: " + configFileName, ex);
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/WebSockets/Server/Http/MimeTypesFactory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace WebSockets.Server.Http
7 | {
8 | internal class MimeTypesFactory
9 | {
10 | private static Dictionary _mimeTypes = new Dictionary();
11 |
12 | public static MimeTypes GetMimeTypes(string webRoot)
13 | {
14 | lock (_mimeTypes)
15 | {
16 | MimeTypes mimeTypes;
17 | if (!_mimeTypes.TryGetValue(webRoot, out mimeTypes))
18 | {
19 | mimeTypes = new MimeTypes(webRoot);
20 | _mimeTypes.Add(webRoot, mimeTypes);
21 | }
22 |
23 | return mimeTypes;
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/WebSockets/Server/IService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace WebSockets.Server
7 | {
8 | public interface IService : IDisposable
9 | {
10 | ///
11 | /// Sends data back to the client. This is built using the IConnectionFactory
12 | ///
13 | void Respond();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/WebSockets/Server/IServiceFactory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace WebSockets.Server
7 | {
8 | ///
9 | /// Implement this to decide what connection to use based on the http header
10 | ///
11 | public interface IServiceFactory
12 | {
13 | IService CreateInstance(ConnectionDetails connectionDetails);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/WebSockets/Server/WebServer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Net.Sockets;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using System.Text.RegularExpressions;
10 | using System.Security.Cryptography;
11 | using System.Threading;
12 | using System.Collections;
13 | using System.Diagnostics;
14 | using WebSockets.Exceptions;
15 | using WebSockets.Server;
16 | using WebSockets.Server.Http;
17 | using WebSockets.Common;
18 | using System.Net.Security;
19 | using System.Security.Authentication;
20 | using System.Security.Cryptography.X509Certificates;
21 |
22 | namespace WebSockets
23 | {
24 | public class WebServer : IDisposable
25 | {
26 | // maintain a list of open connections so that we can notify the client if the server shuts down
27 | private readonly List _openConnections;
28 | private readonly IServiceFactory _serviceFactory;
29 | private readonly IWebSocketLogger _logger;
30 | private X509Certificate2 _sslCertificate;
31 | private TcpListener _listener;
32 | private bool _isDisposed = false;
33 |
34 | public WebServer(IServiceFactory serviceFactory, IWebSocketLogger logger)
35 | {
36 | _serviceFactory = serviceFactory;
37 | _logger = logger;
38 | _openConnections = new List();
39 | }
40 |
41 | public void Listen(int port, X509Certificate2 sslCertificate)
42 | {
43 | try
44 | {
45 | _sslCertificate = sslCertificate;
46 | IPAddress localAddress = IPAddress.Any;
47 | _listener = new TcpListener(localAddress, port);
48 | _listener.Start();
49 | _logger.Information(this.GetType(), "Server started listening on port {0}", port);
50 | StartAccept();
51 | }
52 | catch (SocketException ex)
53 | {
54 | string message = string.Format("Error listening on port {0}. Make sure IIS or another application is not running and consuming your port.", port);
55 | throw new ServerListenerSocketException(message, ex);
56 | }
57 | }
58 |
59 | ///
60 | /// Listens on the port specified
61 | ///
62 | public void Listen(int port)
63 | {
64 | Listen(port, null);
65 | }
66 |
67 | ///
68 | /// Gets the first available port and listens on it. Returns the port
69 | ///
70 | public int Listen()
71 | {
72 | IPAddress localAddress = IPAddress.Any;
73 | _listener = new TcpListener(localAddress, 0);
74 | _listener.Start();
75 | StartAccept();
76 | int port = ((IPEndPoint) _listener.LocalEndpoint).Port;
77 | _logger.Information(this.GetType(), "Server started listening on port {0}", port);
78 | return port;
79 | }
80 |
81 | private void StartAccept()
82 | {
83 | // this is a non-blocking operation. It will consume a worker thread from the threadpool
84 | _listener.BeginAcceptTcpClient(new AsyncCallback(HandleAsyncConnection), null);
85 | }
86 |
87 | private static ConnectionDetails GetConnectionDetails(Stream stream, TcpClient tcpClient)
88 | {
89 | // read the header and check that it is a GET request
90 | string header = HttpHelper.ReadHttpHeader(stream);
91 | Regex getRegex = new Regex(@"^GET(.*)HTTP\/1\.1", RegexOptions.IgnoreCase);
92 |
93 | Match getRegexMatch = getRegex.Match(header);
94 | if (getRegexMatch.Success)
95 | {
96 | // extract the path attribute from the first line of the header
97 | string path = getRegexMatch.Groups[1].Value.Trim();
98 |
99 | // check if this is a web socket upgrade request
100 | Regex webSocketUpgradeRegex = new Regex("Upgrade: websocket", RegexOptions.IgnoreCase);
101 | Match webSocketUpgradeRegexMatch = webSocketUpgradeRegex.Match(header);
102 |
103 | if (webSocketUpgradeRegexMatch.Success)
104 | {
105 | return new ConnectionDetails(stream, tcpClient, path, ConnectionType.WebSocket, header);
106 | }
107 | else
108 | {
109 | return new ConnectionDetails(stream, tcpClient, path, ConnectionType.Http, header);
110 | }
111 | }
112 | else
113 | {
114 | return new ConnectionDetails(stream, tcpClient, string.Empty, ConnectionType.Unknown, header);
115 | }
116 | }
117 |
118 | private Stream GetStream(TcpClient tcpClient)
119 | {
120 | Stream stream = tcpClient.GetStream();
121 |
122 | // we have no ssl certificate
123 | if (_sslCertificate == null)
124 | {
125 | _logger.Information(this.GetType(), "Connection not secure");
126 | return stream;
127 | }
128 |
129 | try
130 | {
131 | SslStream sslStream = new SslStream(stream, false);
132 | _logger.Information(this.GetType(), "Attempting to secure connection...");
133 | sslStream.AuthenticateAsServer(_sslCertificate, false, SslProtocols.Tls, true);
134 | _logger.Information(this.GetType(), "Connection successfully secured");
135 | return sslStream;
136 | }
137 | catch (AuthenticationException e)
138 | {
139 | // TODO: send 401 Unauthorized
140 | throw;
141 | }
142 | }
143 |
144 | private void HandleAsyncConnection(IAsyncResult res)
145 | {
146 | try
147 | {
148 | if (_isDisposed)
149 | {
150 | return;
151 | }
152 |
153 | // this worker thread stays alive until either of the following happens:
154 | // Client sends a close conection request OR
155 | // An unhandled exception is thrown OR
156 | // The server is disposed
157 | using (TcpClient tcpClient = _listener.EndAcceptTcpClient(res))
158 | {
159 | // we are ready to listen for more connections (on another thread)
160 | StartAccept();
161 | _logger.Information(this.GetType(), "Server: Connection opened");
162 |
163 | // get a secure or insecure stream
164 | Stream stream = GetStream(tcpClient);
165 |
166 | // extract the connection details and use those details to build a connection
167 | ConnectionDetails connectionDetails = GetConnectionDetails(stream, tcpClient);
168 | using (IService service = _serviceFactory.CreateInstance(connectionDetails))
169 | {
170 | try
171 | {
172 | // record the connection so we can close it if something goes wrong
173 | lock (_openConnections)
174 | {
175 | _openConnections.Add(service);
176 | }
177 |
178 | // respond to the http request.
179 | // Take a look at the WebSocketConnection or HttpConnection classes
180 | service.Respond();
181 | }
182 | finally
183 | {
184 | // forget the connection, we are done with it
185 | lock (_openConnections)
186 | {
187 | _openConnections.Remove(service);
188 | }
189 | }
190 | }
191 | }
192 |
193 | _logger.Information(this.GetType(), "Server: Connection closed");
194 | }
195 | catch (ObjectDisposedException)
196 | {
197 | // do nothing. This will be thrown if the Listener has been stopped
198 | }
199 | catch (Exception ex)
200 | {
201 | _logger.Error(this.GetType(), ex);
202 | }
203 | }
204 |
205 | private void CloseAllConnections()
206 | {
207 | IDisposable[] openConnections;
208 |
209 | lock (_openConnections)
210 | {
211 | openConnections = _openConnections.ToArray();
212 | _openConnections.Clear();
213 | }
214 |
215 | // safely attempt to close each connection
216 | foreach (IDisposable openConnection in openConnections)
217 | {
218 | try
219 | {
220 | openConnection.Dispose();
221 | }
222 | catch (Exception ex)
223 | {
224 | _logger.Error(this.GetType(), ex);
225 | }
226 | }
227 | }
228 |
229 | public void Dispose()
230 | {
231 | if (!_isDisposed)
232 | {
233 | _isDisposed = true;
234 |
235 | // safely attempt to shut down the listener
236 | try
237 | {
238 | if (_listener != null)
239 | {
240 | if (_listener.Server != null)
241 | {
242 | _listener.Server.Close();
243 | }
244 |
245 | _listener.Stop();
246 | }
247 | }
248 | catch (Exception ex)
249 | {
250 | _logger.Error(this.GetType(), ex);
251 | }
252 |
253 | CloseAllConnections();
254 | _logger.Information(this.GetType(), "Web Server disposed");
255 | }
256 | }
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/WebSockets/Server/WebSocket/WebSocketService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Security.Cryptography;
5 | using System.Text;
6 | using System.Net.Sockets;
7 | using System.Text.RegularExpressions;
8 | using System.Diagnostics;
9 | using System.Threading;
10 | using WebSockets.Common;
11 | using WebSockets.Exceptions;
12 | using WebSockets.Server.Http;
13 | using System.IO;
14 | using WebSockets.Events;
15 |
16 | namespace WebSockets.Server.WebSocket
17 | {
18 | public class WebSocketService : WebSocketBase, IService
19 | {
20 | private readonly Stream _stream;
21 | private readonly string _header;
22 | private readonly IWebSocketLogger _logger;
23 | private readonly TcpClient _tcpClient;
24 | private bool _isDisposed = false;
25 |
26 | public WebSocketService(Stream stream, TcpClient tcpClient, string header, bool noDelay, IWebSocketLogger logger)
27 | : base(logger)
28 | {
29 | _stream = stream;
30 | _header = header;
31 | _logger = logger;
32 | _tcpClient = tcpClient;
33 |
34 | // send requests immediately if true (needed for small low latency packets but not a long stream).
35 | // Basically, dont wait for the buffer to be full before before sending the packet
36 | tcpClient.NoDelay = noDelay;
37 | }
38 |
39 | public void Respond()
40 | {
41 | base.OpenBlocking(_stream, _tcpClient.Client);
42 | }
43 |
44 | protected override void PerformHandshake(Stream stream)
45 | {
46 | string header = _header;
47 |
48 | try
49 | {
50 | Regex webSocketKeyRegex = new Regex("Sec-WebSocket-Key: (.*)");
51 | Regex webSocketVersionRegex = new Regex("Sec-WebSocket-Version: (.*)");
52 |
53 | // check the version. Support version 13 and above
54 | const int WebSocketVersion = 13;
55 | int secWebSocketVersion = Convert.ToInt32(webSocketVersionRegex.Match(header).Groups[1].Value.Trim());
56 | if (secWebSocketVersion < WebSocketVersion)
57 | {
58 | throw new WebSocketVersionNotSupportedException(string.Format("WebSocket Version {0} not suported. Must be {1} or above", secWebSocketVersion, WebSocketVersion));
59 | }
60 |
61 | string secWebSocketKey = webSocketKeyRegex.Match(header).Groups[1].Value.Trim();
62 | string setWebSocketAccept = base.ComputeSocketAcceptString(secWebSocketKey);
63 | string response = ("HTTP/1.1 101 Switching Protocols" + Environment.NewLine
64 | + "Connection: Upgrade" + Environment.NewLine
65 | + "Upgrade: websocket" + Environment.NewLine
66 | + "Sec-WebSocket-Accept: " + setWebSocketAccept);
67 |
68 | HttpHelper.WriteHttpHeader(response, stream);
69 | _logger.Information(this.GetType(), "Web Socket handshake sent");
70 | }
71 | catch (WebSocketVersionNotSupportedException ex)
72 | {
73 | string response = "HTTP/1.1 426 Upgrade Required" + Environment.NewLine + "Sec-WebSocket-Version: 13";
74 | HttpHelper.WriteHttpHeader(response, stream);
75 | throw;
76 | }
77 | catch (Exception ex)
78 | {
79 | HttpHelper.WriteHttpHeader("HTTP/1.1 400 Bad Request", stream);
80 | throw;
81 | }
82 | }
83 |
84 | private static void CloseConnection(Socket socket)
85 | {
86 | socket.Shutdown(SocketShutdown.Both);
87 | socket.Close();
88 | }
89 |
90 | public virtual void Dispose()
91 | {
92 | // send special web socket close message. Don't close the network stream, it will be disposed later
93 | if (_stream.CanWrite && !_isDisposed)
94 | {
95 | using (MemoryStream stream = new MemoryStream())
96 | {
97 | // set the close reason to Normal
98 | BinaryReaderWriter.WriteUShort((ushort) WebSocketCloseCode.Normal, stream, false);
99 |
100 | // send close message to client to begin the close handshake
101 | Send(WebSocketOpCode.ConnectionClose, stream.ToArray());
102 | }
103 |
104 | _isDisposed = true;
105 | _logger.Information(this.GetType(), "Sent web socket close message to client");
106 | CloseConnection(_tcpClient.Client);
107 | }
108 | }
109 |
110 | protected override void OnConnectionClose(byte[] payload)
111 | {
112 | Send(WebSocketOpCode.ConnectionClose, payload);
113 | _logger.Information(this.GetType(), "Sent response close message to client");
114 | _isDisposed = true;
115 |
116 | base.OnConnectionClose(payload);
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/WebSockets/WebSockets.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | 8.0.30703
7 | 2.0
8 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}
9 | Library
10 | Properties
11 | WebSockets
12 | WebSockets
13 | v4.0
14 | 512
15 |
16 |
17 | true
18 | full
19 | false
20 | bin\Debug\
21 | DEBUG;TRACE
22 | prompt
23 | 4
24 |
25 |
26 | pdbonly
27 | true
28 | bin\Release\
29 | TRACE
30 | prompt
31 | 4
32 |
33 |
34 |
35 |
36 |
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 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | PreserveNewest
82 |
83 |
84 |
85 |
92 |
--------------------------------------------------------------------------------
/WebSocketsCmd/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | C:\Projects\WebGrid\WebGrid1
22 |
23 |
24 | 80
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/WebSocketsCmd/Client/ChatWebSocketClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using WebSockets.Client;
6 | using WebSockets.Common;
7 |
8 | namespace WebSocketsCmd.Client
9 | {
10 | class ChatWebSocketClient : WebSocketClient
11 | {
12 | public ChatWebSocketClient(bool noDelay, IWebSocketLogger logger) : base(noDelay, logger)
13 | {
14 |
15 | }
16 |
17 | public void Send(string text)
18 | {
19 | byte[] buffer = Encoding.UTF8.GetBytes(text);
20 | Send(WebSocketOpCode.TextFrame, buffer);
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/WebSocketsCmd/CustomConsoleTraceListener.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Diagnostics;
6 | using System.Threading;
7 |
8 | namespace WebSocketsCmd
9 | {
10 | public class CustomConsoleTraceListener : TraceListener
11 | {
12 | public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args)
13 | {
14 | string message = string.Format(format, args);
15 |
16 | // write the localised date and time but include the time zone in brackets (good for combining logs from different timezones)
17 | TimeSpan utcOffset = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now);
18 | string plusOrMinus = (utcOffset < TimeSpan.Zero) ? "-" : "+";
19 | string utcHourOffset = utcOffset.TotalHours == 0 ? string.Empty : string.Format(" ({0}{1:hh})", plusOrMinus, utcOffset);
20 | string dateWithOffset = string.Format(@"{0:yyyy/MM/dd HH:mm:ss.fff}{1}", DateTime.Now, utcHourOffset);
21 |
22 | // display the threadid
23 | string log = string.Format(@"{0} [{1}] {2}", dateWithOffset, Thread.CurrentThread.ManagedThreadId, message);
24 |
25 | switch (eventType)
26 | {
27 | case TraceEventType.Critical:
28 | case TraceEventType.Error:
29 | Console.ForegroundColor = ConsoleColor.Red;
30 | Console.WriteLine(log);
31 | Console.ResetColor();
32 | break;
33 |
34 | case TraceEventType.Warning:
35 | Console.ForegroundColor = ConsoleColor.Yellow;
36 | Console.WriteLine(log);
37 | Console.ResetColor();
38 | break;
39 |
40 | default:
41 | Console.WriteLine(log);
42 | break;
43 | }
44 | }
45 |
46 | public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message)
47 | {
48 | this.TraceEvent(eventCache, source, eventType, id, message, new object[] {});
49 | }
50 |
51 | public override void WriteLine(string message)
52 | {
53 | Console.WriteLine(message);
54 | }
55 |
56 | public override void Write(string message)
57 | {
58 | Console.Write(message);
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/WebSocketsCmd/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Net;
6 | using WebSockets.Server;
7 | using System.Diagnostics;
8 | using WebSocketsCmd.Client;
9 | using WebSocketsCmd.Properties;
10 | using WebSockets.Client;
11 | using System.Threading.Tasks;
12 | using WebSockets.Common;
13 | using System.Threading;
14 | using WebSockets;
15 | using WebSockets.Events;
16 | using WebSocketsCmd.Server;
17 | using System.IO;
18 |
19 | namespace WebSocketsCmd
20 | {
21 | public class Program
22 | {
23 | private static void TestClient(object state)
24 | {
25 | var logger = (IWebSocketLogger) state;
26 | using (var client = new ChatWebSocketClient(true, logger))
27 | {
28 | Uri uri = new Uri("ws://localhost/chat");
29 | client.TextFrame += Client_TextFrame;
30 | client.ConnectionOpened += Client_ConnectionOpened;
31 |
32 | // test the open handshake
33 | client.OpenBlocking(uri);
34 | }
35 |
36 | Trace.TraceInformation("Client finished, press any key");
37 | Console.ReadKey();
38 | }
39 |
40 | private static void Client_ConnectionOpened(object sender, EventArgs e)
41 | {
42 | Trace.TraceInformation("Client: Connection Opened");
43 | var client = (ChatWebSocketClient) sender;
44 |
45 | // test sending a message to the server
46 | client.Send("Hi");
47 | }
48 |
49 | private static void Client_TextFrame(object sender, TextFrameEventArgs e)
50 | {
51 | Trace.TraceInformation("Client: {0}", e.Text);
52 | var client = (ChatWebSocketClient) sender;
53 |
54 | // lets test the close handshake
55 | client.Dispose();
56 | }
57 |
58 | private static void Main(string[] args)
59 | {
60 | IWebSocketLogger logger = new WebSocketLogger();
61 |
62 | try
63 | {
64 | int port = Settings.Default.Port;
65 | string webRoot = Settings.Default.WebRoot;
66 | if (!Directory.Exists(webRoot))
67 | {
68 | string baseFolder = AppDomain.CurrentDomain.BaseDirectory;
69 | logger.Warning(typeof(Program), "Webroot folder {0} not found. Using application base directory: {1}", webRoot, baseFolder);
70 | webRoot = baseFolder;
71 | }
72 |
73 | // used to decide what to do with incoming connections
74 | ServiceFactory serviceFactory = new ServiceFactory(webRoot, logger);
75 |
76 | using (WebServer server = new WebServer(serviceFactory, logger))
77 | {
78 | server.Listen(port);
79 | Thread clientThread = new Thread(new ParameterizedThreadStart(TestClient));
80 | clientThread.IsBackground = false;
81 | clientThread.Start(logger);
82 | Console.ReadKey();
83 | }
84 | }
85 | catch (Exception ex)
86 | {
87 | logger.Error(typeof(Program), ex);
88 | Console.ReadKey();
89 | }
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/WebSocketsCmd/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("WebSocketsCmd")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("WebSocketsCmd")]
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("a718761f-4d28-4477-ba96-3483f93d61a3")]
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 |
--------------------------------------------------------------------------------
/WebSocketsCmd/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace WebSocketsCmd.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "10.0.0.0")]
16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 |
26 | [global::System.Configuration.UserScopedSettingAttribute()]
27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
28 | [global::System.Configuration.DefaultSettingValueAttribute("C:\\Projects\\WebGrid\\WebGrid")]
29 | public string WebRoot {
30 | get {
31 | return ((string)(this["WebRoot"]));
32 | }
33 | set {
34 | this["WebRoot"] = value;
35 | }
36 | }
37 |
38 | [global::System.Configuration.UserScopedSettingAttribute()]
39 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
40 | [global::System.Configuration.DefaultSettingValueAttribute("80")]
41 | public int Port {
42 | get {
43 | return ((int)(this["Port"]));
44 | }
45 | set {
46 | this["Port"] = value;
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/WebSocketsCmd/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | C:\Projects\WebGrid\WebGrid
7 |
8 |
9 | 80
10 |
11 |
12 |
--------------------------------------------------------------------------------
/WebSocketsCmd/README.md:
--------------------------------------------------------------------------------
1 | WebSocket Server in c#
2 |
3 | Set WebSockets.Cmd
as the startup project
4 |
5 | License
6 |
7 | The MIT License (MIT)
8 |
See LICENCE.txt
9 |
10 | Introduction
11 |
12 | A lot of the Web Socket examples out there are for old Web Socket versions and included complicated code (and external libraries) for fall back communication. All modern browsers that anyone cares about (including safari on an iphone) support at least version 13 of the Web Socket protocol so I'd rather not complicate things. This is a bare bones implementation of the web socket protocol in C# with no external libraries involved. You can connect using standard HTML5 JavaScript.
13 |
14 | This application serves up basic html pages as well as handling WebSocket connections. This may seem confusing but it allows you to send the client the html they need to make a web socket connection and also allows you to share the same port. However, the HttpConnection
is very rudimentary. I'm sure it has some glaring security problems. It was just made to make this demo easier to run. Replace it with your own or don't use it.
15 |
16 | Background
17 |
18 | There is nothing magical about Web Sockets. The spec is easy to follow and there is no need to use special libraries. At one point, I was even considering somehow communicating with Node.js but that is not necessary. The spec can be a bit fiddly with bits and bytes but this was probably done to keep the overheads low. This is my first CodeProject article and I hope you will find it easy to follow. The following links offer some great advice:
19 |
20 | Step by step guide
21 |
22 |
25 |
26 | The official Web Socket spec
27 |
28 |
31 |
32 | Some useful stuff in C#
33 |
34 |
37 |
38 | Using the Code
39 |
40 | NOTE You will get a firewall warning because you are listening on a port. This is normal for any socket based server.
41 |
A good place to put a breakpoint is in the WebServer
class in the HandleAsyncConnection
function. Note that this is a multithreaded server so you may want to freeze threads if this gets confusing. The console output prints the thread id to make things easier. If you want to skip past all the plumbing, then another good place to start is the Respond
function in the WebSocketConnection
class. If you are not interested in the inner workings of Web Sockets and just want to use them, then take a look at the OnTextFrame
in the ChatWebSocketConnection
class. See below.
42 |
43 | Implementation of a chat web socket connection is as follows:
44 |
45 |
46 | internal class ChatWebSocketService : WebSocketService
47 | {
48 | private readonly IWebSocketLogger _logger;
49 |
50 | public ChatWebSocketService(NetworkStream networkStream, TcpClient tcpClient, string header, IWebSocketLogger logger)
51 | : base(networkStream, tcpClient, header, true, logger)
52 | {
53 | _logger = logger;
54 | }
55 |
56 | protected override void OnTextFrame(string text)
57 | {
58 | string response = "ServerABC: " + text;
59 | base.Send(response);
60 | _logger.Information(this.GetType(), response);
61 | }
62 | }
63 |
64 | The factory used to create the connection is as follows:
65 |
66 |
67 | internal class ServiceFactory : IServiceFactory
68 | {
69 | public ServiceFactory(string webRoot, IWebSocketLogger logger)
70 | {
71 | _logger = logger;
72 | _webRoot = webRoot;
73 | }
74 |
75 | public IService CreateInstance(ConnectionDetails connectionDetails)
76 | {
77 | switch (connectionDetails.ConnectionType)
78 | {
79 | case ConnectionType.WebSocket:
80 | // you can support different kinds of web socket connections using a different path
81 | if (connectionDetails.Path == "/chat")
82 | {
83 | return new ChatWebSocketService(connectionDetails.NetworkStream, connectionDetails.TcpClient, connectionDetails.Header, _logger);
84 | }
85 | break;
86 | case ConnectionType.Http:
87 | // this path actually refers to the reletive location of some html file or image
88 | return new HttpService(connectionDetails.NetworkStream, connectionDetails.Path, _webRoot, _logger);
89 | }
90 |
91 | return new BadRequestService(connectionDetails.NetworkStream, connectionDetails.Header, _logger);
92 | }
93 | }
94 |
95 | HTML5 JavaScript used to connect:
96 |
97 |
98 | // open the connection to the Web Socket server
99 | var CONNECTION = new WebSocket('ws://localhost/chat');
100 |
101 | // Log messages from the server
102 | CONNECTION.onmessage = function (e) {
103 | console.log(e.data);
104 | };
105 |
106 | CONNECTION.send('Hellow World');
107 |
108 | Starting the server and the test client:
109 |
110 |
111 | private static void Main(string[] args)
112 | {
113 | IWebSocketLogger logger = new WebSocketLogger();
114 |
115 | try
116 | {
117 | string webRoot = Settings.Default.WebRoot;
118 | int port = Settings.Default.Port;
119 |
120 | // used to decide what to do with incoming connections
121 | ServiceFactory serviceFactory = new ServiceFactory(webRoot, logger);
122 |
123 | using (WebServer server = new WebServer(serviceFactory, logger))
124 | {
125 | server.Listen(port);
126 | Thread clientThread = new Thread(new ParameterizedThreadStart(TestClient));
127 | clientThread.IsBackground = false;
128 | clientThread.Start(logger);
129 | Console.ReadKey();
130 | }
131 | }
132 | catch (Exception ex)
133 | {
134 | logger.Error(null, ex);
135 | Console.ReadKey();
136 | }
137 | }
138 |
139 | The test client runs a short self test to make sure that everything is fine. Opening and closing handshakes are tested here.
140 |
141 | Web Socket Protocol
142 |
143 | The first thing to realize about the protocol is that it is, in essence, a basic duplex TCP/IP socket connection. The connection starts off with the client connecting to a remote server and sending http header text to that server. The header text asks the web server to upgrade the connection to a web socket connection. This is done as a handshake where the web server responds with an appropriate http text header and from then onwards, the client and server will talk the Web Socket language.
144 |
145 | Server Handshake
146 |
147 |
148 | Regex webSocketKeyRegex = new Regex("Sec-WebSocket-Key: (.*)");
149 | Regex webSocketVersionRegex = new Regex("Sec-WebSocket-Version: (.*)");
150 |
151 | // check the version. Support version 13 and above
152 | const int WebSocketVersion = 13;
153 | int secWebSocketVersion = Convert.ToInt32(webSocketVersionRegex.Match(header).Groups[1].Value.Trim());
154 | if (secWebSocketVersion < WebSocketVersion)
155 | {
156 | throw new WebSocketVersionNotSupportedException(string.Format("WebSocket Version {0} not suported. Must be {1} or above", secWebSocketVersion, WebSocketVersion));
157 | }
158 |
159 | string secWebSocketKey = webSocketKeyRegex.Match(header).Groups[1].Value.Trim();
160 | string setWebSocketAccept = base.ComputeSocketAcceptString(secWebSocketKey);
161 | string response = ("HTTP/1.1 101 Switching Protocols" + Environment.NewLine
162 | + "Connection: Upgrade" + Environment.NewLine
163 | + "Upgrade: websocket" + Environment.NewLine
164 | + "Sec-WebSocket-Accept: " + setWebSocketAccept);
165 |
166 | HttpHelper.WriteHttpHeader(response, networkStream);
167 |
168 | This computes the accept string
:
169 |
170 |
171 | /// <summary>
172 | /// Combines the key supplied by the client with a guid and returns the sha1 hash of the combination
173 | /// </summary>
174 | public static string ComputeSocketAcceptString(string secWebSocketKey)
175 | {
176 | // this is a guid as per the web socket spec
177 | const string webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
178 |
179 | string concatenated = secWebSocketKey + webSocketGuid;
180 | byte[] concatenatedAsBytes = Encoding.UTF8.GetBytes(concatenated);
181 | byte[] sha1Hash = SHA1.Create().ComputeHash(concatenatedAsBytes);
182 | string secWebSocketAccept = Convert.ToBase64String(sha1Hash);
183 | return secWebSocketAccept;
184 | }
185 |
186 | Client Handshake
187 |
188 | Uri uri = _uri;
189 | WebSocketFrameReader reader = new WebSocketFrameReader();
190 | Random rand = new Random();
191 | byte[] keyAsBytes = new byte[16];
192 | rand.NextBytes(keyAsBytes);
193 | string secWebSocketKey = Convert.ToBase64String(keyAsBytes);
194 |
195 | string handshakeHttpRequestTemplate = @"GET {0} HTTP/1.1{4}" +
196 | "Host: {1}:{2}{4}" +
197 | "Upgrade: websocket{4}" +
198 | "Connection: Upgrade{4}" +
199 | "Sec-WebSocket-Key: {3}{4}" +
200 | "Sec-WebSocket-Version: 13{4}{4}";
201 |
202 | string handshakeHttpRequest = string.Format(handshakeHttpRequestTemplate, uri.PathAndQuery, uri.Host, uri.Port, secWebSocketKey, Environment.NewLine);
203 | byte[] httpRequest = Encoding.UTF8.GetBytes(handshakeHttpRequest);
204 | networkStream.Write(httpRequest, 0, httpRequest.Length);
205 |
206 | Reading and Writing
207 |
208 | After the handshake as been performed, the server goes into a read
loop. The following two class convert a stream of bytes to a web socket frame and visa versa: WebSocketFrameReader
and WebSocketFrameWriter
.
209 |
210 |
211 | // from WebSocketFrameReader class
212 | public WebSocketFrame Read(NetworkStream stream, Socket socket)
213 | {
214 | byte byte1;
215 |
216 | try
217 | {
218 | byte1 = (byte) stream.ReadByte();
219 | }
220 | catch (IOException)
221 | {
222 | if (socket.Connected)
223 | {
224 | throw;
225 | }
226 | else
227 | {
228 | return null;
229 | }
230 | }
231 |
232 | // process first byte
233 | byte finBitFlag = 0x80;
234 | byte opCodeFlag = 0x0F;
235 | bool isFinBitSet = (byte1 & finBitFlag) == finBitFlag;
236 | WebSocketOpCode opCode = (WebSocketOpCode) (byte1 & opCodeFlag);
237 |
238 | // read and process second byte
239 | byte byte2 = (byte) stream.ReadByte();
240 | byte maskFlag = 0x80;
241 | bool isMaskBitSet = (byte2 & maskFlag) == maskFlag;
242 | uint len = ReadLength(byte2, stream);
243 | byte[] decodedPayload;
244 |
245 | // use the masking key to decode the data if needed
246 | if (isMaskBitSet)
247 | {
248 | const int maskKeyLen = 4;
249 | byte[] maskKey = BinaryReaderWriter.ReadExactly(maskKeyLen, stream);
250 | byte[] encodedPayload = BinaryReaderWriter.ReadExactly((int) len, stream);
251 | decodedPayload = new byte[len];
252 |
253 | // apply the mask key
254 | for (int i = 0; i < encodedPayload.Length; i++)
255 | {
256 | decodedPayload[i] = (Byte) (encodedPayload[i] ^ maskKey[i%maskKeyLen]);
257 | }
258 | }
259 | else
260 | {
261 | decodedPayload = BinaryReaderWriter.ReadExactly((int) len, stream);
262 | }
263 |
264 | WebSocketFrame frame = new WebSocketFrame(isFinBitSet, opCode, decodedPayload, true);
265 | return frame;
266 | }
267 |
268 |
269 | // from WebSocketFrameWriter class
270 | public void Write(WebSocketOpCode opCode, byte[] payload, bool isLastFrame)
271 | {
272 | // best to write everything to a memory stream before we push it onto the wire
273 | // not really necessary but I like it this way
274 | using (MemoryStream memoryStream = new MemoryStream())
275 | {
276 | byte finBitSetAsByte = isLastFrame ? (byte)0x80 : (byte)0x00;
277 | byte byte1 = (byte)(finBitSetAsByte | (byte)opCode);
278 | memoryStream.WriteByte(byte1);
279 |
280 | // NB, dont set the mask flag. No need to mask data from server to client
281 | // depending on the size of the length we want to write it as a byte, ushort or ulong
282 | if (payload.Length < 126)
283 | {
284 | byte byte2 = (byte)payload.Length;
285 | memoryStream.WriteByte(byte2);
286 | }
287 | else if (payload.Length <= ushort.MaxValue)
288 | {
289 | byte byte2 = 126;
290 | memoryStream.WriteByte(byte2);
291 | BinaryReaderWriter.WriteUShort((ushort)payload.Length, memoryStream, false);
292 | }
293 | else
294 | {
295 | byte byte2 = 127;
296 | memoryStream.WriteByte(byte2);
297 | BinaryReaderWriter.WriteULong((ulong)payload.Length, memoryStream, false);
298 | }
299 |
300 | memoryStream.Write(payload, 0, payload.Length);
301 | byte[] buffer = memoryStream.ToArray();
302 | _stream.Write(buffer, 0, buffer.Length);
303 | }
304 | }
305 |
306 |
307 |
308 | Points of Interest
309 |
310 | Problems with Proxy Servers:
311 | Proxy servers which have not been configured to support Web sockets will not work well with them.
312 | I suggest that you use transport layer security if you want this to work across the wider internet especially from within a corporation.
313 |
314 | History
315 |
316 |
317 | - Version 1.0 WebSocket
318 |
319 |
--------------------------------------------------------------------------------
/WebSocketsCmd/Server/ChatWebSocketService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Diagnostics;
6 | using System.Net.Sockets;
7 | using WebSockets.Server.WebSocket;
8 | using WebSockets.Common;
9 | using System.IO;
10 |
11 | namespace WebSocketsCmd.Server
12 | {
13 | internal class ChatWebSocketService : WebSocketService
14 | {
15 | private readonly IWebSocketLogger _logger;
16 |
17 | public ChatWebSocketService(Stream stream, TcpClient tcpClient, string header, IWebSocketLogger logger)
18 | : base(stream, tcpClient, header, true, logger)
19 | {
20 | _logger = logger;
21 | }
22 |
23 | protected override void OnTextFrame(string text)
24 | {
25 | string response = "ServerABC: " + text;
26 | base.Send(response);
27 | _logger.Information(this.GetType(), response);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/WebSocketsCmd/Server/ServiceFactory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Text;
7 | using System.Text.RegularExpressions;
8 | using System.Net.Sockets;
9 | using System.Diagnostics;
10 | using WebSockets.Server;
11 | using WebSockets.Server.Http;
12 | using WebSockets.Common;
13 |
14 | namespace WebSocketsCmd.Server
15 | {
16 | internal class ServiceFactory : IServiceFactory
17 | {
18 | private readonly IWebSocketLogger _logger;
19 | private readonly string _webRoot;
20 |
21 | private string GetWebRoot()
22 | {
23 | if (!string.IsNullOrWhiteSpace(_webRoot) && Directory.Exists(_webRoot))
24 | {
25 | return _webRoot;
26 | }
27 |
28 | return Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase).Replace(@"file:\", string.Empty);
29 | }
30 |
31 | public ServiceFactory(string webRoot, IWebSocketLogger logger)
32 | {
33 | _logger = logger;
34 | _webRoot = string.IsNullOrWhiteSpace(webRoot) ? GetWebRoot() : webRoot;
35 | if (!Directory.Exists(_webRoot))
36 | {
37 | _logger.Warning(this.GetType(), "Web root not found: {0}", _webRoot);
38 | }
39 | else
40 | {
41 | _logger.Information(this.GetType(), "Web root: {0}", _webRoot);
42 | }
43 | }
44 |
45 | public IService CreateInstance(ConnectionDetails connectionDetails)
46 | {
47 | switch (connectionDetails.ConnectionType)
48 | {
49 | case ConnectionType.WebSocket:
50 | // you can support different kinds of web socket connections using a different path
51 | if (connectionDetails.Path == "/chat")
52 | {
53 | return new ChatWebSocketService(connectionDetails.Stream, connectionDetails.TcpClient, connectionDetails.Header, _logger);
54 | }
55 | break;
56 | case ConnectionType.Http:
57 | // this path actually refers to the reletive location of some html file or image
58 | return new HttpService(connectionDetails.Stream, connectionDetails.Path, _webRoot, _logger);
59 | }
60 |
61 | return new BadRequestService(connectionDetails.Stream, connectionDetails.Header, _logger);
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/WebSocketsCmd/WebSocketLogger.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using WebSockets.Common;
6 | using System.Diagnostics;
7 |
8 | namespace WebSocketsCmd
9 | {
10 | internal class WebSocketLogger : IWebSocketLogger
11 | {
12 | public void Information(Type type, string format, params object[] args)
13 | {
14 | Trace.TraceInformation(format, args);
15 | }
16 |
17 | public void Warning(Type type, string format, params object[] args)
18 | {
19 | Trace.TraceWarning(format, args);
20 | }
21 |
22 | public void Error(Type type, string format, params object[] args)
23 | {
24 | Trace.TraceError(format, args);
25 | }
26 |
27 | public void Error(Type type, Exception exception)
28 | {
29 | Error(type, "{0}", exception);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/WebSocketsCmd/WebSocketsCmd.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | x86
6 | 8.0.30703
7 | 2.0
8 | {8B0F4F0F-C3EB-4891-A612-84BED9EC3D71}
9 | Exe
10 | Properties
11 | WebSocketsCmd
12 | WebSocketsCmd
13 | v4.0
14 |
15 |
16 | 512
17 |
18 |
19 | x86
20 | true
21 | full
22 | false
23 | bin\Debug\
24 | DEBUG;TRACE
25 | prompt
26 | 4
27 |
28 |
29 | x86
30 | pdbonly
31 | true
32 | bin\Release\
33 | TRACE
34 | prompt
35 | 4
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | True
55 | True
56 | Settings.settings
57 |
58 |
59 |
60 |
61 |
62 |
63 | PreserveNewest
64 |
65 |
66 | SettingsSingleFileGenerator
67 | Settings.Designer.cs
68 |
69 |
70 |
71 |
72 | PreserveNewest
73 |
74 |
75 |
76 |
77 | {D2DBCC1D-CEF2-400B-A886-7E0D13A25F9C}
78 | WebSockets
79 |
80 |
81 |
82 |
89 |
--------------------------------------------------------------------------------
/WebSocketsCmd/client.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Web Socket Demo
10 |
20 |
21 |
22 |
23 |
26 |
27 |
28 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/WebSocketsCmd/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ninjasource/websocket-server/3fc27cf05077235c9f764728d3ec9e7139845275/WebSocketsCmd/favicon.ico
--------------------------------------------------------------------------------
/gitpublish.bat:
--------------------------------------------------------------------------------
1 | git fetch origin
2 | git status
3 | git add .
4 | git commit -m "Version 1.03 Major refactor and added support for c# Client"
5 | git push origin master
--------------------------------------------------------------------------------