├── .gitignore ├── CONTRIBUTING.md ├── CSSockets.Tests ├── CSSockets.Tests.csproj └── Program.cs ├── CSSockets.sln ├── CSSockets ├── Binary │ ├── Binary.cs │ └── BinaryConversion.cs ├── CSSockets.csproj ├── Http │ ├── Definition │ │ ├── Connection.cs │ │ ├── Encoding.cs │ │ ├── Head.cs │ │ ├── Headers.cs │ │ ├── Messages.cs │ │ ├── URL.cs │ │ └── Version.cs │ └── Reference │ │ ├── Body.cs │ │ ├── ClientConnection.cs │ │ ├── Listener.cs │ │ ├── Messages.cs │ │ ├── RequestHead.cs │ │ ├── ResponseHead.cs │ │ └── ServerConnection.cs ├── Streams │ ├── Base.cs │ ├── Compressors.cs │ ├── Primitive.cs │ └── Wrappers.cs ├── Tcp │ ├── Connection.cs │ ├── IOControl.cs │ ├── Listener.cs │ ├── SocketWrapper.cs │ └── Types.cs └── WebSockets │ ├── Definition │ ├── Connection.cs │ ├── ConnectionFactory.cs │ ├── ConnectionMode.cs │ ├── Headers.cs │ ├── Listener.cs │ ├── Negotiator.cs │ ├── Protocol.cs │ └── Secret.cs │ └── Primitive │ ├── Connection.cs │ ├── ConnectionFactory.cs │ └── Listener.cs ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | 263 | # NuGet command line 264 | CSSockets/nuget.exe -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | I can finish this project myself, however contribution in the form of unit tests and bug reports would be extremely helpful. 2 | -------------------------------------------------------------------------------- /CSSockets.Tests/CSSockets.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | CSSockets.Tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /CSSockets.Tests/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Linq; 4 | using CSSockets.Tcp; 5 | using System.Threading; 6 | using CSSockets.Streams; 7 | using System.Diagnostics; 8 | using CSSockets.Http.Reference; 9 | using CSSockets.Http.Definition; 10 | using System.Collections.Generic; 11 | using static System.Text.Encoding; 12 | using TcpListener = CSSockets.Tcp.Listener; 13 | using HttpListener = CSSockets.Http.Reference.Listener; 14 | using WebSocket = CSSockets.WebSockets.Primitive.Connection; 15 | using WebSocketListener = CSSockets.WebSockets.Primitive.Listener; 16 | using WebSocketFactory = CSSockets.WebSockets.Primitive.ConnectionFactory; 17 | 18 | namespace CSSockets.Tests 19 | { 20 | class Program 21 | { 22 | static void Main(string[] args) 23 | { 24 | WebSocketClientTest(args); 25 | } 26 | 27 | public static void WebSocketClientTest(string[] args) 28 | { 29 | IPEndPoint serverEP = new IPEndPoint(IPAddress.Any, 420); 30 | IPEndPoint clientEP = new IPEndPoint(IPAddress.Loopback, 420); 31 | WebSocketListener listener = new WebSocketListener(serverEP); 32 | WebSocket server = null; 33 | WebSocket client = null; 34 | listener.ClientVerifier = 35 | (address, subprotocols, head) => 36 | subprotocols.Contains("test"); 37 | listener.SubprotocolChooser = (address, subprotocols, head) => "test"; 38 | listener.OnConnection += (connection) => 39 | { 40 | server = connection; 41 | Console.WriteLine("listener connection"); 42 | Console.WriteLine(server.Subprotocol); 43 | server.OnBinary += (data) => Console.WriteLine("SERVER BINARY {0}", data.LongLength); 44 | server.OnString += (data) => Console.WriteLine("SERVER STRING {0}", data.Length); 45 | server.OnClose += (code, reason) => Console.WriteLine("SERVER CLOSED {0} '{1}'", code, reason); 46 | server.SendBinary(new byte[1]); 47 | server.SendBinary(new byte[10]); 48 | server.SendBinary(new byte[100]); 49 | server.SendString("Hijklmn"); 50 | server.SendClose(1000, "OK"); 51 | }; 52 | listener.Start(); 53 | Console.WriteLine("listener open"); 54 | Connection clientTcp = new Connection(); 55 | clientTcp.OnOpen += () => 56 | { 57 | client = WebSocketFactory.Default.Generate(clientTcp, "/"); 58 | client.OnOpen += () => 59 | { 60 | client.OnBinary += (data) => Console.WriteLine("CLIENT BINARY {0}", data.LongLength); 61 | client.OnString += (data) => Console.WriteLine("CLIENT STRING {0}", data.Length); 62 | client.OnClose += (code, reason) => Console.WriteLine("CLIENT CLOSED {0} '{1}'", code, reason); 63 | /*client.SendBinary(new byte[1]); 64 | client.SendBinary(new byte[10]); 65 | client.SendBinary(new byte[100]); 66 | client.SendString("Abcdefg");*/ 67 | client.SendClose(1000, "OK"); 68 | }; 69 | }; 70 | clientTcp.Connect(clientEP); 71 | Console.ReadKey(); 72 | Console.WriteLine("client states: {0} {1} {2} {3}", client.Opening, client.Open, client.Closing, client.Closed); 73 | Console.ReadKey(); 74 | listener.Stop(); 75 | Console.ReadKey(); 76 | } 77 | 78 | public static void WebSocketServerTest(string[] args) 79 | { 80 | IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 420); 81 | WebSocketListener server = new WebSocketListener(endPoint); 82 | WebSocket client = null; 83 | server.OnConnection += (connection) => 84 | { 85 | client = connection; 86 | Console.WriteLine("connection"); 87 | connection.OnBinary += (data) => Console.WriteLine("BINARY {0}", data.LongLength); 88 | connection.OnString += (data) => Console.WriteLine("STRING {0}", data.Length); 89 | connection.OnClose += (code, reason) => Console.WriteLine("CLOSED {0} '{1}'", code, reason); 90 | connection.SendBinary(new byte[1]); 91 | connection.SendBinary(new byte[10]); 92 | connection.SendBinary(new byte[100]); 93 | }; 94 | Console.WriteLine("open"); 95 | server.Start(); 96 | Console.ReadKey(); 97 | client.SendClose(1000, "OK"); 98 | server.Stop(); 99 | Console.ReadKey(); 100 | } 101 | 102 | public static void HttpClientConnectionTest(string[] args) 103 | { 104 | IPEndPoint clientEndPoint = new IPEndPoint(IPAddress.Loopback, 420); 105 | IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Any, 420); 106 | HttpListener listener = new HttpListener(serverEndPoint); 107 | ServerConnection server = null; 108 | listener.OnRequest = (sreq, res) => 109 | { 110 | Console.WriteLine("Server request"); 111 | server = sreq.Connection; 112 | if (sreq.Path != "/echo") 113 | { 114 | res["Content-Length"] = "0"; 115 | res.End(404, "Not Found"); return; 116 | } 117 | switch (sreq.Method) 118 | { 119 | case "GET": 120 | Console.WriteLine("Server GET request"); 121 | res["Content-Type"] = "text/plain"; 122 | res["Transfer-Encoding"] = "chunked"; 123 | res.SendHead(200, "OK"); 124 | res.Write(UTF8.GetBytes("GET on /echo\r\nMake a POST request to echo its body")); 125 | res.End(); 126 | Console.WriteLine("Server response finish"); 127 | break; 128 | case "POST": 129 | Console.WriteLine("Server POST request"); 130 | sreq.OnFinish += () => 131 | { 132 | Console.WriteLine("Server POST request finish"); 133 | if (sreq.BufferedReadable == 0) { res.End(400, "Bad Request"); return; } 134 | res["Content-Type"] = "text/plain"; 135 | res["Transfer-Encoding"] = "chunked"; 136 | res.SendHead(200, "OK"); 137 | res.Write(UTF8.GetBytes("/ path on POST!\r\n")); 138 | res.Write(UTF8.GetBytes("Body is as follows:\r\n\r\n")); 139 | byte[] data = sreq.Read(); 140 | Console.WriteLine(UTF8.GetString(data).Replace("\r", "\\r").Replace("\n", "\\n")); 141 | res.Write(data); 142 | res.End(); 143 | Console.WriteLine("Server response finish"); 144 | }; 145 | break; 146 | default: res.End(400, "Bad Request"); break; 147 | } 148 | }; 149 | listener.Start(); 150 | ClientConnection client = null; 151 | client = new ClientConnection(clientEndPoint, () => 152 | { 153 | OutgoingRequest creq = client.Enqueue("HTTP/1.1", "POST", "/echo"); 154 | creq.OnResponse += (res) => 155 | { 156 | Console.WriteLine("Client response {0} {1}", res.StatusCode, res.StatusDescription); 157 | res.OnFinish += () => 158 | { 159 | Console.WriteLine("Client response finish\r\n{0}", UTF8.GetString(res.Read())); 160 | client.End(); 161 | client.Base.End(); 162 | }; 163 | }; 164 | creq["Transfer-Encoding"] = "chunked"; 165 | creq.SendHead(); 166 | creq.Write(UTF8.GetBytes("I am a body")); 167 | creq.End(); 168 | Console.WriteLine("Client request finish"); 169 | }); 170 | Console.ReadKey(); 171 | listener.Stop(); 172 | Console.ReadKey(); 173 | //client.Terminate(); 174 | } 175 | 176 | public static void HttpServerConnectionTest(string[] args) 177 | { 178 | HttpListener server = new HttpListener(new IPEndPoint(IPAddress.Any, 420)); 179 | server.OnConnection += (connection) => Console.WriteLine("connection"); 180 | server.OnRequest = (req, res) => 181 | { 182 | Console.WriteLine("request"); 183 | if (req.Path == "/favicon.ico") res.End(404, "Not Found"); 184 | else 185 | { 186 | Stopwatch sw = Stopwatch.StartNew(); 187 | res.OnFinish += () => Console.WriteLine("{0}ms for onfinish fire", sw.Elapsed.TotalMilliseconds.ToString("F2")); 188 | res["Transfer-Encoding"] = "chunked"; 189 | res["Content-Type"] = "text/plain"; 190 | res["Connection"] = "close"; 191 | res.SendHead(200, "OK"); 192 | for (int i = 0; i < 1000000; i++) res.Write(ASCII.GetBytes(i.ToString() + " ")); 193 | res.End(); 194 | Console.WriteLine("{0}ms for req end", sw.Elapsed.TotalMilliseconds.ToString("F2")); 195 | sw.Stop(); 196 | } 197 | }; 198 | server.Start(); 199 | Console.WriteLine("started"); 200 | Console.ReadKey(); 201 | server.Stop(); 202 | Console.WriteLine("stopped"); 203 | Console.ReadKey(); 204 | } 205 | 206 | public static void BodyParseSerializeTest(string[] args) 207 | { 208 | BodyParser parser = new BodyParser(); 209 | BodySerializer serial = new BodySerializer(); 210 | 211 | serial.Pipe(parser); 212 | serial.OnFail += () => Console.WriteLine("serializer failed"); 213 | parser.OnData += (data) => Console.WriteLine(UTF8.GetString(data)); 214 | parser.OnFinish += () => Console.WriteLine("parser finished"); 215 | parser.Excess.Pipe(VoidWritable.Default); 216 | 217 | BodyType bodyType = new BodyType(null, TransferEncoding.Chunked, TransferCompression.Deflate); 218 | if (!parser.TrySetFor(bodyType)) Console.WriteLine("parser failed to set"); 219 | if (!serial.TrySetFor(bodyType)) Console.WriteLine("serializer failed to set"); 220 | 221 | serial.Write(UTF8.GetBytes("I am a body\r\nxd\r\n")); 222 | serial.Write(UTF8.GetBytes("I am a body\r\nasfjaskfd\r\nasdfa")); 223 | serial.Finish(); 224 | 225 | Console.ReadKey(); 226 | } 227 | 228 | public static void HeadParseSerializeTest(string[] args) 229 | { 230 | RequestHeadParser rParser = new RequestHeadParser(); 231 | ResponseHeadParser RParser = new ResponseHeadParser(); 232 | RequestHeadSerializer rSerial = new RequestHeadSerializer(); 233 | ResponseHeadSerializer RSerial = new ResponseHeadSerializer(); 234 | 235 | RequestHead rHead = new RequestHead("HTTP/1.1", "GET", "/"); 236 | rHead.Headers["test1"] = "123"; 237 | rHead.Headers["test2"] = "456"; 238 | 239 | ResponseHead RHead = new ResponseHead("HTTP/1.1", 200, "OK"); 240 | RHead.Headers["test1"] = "123"; 241 | RHead.Headers["test2"] = "456"; 242 | 243 | rSerial.Pipe(rParser); 244 | RSerial.Pipe(RParser); 245 | 246 | rParser.OnCollect += (head) => Console.WriteLine("request parser:\n{0}", head.Stringify()); 247 | RParser.OnCollect += (head) => Console.WriteLine("response parser:\n{0}", head.Stringify()); 248 | rSerial.OnFail += () => Console.WriteLine("request serializer failed"); 249 | RSerial.OnFail += () => Console.WriteLine("response serializer failed"); 250 | 251 | rSerial.Write(rHead); 252 | RSerial.Write(RHead); 253 | 254 | Console.ReadKey(); 255 | } 256 | 257 | public static void CompressorTest(string[] args) 258 | { 259 | MemoryDuplex memory = new MemoryDuplex(); 260 | GzipCompressor c = new GzipCompressor(); 261 | c.Pipe(memory); 262 | GzipDecompressor d = new GzipDecompressor(); 263 | memory.Pipe(d); 264 | d.OnData += (data) => Console.WriteLine("data {0}", data.Stringify()); 265 | c.Write(new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }); 266 | c.Finish(); 267 | d.Finish(); 268 | Console.ReadKey(); 269 | } 270 | 271 | struct TcpSocketScalabilityMetrics 272 | { 273 | public int attempts, 274 | clientCreated, 275 | clientActive, 276 | clientSuccessful, 277 | clientError, 278 | serverActive, 279 | serverTimeout, 280 | serverSuccessful, 281 | serverError, 282 | datasSent, 283 | datasReceieved; 284 | 285 | public override string ToString() 286 | { 287 | return string.Format( 288 | "client: {0:0000}c/{1:0000}a/{2:0000}s/{3:0000}e/{4:0000}T" + 289 | " server: {5:0000}a/{6:0000}t/{7:0000}s/{8:0000}e/{9:0000}T" + 290 | " data: {10:0000}s/{11:0000}r" + 291 | " io: {12:0000}c/{13:0000}l/{14:0000}s/{15:00}t", 292 | clientCreated, clientActive, clientSuccessful, clientError, clientSuccessful + clientError, 293 | serverActive, serverTimeout, serverSuccessful, serverError, serverSuccessful + serverError + serverTimeout, 294 | datasSent, datasReceieved, 295 | IOControl.ConnectionCount, IOControl.ListenerCount, IOControl.SocketCount, IOControl.ThreadCount 296 | ); 297 | } 298 | } 299 | 300 | public static void TcpSocketScalabilityTest(string[] args) 301 | { 302 | EndPoint clientEndPoint = new IPEndPoint(IPAddress.Loopback, 420); 303 | EndPoint serverendPoint = new IPEndPoint(IPAddress.Any, 420); 304 | byte[] sending = new byte[] { 1, 2, 3, 4, 5 }; 305 | 306 | TcpSocketScalabilityMetrics metrics = new TcpSocketScalabilityMetrics 307 | { 308 | attempts = 10000 309 | }; 310 | 311 | TcpListener listener = new TcpListener 312 | { 313 | Backlog = metrics.attempts, 314 | BindEndPoint = serverendPoint 315 | }; 316 | listener.OnConnection += (server) => 317 | { 318 | Interlocked.Increment(ref metrics.serverActive); 319 | server.TimeoutAfter = new TimeSpan(0, 1, 0); 320 | server.OnData += (data) => Interlocked.Increment(ref metrics.datasReceieved); 321 | server.OnError += (e) => 322 | { 323 | Interlocked.Increment(ref metrics.serverSuccessful); 324 | Interlocked.Increment(ref metrics.serverError); 325 | }; 326 | server.OnClose += () => 327 | { 328 | Interlocked.Increment(ref metrics.serverSuccessful); 329 | Interlocked.Decrement(ref metrics.serverActive); 330 | }; 331 | server.OnTimeout += () => 332 | { 333 | Interlocked.Increment(ref metrics.serverTimeout); 334 | server.Terminate(); 335 | }; 336 | }; 337 | listener.Start(); 338 | 339 | Thread.Sleep(1000); 340 | 341 | DateTime start = DateTime.UtcNow; 342 | List clients = new List(); 343 | 344 | Console.WriteLine("CLIENT: created / active / success / error / total SERVER: active / timeout / success / error / total DATA: sent / recvd IO: connections / listeners / sockets / threads"); 345 | for (int i = 0; i < metrics.attempts; i++) 346 | { 347 | Interlocked.Increment(ref metrics.clientCreated); 348 | Tcp.Connection client = new Tcp.Connection(); 349 | client.OnDrain += () => Interlocked.Increment(ref metrics.datasSent); 350 | client.OnClose += () => 351 | { 352 | Interlocked.Increment(ref metrics.clientSuccessful); 353 | Interlocked.Decrement(ref metrics.clientActive); 354 | }; 355 | client.OnError += (e) => 356 | { 357 | if (client.State != TcpSocketState.Open) 358 | Interlocked.Increment(ref metrics.clientActive); 359 | Interlocked.Decrement(ref metrics.clientSuccessful); 360 | Interlocked.Increment(ref metrics.clientError); 361 | }; 362 | client.OnOpen += () => 363 | { 364 | Interlocked.Increment(ref metrics.clientActive); 365 | client.Write(sending); 366 | client.End(); 367 | }; 368 | clients.Add(client); 369 | if (metrics.clientCreated % 100 == 0) 370 | Console.WriteLine("[{0}] {1}", (DateTime.UtcNow - start).TotalSeconds.ToString("F6").PadLeft(12), metrics.ToString()); 371 | } 372 | 373 | Console.WriteLine("[{0}] {1} clients created", (DateTime.UtcNow - start).TotalSeconds.ToString("F6").PadLeft(12), metrics.attempts); 374 | 375 | for (int i = 0; i < metrics.attempts; i++) 376 | clients[i].Connect(clientEndPoint); 377 | 378 | Console.WriteLine("[{0}] {1} clients connecting", (DateTime.UtcNow - start).TotalSeconds.ToString("F6").PadLeft(12), metrics.attempts); 379 | 380 | Thread.Sleep(100); 381 | while (true) 382 | { 383 | Console.WriteLine("[{0}] {1}", (DateTime.UtcNow - start).TotalSeconds.ToString("F6").PadLeft(12), metrics.ToString()); 384 | if (IOControl.ConnectionCount == 0) break; 385 | Thread.Sleep(200); 386 | } 387 | listener.Stop(); 388 | Console.WriteLine("[{0}] done", (DateTime.UtcNow - start).TotalSeconds.ToString("F6").PadLeft(12)); 389 | Console.ReadKey(); 390 | } 391 | } 392 | 393 | public static class Extensions 394 | { 395 | private const string CR = "\\r\r"; 396 | private const string LF = "\\n\n"; 397 | private const string CRLF = "\\r\\n\r\n"; 398 | private const char COLON = ':'; 399 | private const string WS = " "; 400 | 401 | public static string Stringify(this byte[] array) 402 | { 403 | if (array.LongLength == 0) return ""; 404 | string s = ""; 405 | foreach (byte item in array) s += item.ToString("X2"); 406 | return s; 407 | } 408 | 409 | public static string Stringify(this RequestHead source) 410 | { 411 | string stringified = source.Method + WS + source.URL + WS + source.Version + CRLF; 412 | for (int i = 0; i < source.Headers.Length; i++) 413 | stringified += source.Headers[i].Key.ToLower() + COLON + WS + source.Headers[i].Value + CRLF; 414 | stringified += CRLF; 415 | return stringified; 416 | } 417 | 418 | public static string Stringify(this ResponseHead source) 419 | { 420 | string stringified = source.Version + WS + source.StatusCode + WS + source.StatusDescription + CRLF; 421 | for (int i = 0; i < source.Headers.Length; i++) 422 | stringified += source.Headers[i].Key.ToLower() + COLON + WS + source.Headers[i].Value + CRLF; 423 | stringified += CRLF; 424 | return stringified; 425 | } 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /CSSockets.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2010 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSSockets", "CSSockets\CSSockets.csproj", "{F4541532-7252-4924-BDC4-64BEDE117381}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSSockets.Tests", "CSSockets.Tests\CSSockets.Tests.csproj", "{573A6E5A-BC66-41A4-BC3A-F7113742FE94}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {F4541532-7252-4924-BDC4-64BEDE117381}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {F4541532-7252-4924-BDC4-64BEDE117381}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {F4541532-7252-4924-BDC4-64BEDE117381}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {F4541532-7252-4924-BDC4-64BEDE117381}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {573A6E5A-BC66-41A4-BC3A-F7113742FE94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {573A6E5A-BC66-41A4-BC3A-F7113742FE94}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {573A6E5A-BC66-41A4-BC3A-F7113742FE94}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {573A6E5A-BC66-41A4-BC3A-F7113742FE94}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {4AA31DB5-986E-4CCC-A1D3-499D5F629D5F} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /CSSockets/Binary/Binary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using CSSockets.Streams; 5 | 6 | namespace CSSockets.Binary 7 | { 8 | public abstract class Reader 9 | { 10 | protected abstract byte[] NonBlockingRead(ulong length); 11 | protected abstract byte[] NonBlockingUnsafeRead(ulong length); 12 | 13 | public byte ReadUInt8() => NonBlockingRead(1)[0]; 14 | public sbyte ReadInt8() => (sbyte)NonBlockingRead(1)[0]; 15 | 16 | public ushort ReadUInt16BE() => (ushort)BinaryConversion.GetBE(NonBlockingRead(2)); 17 | public ushort ReadUInt16LE() => (ushort)BinaryConversion.GetLE(NonBlockingRead(2)); 18 | public short ReadInt16BE() => (short)BinaryConversion.GetBE(NonBlockingRead(2)); 19 | public short ReadInt16LE() => (short)BinaryConversion.GetLE(NonBlockingRead(2)); 20 | 21 | public uint ReadUInt32BE() => (uint)BinaryConversion.GetBE(NonBlockingRead(4)); 22 | public uint ReadUInt32LE() => (uint)BinaryConversion.GetLE(NonBlockingRead(4)); 23 | public int ReadInt32BE() => (int)BinaryConversion.GetBE(NonBlockingRead(4)); 24 | public int ReadInt32LE() => (int)BinaryConversion.GetLE(NonBlockingRead(4)); 25 | 26 | public ulong ReadUInt64BE() => BinaryConversion.GetBE(NonBlockingRead(8)); 27 | public ulong ReadUInt64LE() => BinaryConversion.GetLE(NonBlockingRead(8)); 28 | public long ReadInt64BE() => (long)BinaryConversion.GetBE(NonBlockingRead(8)); 29 | public long ReadInt64LE() => (long)BinaryConversion.GetLE(NonBlockingRead(8)); 30 | 31 | public ulong ReadUIntBE(byte size) => BinaryConversion.GetBE(NonBlockingRead(size / 8u)); 32 | public ulong ReadUIntLE(byte size) => BinaryConversion.GetLE(NonBlockingRead(size / 8u)); 33 | public long ReadIntBE(byte size) => BinaryConversion.ToSignedBE(BinaryConversion.GetBE(NonBlockingRead(size / 8u)), size); 34 | public long ReadIntLE(byte size) => BinaryConversion.ToSignedLE(BinaryConversion.GetLE(NonBlockingRead(size / 8u)), size); 35 | 36 | public float ReadFloat32BE() => new BinaryConversion.IntFloat32(ReadInt32BE()).Float; 37 | public float ReadFloat32LE() => new BinaryConversion.IntFloat32(ReadInt32LE()).Float; 38 | public double ReadFloat64BE() => new BinaryConversion.IntFloat64(ReadInt64BE()).Float; 39 | public double ReadFloat64LE() => new BinaryConversion.IntFloat64(ReadInt64LE()).Float; 40 | 41 | public string ReadString(Encoding encoding, ulong length, byte size) => encoding.GetString(NonBlockingRead(length * (size / 8u))); 42 | 43 | public string ReadStringUnicodeBEZT() => ReadStringZT(Encoding.BigEndianUnicode, 16); 44 | public string ReadStringUnicodeLEZT() => ReadStringZT(Encoding.Unicode, 16); 45 | public string ReadStringUTF8ZT() => ReadStringZT(Encoding.UTF8, 8); 46 | public string ReadStringZT(Encoding encoding, byte size) 47 | { 48 | PrimitiveBuffer buffer = new PrimitiveBuffer(); 49 | ulong block = size / 8u; 50 | while (true) 51 | { 52 | byte[] read = NonBlockingUnsafeRead(block); 53 | if (read == null || read.All((v) => v == 0)) break; 54 | buffer.Write(read); 55 | } 56 | return encoding.GetString(buffer.Read(buffer.Length)); 57 | } 58 | } 59 | public sealed class StreamReader : Reader 60 | { 61 | public IReadable Stream { get; } 62 | 63 | public StreamReader(IReadable readable) => Stream = readable; 64 | 65 | protected sealed override byte[] NonBlockingUnsafeRead(ulong length) 66 | { 67 | byte[] data = new byte[length]; 68 | ulong read = Stream.Read(data); 69 | if (read != length) return null; 70 | return data; 71 | } 72 | protected sealed override byte[] NonBlockingRead(ulong length) 73 | { 74 | byte[] data = new byte[length]; 75 | ulong read = Stream.Read(data); 76 | if (read != length) throw new IndexOutOfRangeException("Reached end of stream"); 77 | return data; 78 | } 79 | } 80 | public sealed class MemoryReader : Reader 81 | { 82 | public byte[] Data { get; } 83 | private readonly object Sync = new object(); 84 | private ulong size; 85 | private ulong offset = 0; 86 | 87 | public MemoryReader(byte[] data, ulong offset = 0) 88 | { 89 | Data = data; 90 | size = (ulong)data.LongLength; 91 | this.offset = offset; 92 | } 93 | 94 | protected sealed override byte[] NonBlockingUnsafeRead(ulong length) 95 | { 96 | lock (Sync) 97 | { 98 | if (offset + length > size) return null; 99 | return PrimitiveBuffer.Slice(Data, offset - length, offset += length); 100 | } 101 | } 102 | protected sealed override byte[] NonBlockingRead(ulong length) 103 | { 104 | lock (Sync) 105 | { 106 | if (offset + length > size) throw new IndexOutOfRangeException("Reached end of memory"); 107 | return PrimitiveBuffer.Slice(Data, offset - length, offset += length); 108 | } 109 | } 110 | } 111 | 112 | public class StreamWriter 113 | { 114 | public IWritable Stream { get; } 115 | 116 | public StreamWriter(IWritable writable) => Stream = writable; 117 | 118 | public bool WriteUInt8(byte value) => Stream.Write(new byte[] { value }); 119 | public bool WriteInt8(sbyte value) => Stream.Write(new byte[] { (byte)value }); 120 | 121 | public bool WriteUInt16BE(ushort value) => Stream.Write(BinaryConversion.SerializeBE(value, 2)); 122 | public bool WriteUInt16LE(ushort value) => Stream.Write(BinaryConversion.SerializeLE(value, 2)); 123 | public bool WriteInt16BE(short value) => Stream.Write(BinaryConversion.SerializeBE((ushort)value, 2)); 124 | public bool WriteInt16LE(short value) => Stream.Write(BinaryConversion.SerializeLE((ushort)value, 2)); 125 | 126 | public bool WriteUInt32BE(uint value) => Stream.Write(BinaryConversion.SerializeBE(value, 4)); 127 | public bool WriteUInt32LE(uint value) => Stream.Write(BinaryConversion.SerializeLE(value, 4)); 128 | public bool WriteInt32BE(int value) => Stream.Write(BinaryConversion.SerializeBE((uint)value, 4)); 129 | public bool WriteInt32LE(int value) => Stream.Write(BinaryConversion.SerializeLE((uint)value, 4)); 130 | 131 | public bool WriteUInt64BE(ulong value) => Stream.Write(BinaryConversion.SerializeBE(value, 8)); 132 | public bool WriteUInt64LE(ulong value) => Stream.Write(BinaryConversion.SerializeLE(value, 8)); 133 | public bool WriteInt64BE(long value) => Stream.Write(BinaryConversion.SerializeBE((ulong)value, 8)); 134 | public bool WriteInt64LE(long value) => Stream.Write(BinaryConversion.SerializeLE((ulong)value, 8)); 135 | 136 | public bool WriteUIntBE(ulong value, byte size) => Stream.Write(BinaryConversion.SerializeBE(value, size / 8)); 137 | public bool WriteUIntLE(ulong value, byte size) => Stream.Write(BinaryConversion.SerializeLE(value, size / 8)); 138 | public bool WriteIntBE(long value, byte size) => Stream.Write(BinaryConversion.SerializeBE((ulong)value, size / 8)); 139 | public bool WriteIntLE(long value, byte size) => Stream.Write(BinaryConversion.SerializeLE((ulong)value, size / 8)); 140 | 141 | public bool WriteFloat32BE(float value) => Stream.Write(BinaryConversion.SerializeBE((ulong)new BinaryConversion.IntFloat32(value).Integer, 4)); 142 | public bool WriteFloat32LE(float value) => Stream.Write(BinaryConversion.SerializeLE((ulong)new BinaryConversion.IntFloat32(value).Integer, 4)); 143 | public bool WriteFloat64BE(double value) => Stream.Write(BinaryConversion.SerializeBE((ulong)new BinaryConversion.IntFloat64(value).Integer, 8)); 144 | public bool WriteFloat64LE(double value) => Stream.Write(BinaryConversion.SerializeLE((ulong)new BinaryConversion.IntFloat64(value).Integer, 8)); 145 | 146 | public bool WriteStringUTF8(string value) => WriteString(value, Encoding.UTF8); 147 | public bool WriteStringUnicodeBE(string value) => WriteString(value, Encoding.BigEndianUnicode); 148 | public bool WriteStringUnicodeLE(string value) => WriteString(value, Encoding.Unicode); 149 | public bool WriteString(string value, Encoding encoding) => Stream.Write(encoding.GetBytes(value)); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /CSSockets/Binary/BinaryConversion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace CSSockets.Binary 5 | { 6 | public static class BinaryConversion 7 | { 8 | public static ulong GetBE(byte[] data) 9 | { 10 | ulong value = 0; 11 | for (int i = 0; i < data.Length; i++) value = unchecked(value << 8 | (data[i] & 0xFFu)); 12 | return value; 13 | } 14 | public static ulong GetLE(byte[] data) 15 | { 16 | ulong value = 0; 17 | for (int i = 0; i < data.Length; i++) value = unchecked(value << 8 | (data[data.Length - 1 - i] & 0xFFu)); 18 | return value; 19 | } 20 | public static bool IsNegativeBE(ulong value, int size) => ((value >> (size - 8)) & 128) == 128; 21 | public static bool IsNegativeLE(ulong value, int size) => (value & 128) == 128; 22 | public static long ToSignedBE(ulong value, int size) => IsNegativeBE(value, size) ? -(long)Math.Pow(2, size) + (long)value : (long)value; 23 | public static long ToSignedLE(ulong value, int size) => IsNegativeLE(value, size) ? -(long)Math.Pow(2, size) + (long)value : (long)value; 24 | public static byte[] SerializeBE(ulong value, int size) 25 | { 26 | byte[] data = new byte[size]; 27 | for (int i = 0; i < size; i++) 28 | { 29 | data[size - 1 - i] = (byte)(value & 0xFF); 30 | value >>= 8; 31 | } 32 | return data; 33 | } 34 | public static byte[] SerializeLE(ulong value, int size) 35 | { 36 | byte[] data = new byte[size]; 37 | for (int i = 0; i < size; i++) 38 | { 39 | data[i] = (byte)(value & 0xFF); 40 | value >>= 8; 41 | } 42 | return data; 43 | } 44 | 45 | [StructLayout(LayoutKind.Explicit)] 46 | public struct IntFloat32 47 | { 48 | [FieldOffset(0)] 49 | int i; 50 | [FieldOffset(0)] 51 | float f; 52 | 53 | public int Integer => i; 54 | public float Float => f; 55 | 56 | public IntFloat32(int i) : this() 57 | { 58 | f = default(float); 59 | this.i = i; 60 | } 61 | public IntFloat32(float f) : this() 62 | { 63 | i = default(int); 64 | this.f = f; 65 | } 66 | } 67 | [StructLayout(LayoutKind.Explicit)] 68 | public struct IntFloat64 69 | { 70 | [FieldOffset(0)] 71 | long i; 72 | [FieldOffset(0)] 73 | double f; 74 | 75 | public long Integer => i; 76 | public double Float => f; 77 | 78 | public IntFloat64(long i) : this() 79 | { 80 | f = default(double); 81 | this.i = i; 82 | } 83 | public IntFloat64(double f) : this() 84 | { 85 | i = default(long); 86 | this.f = f; 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /CSSockets/CSSockets.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | CSSockets 6 | 7 | 8 | 9 | true 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /CSSockets/Http/Definition/Connection.cs: -------------------------------------------------------------------------------- 1 | using CSSockets.Tcp; 2 | using CSSockets.Streams; 3 | using CSSockets.Http.Reference; 4 | 5 | namespace CSSockets.Http.Definition 6 | { 7 | public abstract class Connection : IEndable 8 | where TReq : Head, new() where TRes : Head, new() 9 | { 10 | protected readonly object Sync = new object(); 11 | 12 | protected HeadParser IncomingHead; 13 | protected HeadSerializer OutgoingHead; 14 | protected BodyParser IncomingBody; 15 | protected BodySerializer OutgoingBody; 16 | 17 | public virtual event ControlHandler OnEnd; 18 | 19 | public Connection Base { get; } 20 | public bool Frozen { get; protected set; } = false; 21 | public bool Ended { get; protected set; } = false; 22 | 23 | public Connection(Connection connection) 24 | { 25 | Base = connection; 26 | Base.OnClose += OnBaseClose; 27 | } 28 | 29 | protected virtual void OnBaseClose() => End(); 30 | 31 | public abstract IncomingMessage Incoming { get; } 32 | public abstract OutgoingMessage Outgoing { get; } 33 | public abstract bool StartOutgoing(TRes head); 34 | public abstract bool FinishOutgoing(); 35 | 36 | public abstract byte[] Freeze(); 37 | public virtual bool End() 38 | { 39 | lock (Sync) 40 | { 41 | if (Ended) return false; 42 | if (!Frozen) Freeze(); 43 | OnEnd?.Invoke(); 44 | return Ended = true; 45 | } 46 | } 47 | public virtual bool Terminate() 48 | { 49 | lock (Sync) return End() && Base.Terminate() && (Ended = true); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /CSSockets/Http/Definition/Encoding.cs: -------------------------------------------------------------------------------- 1 | namespace CSSockets.Http.Definition 2 | { 3 | public enum TransferEncoding : byte 4 | { 5 | None, 6 | Binary, 7 | Chunked 8 | } 9 | public enum TransferCompression : byte 10 | { 11 | None, 12 | Deflate, 13 | Gzip, 14 | Compress 15 | } 16 | public struct BodyType 17 | { 18 | public ulong? Length { get; } 19 | public TransferEncoding Encoding { get; } 20 | public TransferCompression Compression { get; } 21 | 22 | public BodyType(ulong? length, TransferEncoding encoding, TransferCompression compression) : this() 23 | { 24 | Length = length; 25 | Encoding = encoding; 26 | Compression = compression; 27 | } 28 | 29 | public static BodyType? TryDetectFor(Head head, bool defaultNoBody) 30 | { 31 | // Reference: RFC 7320's 3.3.3 32 | TransferEncoding encoding = TransferEncoding.Binary; 33 | TransferCompression compression = TransferCompression.None; 34 | ulong? contentLen = null; 35 | if (head.Headers["Content-Length"] != null) 36 | { 37 | if (!ulong.TryParse(head.Headers["Content-Length"], out ulong len)) 38 | return null; 39 | contentLen = len; 40 | } 41 | if (head.Headers["Transfer-Encoding"] == null) 42 | { 43 | if (contentLen == null && defaultNoBody) 44 | // 3.3.3.6 45 | return new BodyType(null, encoding = TransferEncoding.None, compression); 46 | return new BodyType(contentLen, encoding, compression); 47 | } 48 | string[] split = head.Headers["Transfer-Encoding"].Split(','); 49 | for (int i = 0; i < split.Length; i++) 50 | { 51 | switch (split[i].Trim()) 52 | { 53 | case "chunked": 54 | if (contentLen != null) return null; // 3.3.3.3 55 | encoding = TransferEncoding.Chunked; 56 | break; 57 | case "gzip": 58 | if (compression != TransferCompression.None) return null; 59 | compression = TransferCompression.Gzip; 60 | break; 61 | case "deflate": 62 | if (compression != TransferCompression.None) return null; 63 | compression = TransferCompression.Deflate; 64 | break; 65 | case "compress": 66 | return null; // not implemented 67 | default: return null; // unknown encoding 68 | } 69 | } 70 | return new BodyType(contentLen, encoding, compression); 71 | } 72 | 73 | public override string ToString() => string.Format("{0} transfer {1} compression ({2} content length)", Encoding, Compression, Length ?? 0); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /CSSockets/Http/Definition/Head.cs: -------------------------------------------------------------------------------- 1 | using CSSockets.Streams; 2 | 3 | namespace CSSockets.Http.Definition 4 | { 5 | public abstract class Head 6 | { 7 | public Version Version { get; set; } 8 | public HeaderCollection Headers { get; } 9 | 10 | public Head() => Headers = new HeaderCollection(); 11 | public Head(Version version) : this() => Version = version; 12 | public Head(Version version, HeaderCollection headers) 13 | { 14 | Version = version; 15 | Headers = headers; 16 | } 17 | } 18 | 19 | public abstract class HeadParser : Translator where T : Head 20 | { 21 | protected const char CR = '\r'; 22 | protected const char LF = '\n'; 23 | protected const string CRLF = "\r\n"; 24 | protected const char COLON = ':'; 25 | protected const char WS = ' '; 26 | } 27 | public abstract class HeadSerializer : Transform where T : Head 28 | { 29 | protected const char CR = '\r'; 30 | protected const char LF = '\n'; 31 | protected const string CRLF = "\r\n"; 32 | protected const char COLON = ':'; 33 | protected const char WS = ' '; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /CSSockets/Http/Definition/Headers.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Specialized; 3 | 4 | namespace CSSockets.Http.Definition 5 | { 6 | public struct Header 7 | { 8 | public string Key { get; } 9 | public string Value { get; } 10 | public Header(string key, string value) : this() { Key = key; Value = value; } 11 | } 12 | public sealed class HeaderCollection 13 | { 14 | private readonly StringDictionary Collection = new StringDictionary(); 15 | private readonly StringCollection Keys = new StringCollection(); 16 | public int Length => Keys.Count; 17 | 18 | public HeaderCollection() { } 19 | public HeaderCollection(IDictionary headers) 20 | { 21 | foreach (KeyValuePair header in headers) Add(header.Key, header.Value); 22 | } 23 | public HeaderCollection(IEnumerable
headers) 24 | { 25 | foreach (Header header in headers) Add(header.Key, header.Value); 26 | } 27 | public HeaderCollection(params Header[] headers) 28 | { 29 | foreach (Header header in headers) Add(header.Key, header.Value); 30 | } 31 | 32 | public string this[string key] 33 | { 34 | get => Collection[key.ToLower()]; 35 | set 36 | { 37 | if (key == null) Remove(key); 38 | else if (!Keys.Contains(key)) Insert(key, value); 39 | else if (value != string.Empty) Collection[key] = value; 40 | else Remove(key); 41 | } 42 | } 43 | public Header this[int index] => new Header(Keys[index], Collection[Keys[index]]); 44 | 45 | public bool Exists(string key) => Keys.Contains(key.ToLower()); 46 | public bool Add(Header header) 47 | { 48 | if (Keys.Contains(header.Key.ToLower())) return false; 49 | return Insert(header.Key.ToLower(), header.Value); 50 | } 51 | public bool Add(string key, string value) 52 | { 53 | if (Keys.Contains(key = key.ToLower())) return false; 54 | return Insert(key, value); 55 | } 56 | private bool Insert(string key, string value) 57 | { 58 | Keys.Add(key); 59 | Collection[key] = value; 60 | return true; 61 | } 62 | public bool Remove(string key) 63 | { 64 | if (!Keys.Contains(key)) return false; 65 | Keys.Remove(key); 66 | Collection.Remove(key); 67 | return true; 68 | } 69 | public bool Clear() 70 | { 71 | Collection.Clear(); 72 | Keys.Clear(); 73 | return true; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /CSSockets/Http/Definition/Messages.cs: -------------------------------------------------------------------------------- 1 | using CSSockets.Streams; 2 | 3 | namespace CSSockets.Http.Definition 4 | { 5 | public abstract class IncomingMessage : MemoryDuplex, IFinishable, IEndable 6 | where TReq : Head, new() where TRes : Head, new() 7 | { 8 | public TReq Head { get; } 9 | public Connection Connection { get; } 10 | 11 | // Head is supposed to be static, don't lock on Sync 12 | public Version Version => Head.Version; 13 | public string this[string key] => Head.Headers[key]; 14 | 15 | public virtual event ControlHandler OnFinish; 16 | 17 | public bool Ended { get; private set; } = false; 18 | public bool Finished { get; private set; } = false; 19 | 20 | public IncomingMessage(Connection connection, TReq head) 21 | { 22 | Connection = connection; 23 | Head = head; 24 | } 25 | 26 | public virtual bool Finish() 27 | { 28 | lock (Sync) 29 | { 30 | OnFinish?.Invoke(); 31 | return Finished = true; 32 | } 33 | } 34 | public virtual bool End() 35 | { 36 | lock (Sync) 37 | { 38 | return Ended = true && Connection.Terminate(); 39 | } 40 | } 41 | } 42 | 43 | public abstract class OutgoingMessage : MemoryDuplex, IFinishable, IEndable 44 | where TReq : Head, new() where TRes : Head, new() 45 | { 46 | public TRes Head { get; set; } = new TRes(); 47 | public Connection Connection { get; } 48 | 49 | public Version Version 50 | { 51 | get { lock (Sync) return Head.Version; } 52 | set { lock (Sync) { if (SentHead) return; Head.Version = value; } } 53 | } 54 | public string this[string key] 55 | { 56 | get { lock (Sync) return Head.Headers[key]; } 57 | set { lock (Sync) { if (SentHead) return; Head.Headers[key] = value; } } 58 | } 59 | 60 | public virtual event ControlHandler OnFinish; 61 | 62 | public bool Ended { get; private set; } = false; 63 | public bool SentHead { get; private set; } = false; 64 | public bool Finished { get; private set; } = false; 65 | 66 | public OutgoingMessage(Connection connection, Version version) 67 | { 68 | Connection = connection; 69 | Head.Version = version; 70 | } 71 | 72 | public virtual bool Finish() 73 | { 74 | lock (Sync) 75 | { 76 | OnFinish?.Invoke(); 77 | return Finished = true; 78 | } 79 | } 80 | public virtual bool SendHead() 81 | { 82 | lock (Sync) 83 | { 84 | if (SentHead) return false; 85 | return Connection.StartOutgoing(Head) && (SentHead = true); 86 | } 87 | } 88 | public virtual bool End() 89 | { 90 | lock (Sync) 91 | { 92 | return (!SentHead ? SendHead() : true) && Connection.FinishOutgoing() && (Ended = true); 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /CSSockets/Http/Definition/URL.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace CSSockets.Http.Definition 5 | { 6 | public struct Query 7 | { 8 | public string Key { get;set; } 9 | public string Value { get; set; } 10 | public Query(string key, string value) 11 | { 12 | Key = key; 13 | Value = value; 14 | } 15 | public override string ToString() 16 | => Key + "=" + Value; 17 | } 18 | public sealed class Queries 19 | { 20 | private readonly Dictionary Tokens = new Dictionary(); 21 | private readonly List Keys = new List(); 22 | public int Length => Keys.Count; 23 | 24 | public string this[string key] 25 | { 26 | get => Get(key)?.Value; 27 | set => Set(key, value); 28 | } 29 | public Query? this[int index] => Get(Keys[index]); 30 | 31 | public Query? Get(string key) 32 | { 33 | if (!Tokens.TryGetValue(key, out Query item)) 34 | return null; 35 | return item; 36 | } 37 | public bool Set(string key, string value) 38 | { 39 | if (!Tokens.ContainsKey(key)) Keys.Add(key); 40 | Tokens[key] = new Query(key, value); 41 | return true; 42 | } 43 | public bool Remove(string key) 44 | { 45 | if (!Tokens.ContainsKey(key)) return false; 46 | Tokens.Remove(key); 47 | Keys.Remove(key); 48 | return true; 49 | } 50 | public bool Clear() 51 | { 52 | Tokens.Clear(); 53 | Keys.Clear(); 54 | return true; 55 | } 56 | 57 | public static bool TryParse(string s, out Queries result) 58 | { 59 | Queries temp = new Queries(); 60 | result = null; 61 | if (s.Length == 0) 62 | { 63 | // empty 64 | result = temp; 65 | return true; 66 | } 67 | if (!s.StartsWith("?")) return false; 68 | string[] split = s.Substring(1).Split("&"); 69 | for (int i = 0; i < split.Length; i++) 70 | { 71 | string queryToken = split[i]; 72 | string[] queryTokenSplit = queryToken.Split("="); 73 | if (queryTokenSplit.Length != 2) return false; 74 | temp.Set(queryTokenSplit[0], queryTokenSplit[1]); 75 | } 76 | result = temp; 77 | return true; 78 | } 79 | public static Queries Parse(string s) 80 | { 81 | if (!TryParse(s, out Queries result)) 82 | throw new ArgumentException("Invalid query string"); 83 | return result; 84 | } 85 | 86 | public override string ToString() 87 | { 88 | string s = null; 89 | for (int i = 0; i < Keys.Count; i++) 90 | { 91 | s = s ?? "?"; 92 | s += Keys[i] + "=" + Tokens[Keys[i]].Value + "&"; 93 | } 94 | return s == null ? "" : s.Substring(0, s.Length); 95 | } 96 | } 97 | public sealed class Path 98 | { 99 | private enum TraverseResult : byte 100 | { 101 | Success = 0, 102 | NotApath = 1, 103 | NotRpath = 2, 104 | APathRdirs = 3, 105 | RpathInvalidRdirs = 4, 106 | TraverseBeyondRoot = 5 107 | } 108 | 109 | private List Location { get; set; } 110 | public string FullPath { get; private set; } 111 | public string Directory { get; private set; } 112 | public string Entry { get; private set; } 113 | 114 | private void AssembleStringified() 115 | { 116 | FullPath = "/"; 117 | Directory = "/"; 118 | Entry = ""; 119 | int i = 0; 120 | for (; i < Location.Count - 1; i++) 121 | FullPath = Directory += Location[i] + "/"; 122 | FullPath += Location.Count > 0 ? (Entry += Location[i++]) : ""; 123 | } 124 | 125 | private TraverseResult InternalInitialize(string path) 126 | { 127 | List pathList = new List(); 128 | string[] dirs = path.Split('/'); 129 | if (dirs[0] != "") 130 | return TraverseResult.NotApath; 131 | for (int i = 1; i < dirs.Length; i++) 132 | { 133 | if (dirs[i] == "." || dirs[i] == "..") 134 | return TraverseResult.APathRdirs; 135 | pathList.Add(dirs[i]); 136 | } 137 | Location = pathList; 138 | AssembleStringified(); 139 | return TraverseResult.Success; 140 | } 141 | public bool Initialize(string path = "/") => InternalInitialize(path) == TraverseResult.Success; 142 | 143 | private TraverseResult InternalTraverse(string path) 144 | { 145 | List pathList = new List(Location); 146 | string[] dirs = path.Split('/'); 147 | if (dirs[0] == "") 148 | return TraverseResult.NotRpath; 149 | bool acceptingBackwards = true; 150 | for (int i = 1; i < dirs.Length; i++) 151 | { 152 | if (dirs[i] == "..") 153 | { 154 | if (!acceptingBackwards) return TraverseResult.RpathInvalidRdirs; 155 | pathList.RemoveAt(pathList.Count - 1); 156 | } 157 | else if (dirs[i] == ".") { if (!acceptingBackwards) return TraverseResult.RpathInvalidRdirs; } 158 | else { pathList.Add(dirs[i]); acceptingBackwards = false; } 159 | } 160 | Location = pathList; 161 | AssembleStringified(); 162 | return TraverseResult.Success; 163 | } 164 | public bool Traverse(string path = "/") => InternalTraverse(path) == TraverseResult.Success; 165 | 166 | public bool Contains(Path other) 167 | { 168 | int checkLen = Math.Min(Location.Count, other.Location.Count); 169 | for (int i = 0; i < checkLen; i++) if (Location[i] != other.Location[i]) return false; 170 | return checkLen == Location.Count; 171 | } 172 | 173 | public Path() => Initialize(); 174 | public Path(Path other) => Initialize(other.FullPath); 175 | public Path(string path) => Initialize(path); 176 | 177 | public override string ToString() => FullPath; 178 | 179 | public static implicit operator Path(string value) => new Path(value); 180 | public static implicit operator string(Path value) => value.FullPath; 181 | } 182 | public sealed class URL 183 | { 184 | public Path Path { get; set; } 185 | public string Hash { get; set; } 186 | public Queries Queries { get; set; } 187 | public string Query 188 | { 189 | get => Queries?.ToString(); 190 | set => Queries = Queries.Parse(value); 191 | } 192 | 193 | public static URL Parse(string s) 194 | { 195 | if (!TryParse(s, out URL result)) 196 | throw new ArgumentException("Invalid query"); 197 | return result; 198 | } 199 | public static bool TryParse(string s, out URL result) 200 | { 201 | URL temp = new URL(); 202 | result = null; 203 | 204 | string[] splitForHash = s.Split("#"); 205 | if (splitForHash.Length > 1) temp.Hash = splitForHash[1]; 206 | string[] splitForSearch = splitForHash[0].Split("?"); 207 | if (splitForSearch.Length > 1) 208 | { 209 | if (!Queries.TryParse("?" + splitForSearch[1], out Queries searches)) 210 | return false; 211 | temp.Queries = searches; 212 | } 213 | Path tempPath = new Path(); 214 | if (!tempPath.Initialize(splitForSearch[0])) 215 | return false; 216 | temp.Path = tempPath; 217 | result = temp; 218 | return true; 219 | } 220 | 221 | public URL() 222 | { 223 | Path = null; 224 | Hash = null; 225 | Queries = null; 226 | } 227 | public URL(string path, string hash, string searchString) 228 | { 229 | Path.Traverse(path); 230 | Hash = hash; 231 | Query = searchString; 232 | } 233 | public URL(Path path, string hash, Queries searches) 234 | { 235 | Path = new Path(path.FullPath); 236 | Hash = hash; 237 | Queries = searches; 238 | } 239 | public URL(string query) 240 | { 241 | URL result = Parse(query); 242 | Path = result.Path; 243 | Hash = result.Hash; 244 | Queries = result.Queries; 245 | } 246 | public override string ToString() => 247 | (Path.ToString() ?? throw new InvalidOperationException("Path in HttpQuery is null")) 248 | + (Query ?? "") 249 | + (Hash == null ? "" : "#" + Hash); 250 | 251 | public static implicit operator URL(string value) => Parse(value); 252 | public static implicit operator string(URL value) => value.ToString(); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /CSSockets/Http/Definition/Version.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CSSockets.Http.Definition 4 | { 5 | public struct Version 6 | { 7 | public byte Major { get; } 8 | public byte Minor { get; } 9 | 10 | public static bool TryParse(string str, out Version result) 11 | { 12 | result = default(Version); 13 | if (str.Length < 5 || !str.StartsWith("HTTP/")) 14 | return false; 15 | str = str.Substring(5); 16 | string[] split = str.Split("."); 17 | if (split.Length != 2) 18 | return false; 19 | if (!byte.TryParse(split[0], out byte _1)) 20 | return false; 21 | if (!byte.TryParse(split[1], out byte _2)) 22 | return false; 23 | result = new Version(_1, _2); 24 | return true; 25 | } 26 | public static Version Parse(string str) 27 | { 28 | if (!TryParse(str, out Version result)) 29 | throw new ArgumentException("Invalid string format"); 30 | return result; 31 | } 32 | 33 | public Version(byte major, byte minor) 34 | { 35 | Major = major; 36 | Minor = minor; 37 | } 38 | 39 | public override string ToString() 40 | => "HTTP/" + Major + "." + Minor; 41 | 42 | public static implicit operator System.Version(Version version) 43 | => new System.Version(version.Major, version.Minor); 44 | public static implicit operator Version(System.Version version) 45 | { 46 | if (version.Revision != -1 || version.Build != -1 || 47 | version.Major < 0 || version.Minor < 0 || 48 | version.Major > 255 || version.Minor > 255) 49 | throw new InvalidOperationException("Cannot convert version " + version + " to an HTTP version"); 50 | return new Version((byte)version.Major, (byte)version.Minor); 51 | } 52 | public static implicit operator Version(string str) => Parse(str); 53 | public static implicit operator string(Version version) => version.ToString(); 54 | 55 | public static bool operator ==(Version a, Version b) => a.Major == b.Major && a.Minor == b.Minor; 56 | public static bool operator !=(Version a, Version b) => a.Major != b.Major || a.Minor != b.Minor; 57 | public override bool Equals(object obj) 58 | { 59 | if (obj is null) return false; 60 | if (!(obj is Version)) return false; 61 | Version actual = (Version)obj; 62 | return Major == actual.Major && Minor == actual.Minor; 63 | } 64 | public override int GetHashCode() => Major * 256 + Minor; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /CSSockets/Http/Reference/Body.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using CSSockets.Streams; 4 | using System.Globalization; 5 | using CSSockets.Http.Definition; 6 | 7 | namespace CSSockets.Http.Reference 8 | { 9 | public sealed class BodySerializer : Duplex, IFinishable 10 | { 11 | private const string CRLF = "\r\n"; 12 | private static readonly byte[] CRLF_BYTES = Encoding.ASCII.GetBytes(CRLF); 13 | private static readonly byte[] LAST_CHUNK = Encoding.ASCII.GetBytes("0" + CRLF + CRLF); 14 | 15 | public event ControlHandler OnFinish; 16 | 17 | public bool Finished { get; private set; } = true; 18 | public BodyType? Type { get; private set; } = null; 19 | public ulong CurrentLength { get; private set; } = 0; 20 | public ulong CurrentEncodedLength { get; private set; } = 0; 21 | 22 | private Duplex Transform = null; 23 | private bool IsTransforming = false; 24 | private bool IsTransformFinishable = false; 25 | 26 | public bool TrySetFor(BodyType type) 27 | { 28 | lock (Sync) 29 | { 30 | if (!Finished) return false; 31 | if (type.Compression == TransferCompression.Compress) 32 | return false; 33 | if (type.Encoding == TransferEncoding.None) 34 | return true; 35 | Finished = false; 36 | Type = type; 37 | switch (type.Compression) 38 | { 39 | case TransferCompression.None: 40 | IsTransforming = false; 41 | break; 42 | case TransferCompression.Deflate: 43 | IsTransforming = true; 44 | Transform = new DeflateCompressor(); 45 | IsTransformFinishable = true; 46 | break; 47 | case TransferCompression.Gzip: 48 | IsTransforming = true; 49 | Transform = new GzipCompressor(); 50 | IsTransformFinishable = true; 51 | break; 52 | } 53 | return true; 54 | } 55 | } 56 | 57 | protected sealed override bool HandleWritable(byte[] source) 58 | { 59 | if (IsTransforming) 60 | { 61 | if (!Transform.Write(source)) return false; 62 | if (Transform.BufferedReadable > 0) return WriteChunk(Transform.Read()); 63 | return true; 64 | } 65 | return WriteChunk(source); 66 | } 67 | private bool WriteChunk(byte[] source) 68 | { 69 | switch (Type.Value.Encoding) 70 | { 71 | case TransferEncoding.None: return false; 72 | case TransferEncoding.Binary: 73 | CurrentLength = CurrentEncodedLength += (ulong)source.LongLength; 74 | if (CurrentEncodedLength > Type.Value.Length) return false; 75 | return HandleReadable(source); 76 | case TransferEncoding.Chunked: 77 | string str = source.Length.ToString("X"); 78 | if (!HandleReadable(Encoding.ASCII.GetBytes(str))) return false; 79 | if (!HandleReadable(CRLF_BYTES)) return false; 80 | if (!HandleReadable(source)) return false; 81 | if (!HandleReadable(CRLF_BYTES)) return false; 82 | CurrentLength += (ulong)source.LongLength; 83 | CurrentEncodedLength += (ulong)str.Length + 2 + (ulong)source.LongLength + 2; 84 | return true; 85 | default: return false; 86 | } 87 | } 88 | private bool WriteLast() 89 | { 90 | switch (Type.Value.Encoding) 91 | { 92 | case TransferEncoding.None: return false; 93 | case TransferEncoding.Binary: return true; 94 | case TransferEncoding.Chunked: return HandleReadable(LAST_CHUNK); 95 | default: return false; 96 | } 97 | } 98 | public bool Finish() 99 | { 100 | lock (Sync) 101 | { 102 | if (Finished) return false; 103 | if (IsTransforming) 104 | { 105 | if (IsTransformFinishable && !(Transform as IFinishable).Finish()) 106 | return false; 107 | if (Transform.BufferedReadable > 0 && !WriteChunk(Transform.Read())) 108 | return false; 109 | } 110 | if (!WriteLast()) return false; 111 | Transform = null; 112 | IsTransforming = false; 113 | IsTransformFinishable = false; 114 | Finished = true; 115 | Type = null; 116 | CurrentLength = CurrentEncodedLength = 0; 117 | OnFinish?.Invoke(); 118 | return true; 119 | } 120 | } 121 | } 122 | 123 | public sealed class BodyParser : Duplex, IFinishable 124 | { 125 | private enum ParseState : byte 126 | { 127 | Dormant, 128 | Binary, 129 | Chunked_Length, 130 | Chunked_LengthLf, 131 | Chunked_ChunkData, 132 | Chunked_ChunkCr, 133 | Chunked_ChunkLf, 134 | Chunked_Trailer, 135 | Chunked_Lf 136 | } 137 | private const char CR = '\r'; 138 | private const char LF = '\n'; 139 | 140 | private ParseState State = ParseState.Dormant; 141 | public bool Finished => State == ParseState.Dormant; 142 | public bool Malformed { get; private set; } = false; 143 | public event ControlHandler OnFinish; 144 | 145 | private Duplex ExcessStore = new MemoryDuplex(); 146 | public IReadable Excess => ExcessStore; 147 | 148 | private Duplex Transform = null; 149 | private bool IsTransforming = false; 150 | private bool IsTransformFinishable = false; 151 | 152 | private string ChunkLengthString = null; 153 | private ulong ChunkIndex = 0; 154 | private ulong ChunkLength = 0; 155 | 156 | public BodyType? Type { get; private set; } = null; 157 | public ulong ContentLength { get; private set; } = 0; 158 | public ulong EncodedContentLength { get; private set; } = 0; 159 | 160 | public bool TrySetFor(BodyType type) 161 | { 162 | lock (Sync) 163 | { 164 | if (State != ParseState.Dormant) return false; 165 | if (type.Compression == TransferCompression.Compress) 166 | return false; 167 | if (type.Encoding == TransferEncoding.None) 168 | return true; 169 | Type = type; 170 | switch (type.Compression) 171 | { 172 | case TransferCompression.None: 173 | IsTransforming = false; 174 | break; 175 | case TransferCompression.Deflate: 176 | IsTransforming = true; 177 | Transform = new DeflateDecompressor(); 178 | IsTransformFinishable = true; 179 | break; 180 | case TransferCompression.Gzip: 181 | IsTransforming = true; 182 | Transform = new GzipDecompressor(); 183 | IsTransformFinishable = true; 184 | break; 185 | } 186 | switch (type.Encoding) 187 | { 188 | case TransferEncoding.Binary: State = ParseState.Binary; break; 189 | case TransferEncoding.Chunked: State = ParseState.Chunked_Length; break; 190 | } 191 | return true; 192 | } 193 | } 194 | 195 | protected sealed override bool HandleWritable(byte[] source) 196 | { 197 | if (Malformed) return false; 198 | char c = '\0'; ulong length, i = 0, sourceLength = (ulong)source.LongLength; 199 | for (; i < sourceLength;) 200 | switch (State) 201 | { 202 | case ParseState.Dormant: return false; 203 | case ParseState.Binary: 204 | ContentLength = EncodedContentLength += sourceLength; 205 | if (!WriteChunk(source)) return false; 206 | if (Type.Value.Length.HasValue && Type.Value.Length == ContentLength) 207 | return Finish(); 208 | return true; 209 | case ParseState.Chunked_Length: 210 | EncodedContentLength++; 211 | c = (char)source[i++]; 212 | if (c == CR) State = ParseState.Chunked_LengthLf; 213 | else ChunkLengthString = ChunkLengthString == null ? c.ToString() : ChunkLengthString + c; 214 | break; 215 | case ParseState.Chunked_LengthLf: 216 | EncodedContentLength++; 217 | c = (char)source[i++]; 218 | if (c != LF) return !(Malformed = true); 219 | ChunkIndex = 0; 220 | if (!ulong.TryParse(ChunkLengthString, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out length)) 221 | return !(Malformed = true); 222 | ChunkLengthString = null; 223 | State = (ChunkLength = length) == 0 ? ParseState.Chunked_Trailer : ParseState.Chunked_ChunkData; 224 | break; 225 | case ParseState.Chunked_ChunkData: 226 | length = Math.Min(ChunkLength - ChunkIndex, sourceLength - i); 227 | WriteChunk(PrimitiveBuffer.Slice(source, i, i += length)); 228 | ChunkIndex += length; ContentLength += length; EncodedContentLength += length; 229 | if (ChunkIndex >= ChunkLength) State = ParseState.Chunked_ChunkCr; 230 | break; 231 | case ParseState.Chunked_ChunkCr: 232 | EncodedContentLength++; 233 | c = (char)source[i++]; 234 | if (c != CR) return !(Malformed = true); 235 | State = ParseState.Chunked_ChunkLf; 236 | break; 237 | case ParseState.Chunked_ChunkLf: 238 | EncodedContentLength++; 239 | c = (char)source[i++]; 240 | if (c != LF) return !(Malformed = true); 241 | ChunkIndex = 0; 242 | State = ParseState.Chunked_Length; 243 | break; 244 | case ParseState.Chunked_Trailer: 245 | while ((c = (char)source[i++]) != CR) EncodedContentLength++; 246 | State = ParseState.Chunked_Lf; 247 | break; 248 | case ParseState.Chunked_Lf: 249 | EncodedContentLength++; 250 | c = (char)source[i++]; 251 | if (c != LF) return !(Malformed = true); 252 | ExcessStore.Write(source, i); 253 | return Finish(); 254 | } 255 | return true; 256 | } 257 | private bool WriteChunk(byte[] source) 258 | { 259 | if (IsTransforming) 260 | { 261 | if (!Transform.Write(source)) return false; 262 | if (Transform.BufferedReadable > 0) return HandleReadable(Transform.Read()); 263 | return true; 264 | } 265 | return HandleReadable(source); 266 | } 267 | 268 | public bool Finish() 269 | { 270 | lock (Sync) 271 | { 272 | if (State == ParseState.Dormant) return false; 273 | State = ParseState.Dormant; 274 | if (IsTransforming) 275 | { 276 | if (IsTransformFinishable && !(Transform as IFinishable).Finish()) 277 | return false; 278 | if (Transform.BufferedReadable > 0 && !HandleReadable(Transform.Read())) 279 | return false; 280 | } 281 | Transform = null; 282 | Malformed = false; 283 | IsTransforming = IsTransformFinishable = false; 284 | ChunkLengthString = null; 285 | ChunkIndex = ChunkLength = 0; 286 | OnFinish?.Invoke(); 287 | return true; 288 | } 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /CSSockets/Http/Reference/ClientConnection.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using CSSockets.Tcp; 3 | using CSSockets.Streams; 4 | using CSSockets.Http.Definition; 5 | using System.Collections.Generic; 6 | 7 | namespace CSSockets.Http.Reference 8 | { 9 | public sealed class ClientConnection : Connection 10 | { 11 | private OutgoingRequest _Outgoing = null; 12 | private IncomingResponse _Incoming = null; 13 | public override OutgoingMessage Outgoing => _Outgoing; 14 | public override IncomingMessage Incoming => _Incoming; 15 | private readonly Queue unanswered = new Queue(); 16 | 17 | public ClientConnection(EndPoint connectEndPoint, ControlHandler onOpen = null) : this(new Connection()) 18 | => Base.Connect(connectEndPoint); 19 | public ClientConnection(Connection connection) : base(connection) 20 | { 21 | IncomingHead = new ResponseHeadParser(); 22 | IncomingBody = new BodyParser(); 23 | OutgoingHead = new RequestHeadSerializer(); 24 | OutgoingBody = new BodySerializer(); 25 | IncomingHead.OnFail += OnSegmentFail; 26 | IncomingBody.OnFail += OnSegmentFail; 27 | OutgoingHead.OnFail += OnSegmentFail; 28 | OutgoingBody.OnFail += OnSegmentFail; 29 | IncomingHead.Pipe(excess); 30 | IncomingBody.Excess.Pipe(excess); 31 | } 32 | 33 | private readonly MemoryDuplex excess = new MemoryDuplex(); 34 | private bool gotReqHead = false; 35 | private bool hasReqBody = false; 36 | private bool pdnResHead = false; 37 | private bool gotResHead = false; 38 | private bool gotResBody = false; 39 | 40 | public OutgoingRequest Enqueue(Version version, string method, URL url) 41 | { 42 | lock (Sync) 43 | { 44 | if (_Outgoing != null) return null; 45 | gotReqHead = hasReqBody = gotResHead = gotResBody = false; 46 | OutgoingRequest newPending = new OutgoingRequest(this, version, method, url); 47 | _Outgoing = newPending; 48 | unanswered.Enqueue(newPending); 49 | return newPending; 50 | } 51 | } 52 | public override bool StartOutgoing(RequestHead head) 53 | { 54 | lock (Sync) 55 | { 56 | if (_Outgoing == null) return false; 57 | 58 | BodyType? type = BodyType.TryDetectFor(head, true); 59 | if (type == null) return false; 60 | if (!OutgoingBody.TrySetFor(type.Value)) return false; 61 | 62 | OutgoingHead.Write(head); 63 | OutgoingHead.Burst(Base); 64 | 65 | if (!OutgoingBody.Finished) 66 | { 67 | hasReqBody = true; 68 | OutgoingBody.OnFinish += FinishOutgoingMessage; 69 | OutgoingBody.Pipe(Base); 70 | _Outgoing.Pipe(OutgoingBody); 71 | } 72 | WaitIncoming(); 73 | return gotReqHead = true; 74 | } 75 | } 76 | private void FinishOutgoingMessage() 77 | { 78 | if (!FinishOutgoing()) Terminate(); 79 | } 80 | public override bool FinishOutgoing() 81 | { 82 | lock (Sync) 83 | { 84 | if (!hasReqBody) return true; 85 | OutgoingBody.OnFinish -= FinishOutgoingMessage; 86 | if (!OutgoingBody.Finished) OutgoingBody.Finish(); 87 | _Outgoing.Finish(); 88 | OutgoingBody.Unpipe(); 89 | _Outgoing.Unpipe(); 90 | _Outgoing = null; 91 | return !(hasReqBody = false); 92 | } 93 | } 94 | private void WaitIncoming() 95 | { 96 | lock (Sync) 97 | { 98 | if (pdnResHead || unanswered.Count == 0) return; 99 | pdnResHead = true; 100 | IncomingHead.OnCollect += PushIncoming; 101 | excess.Burst(IncomingHead); 102 | if (!gotResHead) Base.Pipe(IncomingHead); 103 | } 104 | } 105 | private void PushIncoming(ResponseHead head) 106 | { 107 | lock (Sync) 108 | { 109 | gotResHead = true; 110 | IncomingHead.OnCollect -= PushIncoming; 111 | 112 | BodyType? type = BodyType.TryDetectFor(head, true); 113 | if (type == null) { Terminate(); return; } 114 | if (!IncomingBody.TrySetFor(type.Value)) { Terminate(); return; } 115 | 116 | IncomingResponse incoming = _Incoming = new IncomingResponse(this, head); 117 | OutgoingRequest outgoing = unanswered.Dequeue(); 118 | 119 | outgoing.FireResponse(incoming); 120 | if (Ended || Frozen) return; 121 | 122 | if (!IncomingBody.Finished) 123 | { 124 | IncomingBody.OnFinish += FinishIncomingMessage; 125 | IncomingBody.Pipe(_Incoming); 126 | excess.Burst(IncomingBody); 127 | if (!gotResBody) Base.Pipe(IncomingBody); 128 | } 129 | } 130 | } 131 | private void FinishIncomingMessage() 132 | { 133 | lock (Sync) 134 | { 135 | gotResBody = true; 136 | IncomingBody.OnFinish -= FinishIncomingMessage; 137 | _Incoming.Finish(); 138 | _Incoming = null; 139 | Base.Unpipe(); 140 | pdnResHead = false; 141 | WaitIncoming(); 142 | } 143 | } 144 | private void OnSegmentFail() => Terminate(); 145 | public override byte[] Freeze() 146 | { 147 | lock (Sync) 148 | { 149 | _Incoming = null; _Outgoing = null; Frozen = true; unanswered.Clear(); 150 | gotReqHead = hasReqBody = pdnResHead = gotResHead = gotResBody = false; 151 | Base.Unpipe(); 152 | IncomingHead.Unpipe(); 153 | IncomingHead.OnFail -= OnSegmentFail; 154 | if (pdnResHead) IncomingHead.OnCollect -= PushIncoming; 155 | IncomingBody.Unpipe(); 156 | IncomingBody.Excess.Unpipe(); 157 | IncomingBody.OnFail -= OnSegmentFail; 158 | if (!hasReqBody) IncomingBody.OnFinish -= FinishIncomingMessage; 159 | OutgoingHead.Unpipe(); 160 | OutgoingHead.OnFail -= OnSegmentFail; 161 | OutgoingBody.Unpipe(); 162 | OutgoingBody.OnFail -= OnSegmentFail; 163 | if (!gotResBody) OutgoingBody.OnFinish -= FinishOutgoingMessage; 164 | return excess.Read(); 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /CSSockets/Http/Reference/Listener.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using CSSockets.Tcp; 3 | using System.Collections.Generic; 4 | 5 | namespace CSSockets.Http.Reference 6 | { 7 | public delegate void ConnectionHandler(ServerConnection connection); 8 | public sealed class Listener 9 | { 10 | private readonly object Sync = new object(); 11 | private readonly HashSet Connections = new HashSet(); 12 | 13 | public Tcp.Listener Base { get; } 14 | public bool Listening => Base.Listening; 15 | public EndPoint BindEndPoint 16 | { 17 | get => Base.BindEndPoint; 18 | set => Base.BindEndPoint = value; 19 | } 20 | 21 | public ConnectionHandler OnConnection { get; set; } = null; 22 | private IncomingRequestHandler _OnRequest = null; 23 | public IncomingRequestHandler OnRequest 24 | { 25 | get => _OnRequest; 26 | set => _OnRequest = Listening ? _OnRequest : value; 27 | } 28 | 29 | public Listener() 30 | { 31 | Base = new Tcp.Listener(); 32 | Base.OnConnection += ConnectionHandler; 33 | } 34 | public Listener(EndPoint endPoint) 35 | { 36 | Base = new Tcp.Listener(endPoint); 37 | Base.OnConnection += ConnectionHandler; 38 | } 39 | public Listener(Tcp.Listener listener) 40 | { 41 | Base = listener; 42 | Base.OnConnection += ConnectionHandler; 43 | } 44 | 45 | public void Start() 46 | { 47 | lock (Sync) Base.Start(); 48 | } 49 | public void Stop() 50 | { 51 | lock (Sync) 52 | { 53 | Base.Stop(); 54 | Connection[] connections = new Connection[Connections.Count]; 55 | Connections.CopyTo(connections); 56 | for (int i = 0; i < connections.Length; i++) connections[i].Terminate(); 57 | Connections.Clear(); 58 | } 59 | } 60 | 61 | private void ConnectionHandler(Connection connection) 62 | { 63 | ServerConnection upgraded; 64 | lock (Sync) 65 | { 66 | if (!Listening) { connection.Terminate(); return; } 67 | upgraded = new ServerConnection(connection, _OnRequest); 68 | Connections.Add(connection); 69 | upgraded.OnEnd += () => { lock (Sync) Connections.Remove(connection); }; 70 | } 71 | OnConnection?.Invoke(upgraded); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CSSockets/Http/Reference/Messages.cs: -------------------------------------------------------------------------------- 1 | using CSSockets.Streams; 2 | using CSSockets.Http.Definition; 3 | 4 | namespace CSSockets.Http.Reference 5 | { 6 | public sealed class IncomingRequest : IncomingMessage 7 | { 8 | public new ServerConnection Connection => base.Connection as ServerConnection; 9 | 10 | public URL URL => Head.URL; 11 | public Path Path => Head.URL.Path; 12 | public string Hash => Head.URL.Hash; 13 | public string Method => Head.Method; 14 | public Queries Queries => Head.URL.Queries; 15 | 16 | public IncomingRequest(ServerConnection connection, RequestHead head) : base(connection, head) { } 17 | } 18 | public sealed class IncomingResponse : IncomingMessage 19 | { 20 | public new ClientConnection Connection => base.Connection as ClientConnection; 21 | 22 | public ushort StatusCode => Head.StatusCode; 23 | public string StatusDescription => Head.StatusDescription; 24 | 25 | public IncomingResponse(ClientConnection connection, ResponseHead head) : base(connection, head) { } 26 | } 27 | 28 | public delegate void IncomingResponseHandler(IncomingResponse response); 29 | public sealed class OutgoingRequest : OutgoingMessage 30 | { 31 | public new ClientConnection Connection => base.Connection as ClientConnection; 32 | 33 | public string Method 34 | { 35 | get { lock (Sync) return Head.Method; } 36 | set { lock (Sync) { if (SentHead) return; Head.Method = value; } } 37 | } 38 | public URL URL 39 | { 40 | get { lock (Sync) return Head.URL; } 41 | set { lock (Sync) { if (SentHead) return; Head.URL = value; } } 42 | } 43 | 44 | public bool Responded { get; private set; } = false; 45 | public event IncomingResponseHandler OnResponse; 46 | internal bool FireResponse(IncomingResponse response) 47 | { 48 | lock (Sync) 49 | { 50 | if (Responded) return false; 51 | OnResponse?.Invoke(response); 52 | return Responded = true; 53 | } 54 | } 55 | 56 | public OutgoingRequest(ClientConnection connection, Version version, string method, URL url) : base(connection, version) 57 | { 58 | Method = method; 59 | URL = url; 60 | } 61 | } 62 | public sealed class OutgoingResponse : OutgoingMessage 63 | { 64 | public new ServerConnection Connection => base.Connection as ServerConnection; 65 | 66 | public ushort StatusCode 67 | { 68 | get { lock (Sync) return Head.StatusCode; } 69 | set { lock (Sync) { if (SentHead) return; Head.StatusCode = value; } } 70 | } 71 | public string StatusDescription 72 | { 73 | get { lock (Sync) return Head.StatusDescription; } 74 | set { lock (Sync) { if (SentHead) return; Head.StatusDescription = value; } } 75 | } 76 | 77 | public OutgoingResponse(ServerConnection connection, Version version) : base(connection, version) { } 78 | 79 | public bool SendHead(ushort statusCode, string statusDescription) 80 | { 81 | lock (Sync) 82 | { 83 | if (SentHead) return false; 84 | Head.StatusCode = statusCode; 85 | Head.StatusDescription = statusDescription; 86 | return SendHead(); 87 | } 88 | } 89 | public bool End(ushort statusCode, string statusDescription) 90 | { 91 | lock (Sync) 92 | { 93 | if (SentHead) return false; 94 | Head.StatusCode = statusCode; 95 | Head.StatusDescription = statusDescription; 96 | return End(); 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /CSSockets/Http/Reference/RequestHead.cs: -------------------------------------------------------------------------------- 1 | using CSSockets.Streams; 2 | using CSSockets.Http.Definition; 3 | 4 | namespace CSSockets.Http.Reference 5 | { 6 | public sealed class RequestHead : Head 7 | { 8 | public string Method { get; set; } 9 | public URL URL { get; set; } 10 | 11 | public RequestHead() : base() { } 12 | public RequestHead(Version version, string method, URL query) : base(version) 13 | { 14 | Method = method; 15 | URL = query; 16 | } 17 | public RequestHead(Version version, string method, URL query, HeaderCollection headers) : base(version, headers) 18 | { 19 | Method = method; 20 | URL = query; 21 | } 22 | } 23 | 24 | public sealed class RequestHeadSerializer : HeadSerializer 25 | { 26 | public override bool Write(RequestHead source) 27 | { 28 | string stringified = source.Method + WS + source.URL + WS + source.Version + CRLF; 29 | for (int i = 0; i < source.Headers.Length; i++) 30 | stringified += source.Headers[i].Key.ToLower() + COLON + WS + source.Headers[i].Value + CRLF; 31 | stringified += CRLF; 32 | return HandleReadable(System.Text.Encoding.ASCII.GetBytes(stringified)); 33 | } 34 | } 35 | public sealed class RequestHeadParser : HeadParser 36 | { 37 | private enum ParseState : byte 38 | { 39 | Method, 40 | Query, 41 | Version, 42 | FirstLf, 43 | HeaderName, 44 | HeaderValue, 45 | HeaderLf, 46 | Lf 47 | } 48 | 49 | private ParseState State = ParseState.Method; 50 | public bool Malformed { get; private set; } = false; 51 | 52 | public RequestHeadParser() => Reset(); 53 | 54 | private string IncomingMethod; 55 | private string IncomingQuery; 56 | private string IncomingVersion; 57 | private Version IncomingVersionValue; 58 | private HeaderCollection IncomingHeaders; 59 | private string IncomingHeaderName; 60 | private string IncomingHeaderValue; 61 | 62 | public bool Reset() 63 | { 64 | lock (Sync) 65 | { 66 | Malformed = false; 67 | State = ParseState.Method; 68 | IncomingMethod = ""; 69 | IncomingQuery = ""; 70 | IncomingVersion = ""; 71 | IncomingVersionValue = default(Version); 72 | IncomingHeaders = new HeaderCollection(); 73 | IncomingHeaderName = ""; 74 | IncomingHeaderValue = ""; 75 | return true; 76 | } 77 | } 78 | protected override bool HandleWritable(byte[] source) 79 | { 80 | if (Malformed) return false; 81 | ulong i = 0, sourceLength = (ulong)source.LongLength; 82 | for (char c; i < sourceLength; i++) 83 | { 84 | c = (char)source[i]; 85 | switch (State) 86 | { 87 | case ParseState.Method: 88 | if (c != WS) IncomingMethod += c; 89 | else State = ParseState.Query; 90 | break; 91 | case ParseState.Query: 92 | if (c != WS) IncomingQuery += c; 93 | else 94 | { 95 | if (!URL.TryParse(IncomingQuery, out URL result)) 96 | return !(Malformed = true); 97 | IncomingQuery = result; 98 | State = ParseState.Version; 99 | } 100 | break; 101 | case ParseState.Version: 102 | if (c != CR) IncomingVersion += c; 103 | else 104 | { 105 | if (!Version.TryParse(IncomingVersion, out Version result)) 106 | return !(Malformed = true); 107 | IncomingVersionValue = result; 108 | State = ParseState.FirstLf; 109 | } 110 | break; 111 | case ParseState.FirstLf: 112 | if (c != LF) return !(Malformed = true); 113 | State = ParseState.HeaderName; 114 | break; 115 | case ParseState.HeaderName: 116 | if (c == CR) State = ParseState.Lf; 117 | else if (c != COLON) IncomingHeaderName += c; 118 | else State = ParseState.HeaderValue; 119 | break; 120 | case ParseState.HeaderValue: 121 | if (c != CR) IncomingHeaderValue += c; 122 | else 123 | { 124 | IncomingHeaders.Add(IncomingHeaderName, IncomingHeaderValue.TrimStart()); 125 | IncomingHeaderName = ""; 126 | IncomingHeaderValue = ""; 127 | State = ParseState.HeaderLf; 128 | } 129 | break; 130 | case ParseState.HeaderLf: 131 | if (c != LF) return !(Malformed = true); 132 | else State = ParseState.HeaderName; 133 | break; 134 | case ParseState.Lf: 135 | if (c != LF) return !(Malformed = true); 136 | HandleReadable(PrimitiveBuffer.Slice(source, i + 1, sourceLength)); 137 | Pickup(new RequestHead(IncomingVersionValue, IncomingMethod, IncomingQuery, IncomingHeaders)); 138 | return Reset(); 139 | } 140 | } 141 | return true; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /CSSockets/Http/Reference/ResponseHead.cs: -------------------------------------------------------------------------------- 1 | using CSSockets.Streams; 2 | using CSSockets.Http.Definition; 3 | 4 | namespace CSSockets.Http.Reference 5 | { 6 | public sealed class ResponseHead : Head 7 | { 8 | public ushort StatusCode { get; set; } 9 | public string StatusDescription { get; set; } 10 | 11 | public ResponseHead() : base() { } 12 | public ResponseHead(Version version, ushort statusCode, string statusDescription) : base(version) 13 | { 14 | StatusCode = statusCode; 15 | StatusDescription = statusDescription; 16 | } 17 | public ResponseHead(Version version, ushort statusCode, string statusDescription, HeaderCollection headers) : base(version, headers) 18 | { 19 | StatusCode = statusCode; 20 | StatusDescription = statusDescription; 21 | } 22 | } 23 | public sealed class ResponseHeadSerializer : HeadSerializer 24 | { 25 | public override bool Write(ResponseHead source) 26 | { 27 | string stringified = source.Version + WS + source.StatusCode + WS + source.StatusDescription + CRLF; 28 | for (int i = 0; i < source.Headers.Length; i++) 29 | stringified += source.Headers[i].Key.ToLower() + COLON + WS + source.Headers[i].Value + CRLF; 30 | stringified += CRLF; 31 | return HandleReadable(System.Text.Encoding.ASCII.GetBytes(stringified)); 32 | } 33 | } 34 | public sealed class ResponseHeadParser : HeadParser 35 | { 36 | private enum ParseState : byte 37 | { 38 | Version, 39 | StatusCode, 40 | StatusDescription, 41 | FirstLf, 42 | HeaderName, 43 | HeaderValue, 44 | HeaderLf, 45 | Lf 46 | } 47 | 48 | private ParseState State = ParseState.Version; 49 | public bool Malformed { get; private set; } = false; 50 | 51 | public ResponseHeadParser() => Reset(); 52 | 53 | private string IncomingVersion; 54 | private Version IncomingVersionValue; 55 | private string IncomingStatusCode; 56 | private string IncomingStatusDescription; 57 | private HeaderCollection IncomingHeaders; 58 | private string IncomingHeaderName; 59 | private string IncomingHeaderValue; 60 | 61 | public bool Reset() 62 | { 63 | lock (Sync) 64 | { 65 | Malformed = false; 66 | State = ParseState.Version; 67 | IncomingVersion = ""; 68 | IncomingVersionValue = default(Version); 69 | IncomingStatusCode = ""; 70 | IncomingStatusDescription = ""; 71 | IncomingHeaders = new HeaderCollection(); 72 | IncomingHeaderName = ""; 73 | IncomingHeaderValue = ""; 74 | return true; 75 | } 76 | } 77 | protected override bool HandleWritable(byte[] source) 78 | { 79 | if (Malformed) return false; 80 | ulong i = 0, sourceLength = (ulong)source.LongLength; 81 | for (char c; i < sourceLength; i++) 82 | { 83 | c = (char)source[i]; 84 | switch (State) 85 | { 86 | case ParseState.Version: 87 | if (c != WS) IncomingVersion += c; 88 | else 89 | { 90 | if (!Version.TryParse(IncomingVersion, out Version result)) 91 | return !(Malformed = true); 92 | IncomingVersionValue = result; 93 | State = ParseState.StatusCode; 94 | } 95 | break; 96 | case ParseState.StatusCode: 97 | if (c == WS) State = ParseState.StatusDescription; 98 | else if (char.IsDigit(c)) IncomingStatusCode += c; 99 | else return !(Malformed = true); 100 | break; 101 | case ParseState.StatusDescription: 102 | if (c != CR) IncomingStatusDescription += c; 103 | else State = ParseState.FirstLf; 104 | break; 105 | case ParseState.FirstLf: 106 | if (c != LF) return !(Malformed = true); 107 | State = ParseState.HeaderName; 108 | break; 109 | case ParseState.HeaderName: 110 | if (c == CR) State = ParseState.Lf; 111 | else if (c != COLON) IncomingHeaderName += c; 112 | else State = ParseState.HeaderValue; 113 | break; 114 | case ParseState.HeaderValue: 115 | if (c != CR) IncomingHeaderValue += c; 116 | else 117 | { 118 | IncomingHeaders.Add(IncomingHeaderName, IncomingHeaderValue.TrimStart()); 119 | IncomingHeaderName = ""; 120 | IncomingHeaderValue = ""; 121 | State = ParseState.HeaderLf; 122 | } 123 | break; 124 | case ParseState.HeaderLf: 125 | if (c != LF) return !(Malformed = true); 126 | else State = ParseState.HeaderName; 127 | break; 128 | case ParseState.Lf: 129 | if (c != LF) return !(Malformed = true); 130 | HandleReadable(PrimitiveBuffer.Slice(source, i + 1, sourceLength)); 131 | Pickup(new ResponseHead(IncomingVersionValue, ushort.Parse(IncomingStatusCode), IncomingStatusDescription, IncomingHeaders)); 132 | return Reset(); 133 | } 134 | } 135 | return true; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /CSSockets/Http/Reference/ServerConnection.cs: -------------------------------------------------------------------------------- 1 | using CSSockets.Tcp; 2 | using CSSockets.Streams; 3 | using CSSockets.Http.Definition; 4 | 5 | namespace CSSockets.Http.Reference 6 | { 7 | public delegate void IncomingRequestHandler(IncomingRequest request, OutgoingResponse response); 8 | public sealed class ServerConnection : Connection 9 | { 10 | private IncomingRequest _Incoming = null; 11 | private OutgoingResponse _Outgoing = null; 12 | public override IncomingMessage Incoming => _Incoming; 13 | public override OutgoingMessage Outgoing => _Outgoing; 14 | 15 | public IncomingRequestHandler RequestHandler { get; set; } 16 | 17 | public ServerConnection(Connection connection, IncomingRequestHandler requestHandler) : base(connection) 18 | { 19 | IncomingHead = new RequestHeadParser(); 20 | OutgoingHead = new ResponseHeadSerializer(); 21 | IncomingBody = new BodyParser(); 22 | OutgoingBody = new BodySerializer(); 23 | IncomingHead.OnFail += OnSegmentFail; 24 | IncomingBody.OnFail += OnSegmentFail; 25 | OutgoingHead.OnFail += OnSegmentFail; 26 | OutgoingBody.OnFail += OnSegmentFail; 27 | IncomingHead.Pipe(excess); 28 | IncomingBody.Excess.Pipe(excess); 29 | RequestHandler = requestHandler; 30 | WaitIncoming(); 31 | } 32 | 33 | private readonly MemoryDuplex excess = new MemoryDuplex(); 34 | private bool gotReqHead = false; 35 | private bool gotReqBody = false; 36 | private bool gotResHead = false; 37 | private bool hasResBody = false; 38 | 39 | private void WaitIncoming() 40 | { 41 | lock (Sync) 42 | { 43 | _Incoming = null; _Outgoing = null; 44 | gotReqHead = gotReqBody = gotResHead = false; 45 | IncomingHead.OnCollect += PushIncoming; 46 | excess.Burst(IncomingHead); 47 | if (!gotReqHead) Base.Pipe(IncomingHead); 48 | } 49 | } 50 | private void PushIncoming(RequestHead head) 51 | { 52 | lock (Sync) 53 | { 54 | gotReqHead = true; 55 | 56 | BodyType? type = BodyType.TryDetectFor(head, true); 57 | if (type == null) { Terminate(); return; } 58 | if (!IncomingBody.TrySetFor(type.Value)) { Terminate(); return; } 59 | 60 | IncomingRequest incoming = _Incoming = new IncomingRequest(this, head); 61 | OutgoingResponse outgoing = _Outgoing = new OutgoingResponse(this, head.Version); 62 | 63 | RequestHandler(incoming, outgoing); 64 | if (Ended || Frozen) return; 65 | 66 | if (!IncomingBody.Finished) 67 | { 68 | IncomingBody.Pipe(_Incoming); 69 | IncomingBody.OnFinish += FinishIncomingMessage; 70 | excess.Burst(IncomingBody); 71 | if (!gotReqBody) Base.Pipe(IncomingBody); 72 | } 73 | } 74 | } 75 | private void FinishIncomingMessage() 76 | { 77 | lock (Sync) 78 | { 79 | gotReqBody = true; 80 | IncomingBody.OnFinish -= FinishIncomingMessage; 81 | IncomingBody.Unpipe(); 82 | _Incoming.Finish(); 83 | Base.Unpipe(); 84 | } 85 | } 86 | public override bool StartOutgoing(ResponseHead head) 87 | { 88 | lock (Sync) 89 | { 90 | if (!gotReqHead) return false; 91 | 92 | BodyType? type = BodyType.TryDetectFor(head, true); 93 | if (type == null) return false; 94 | if (!OutgoingBody.TrySetFor(type.Value)) return false; 95 | 96 | OutgoingHead.Write(head); 97 | OutgoingHead.Burst(Base); 98 | 99 | if (!OutgoingBody.Finished) 100 | { 101 | hasResBody = true; 102 | OutgoingBody.OnFinish += FinishOutgoingMessage; 103 | OutgoingBody.Pipe(Base); 104 | _Outgoing.Pipe(OutgoingBody); 105 | } 106 | return gotResHead = true; 107 | } 108 | } 109 | public override bool FinishOutgoing() 110 | { 111 | lock (Sync) 112 | { 113 | if (!gotResHead) return false; 114 | OutgoingBody.OnFinish -= FinishOutgoingMessage; 115 | if (!OutgoingBody.Finished) OutgoingBody.Finish(); 116 | OutgoingBody.Unpipe(); 117 | _Outgoing.Finish(); 118 | _Incoming = null; 119 | _Outgoing = null; 120 | WaitIncoming(); 121 | return !(hasResBody = false); 122 | } 123 | } 124 | private void FinishOutgoingMessage() 125 | { 126 | if (!FinishOutgoing()) Terminate(); 127 | } 128 | private void OnSegmentFail() => Terminate(); 129 | public override byte[] Freeze() 130 | { 131 | lock (Sync) 132 | { 133 | _Incoming = null; _Outgoing = null; Frozen = true; 134 | gotReqHead = gotReqBody = gotResHead = hasResBody = false; 135 | Base.Unpipe(); 136 | IncomingHead.Unpipe(); 137 | IncomingHead.OnFail -= OnSegmentFail; 138 | if (!gotReqHead) IncomingHead.OnCollect -= PushIncoming; 139 | IncomingBody.Unpipe(); 140 | IncomingBody.Excess.Unpipe(); 141 | IncomingBody.OnFail -= OnSegmentFail; 142 | if (!gotReqBody) IncomingBody.OnFinish -= FinishIncomingMessage; 143 | OutgoingHead.Unpipe(); 144 | OutgoingHead.OnFail -= OnSegmentFail; 145 | OutgoingBody.Unpipe(); 146 | OutgoingBody.OnFail -= OnSegmentFail; 147 | if (!hasResBody) OutgoingBody.OnFinish -= FinishOutgoingMessage; 148 | return excess.Read(); 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /CSSockets/Streams/Base.cs: -------------------------------------------------------------------------------- 1 | namespace CSSockets.Streams 2 | { 3 | public delegate void ControlHandler(); 4 | public delegate void DataHandler(byte[] data); 5 | public delegate void OutputHandler(T item); 6 | 7 | public interface IEndable 8 | { 9 | bool Ended { get; } 10 | bool End(); 11 | } 12 | public interface IFinishable 13 | { 14 | bool Finished { get; } 15 | bool Finish(); 16 | } 17 | 18 | public interface IReadable 19 | { 20 | event DataHandler OnData; 21 | event ControlHandler OnFail; 22 | 23 | IWritable PipedTo { get; } 24 | bool Pipe(IWritable to); 25 | bool Burst(IWritable to); 26 | bool Unpipe(); 27 | 28 | ulong BufferedReadable { get; } 29 | ulong ReadCount { get; } 30 | 31 | ulong Read(byte[] destination, ulong start = 0); 32 | byte[] Read(ulong length); 33 | byte[] Read(); 34 | } 35 | public interface IWritable 36 | { 37 | ulong WriteCount { get; } 38 | 39 | bool Unpipe(IReadable from); 40 | bool Write(byte[] source); 41 | bool Write(byte[] source, ulong start); 42 | bool Write(byte[] source, ulong start, ulong end); 43 | } 44 | public interface IDuplex : IReadable, IWritable { } 45 | 46 | public interface ICollector 47 | { 48 | event OutputHandler OnCollect; 49 | } 50 | public interface ITransform 51 | { 52 | bool Write(T source); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /CSSockets/Streams/Compressors.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | 4 | namespace CSSockets.Streams 5 | { 6 | public class Compressor : Duplex, IFinishable where T : Stream 7 | { 8 | protected readonly SimpleStream Backstore = new SimpleStream(); 9 | protected T Transformer; 10 | public CompressionLevel CompressionLevel { get; } 11 | public bool Finished { get; private set; } = false; 12 | 13 | protected Compressor(CompressionLevel compressionLevel) => CompressionLevel = compressionLevel; 14 | 15 | protected bool PipeTransformer() 16 | { 17 | byte[] ret = new byte[Backstore.Length]; 18 | Backstore.Read(ret, 0, (int)Backstore.Length); 19 | return HandleReadable(ret); 20 | } 21 | protected override bool HandleWritable(byte[] source) 22 | { 23 | if (Finished) throw new System.ObjectDisposedException("Stream has ended", null as System.Exception); 24 | Transformer.Write(source, 0, source.Length); 25 | return PipeTransformer(); 26 | } 27 | 28 | public bool Finish() 29 | { 30 | lock (Sync) 31 | { 32 | if (Finished) return false; 33 | Transformer.Dispose(); 34 | PipeTransformer(); 35 | Backstore.Dispose(); 36 | return Finished = true; 37 | } 38 | } 39 | } 40 | public class DeflateCompressor : Compressor 41 | { 42 | public DeflateCompressor(CompressionLevel compressionLevel = CompressionLevel.Optimal) : base(compressionLevel) 43 | => Transformer = new DeflateStream(Backstore, compressionLevel); 44 | } 45 | public class GzipCompressor : Compressor 46 | { 47 | public GzipCompressor(CompressionLevel compressionLevel = CompressionLevel.Optimal) : base(compressionLevel) 48 | => Transformer = new GZipStream(Backstore, compressionLevel); 49 | } 50 | 51 | public class Decompressor : Duplex, IFinishable where T : Stream 52 | { 53 | public const int TRANSFER_SIZE_DEFAULT = 1024; 54 | 55 | protected readonly SimpleStream Backstore = new SimpleStream(); 56 | protected T Transformer; 57 | public int TransferSize { get; set; } 58 | public bool Finished { get; private set; } = false; 59 | 60 | protected Decompressor(int transferSize) => TransferSize = transferSize; 61 | 62 | protected bool PipeTransformer() 63 | { 64 | byte[] ret = new byte[TransferSize]; int len; 65 | while ((len = Transformer.Read(ret, 0, ret.Length)) > 0) 66 | if (!HandleReadable(PrimitiveBuffer.Slice(ret, 0, len))) return false; 67 | return true; 68 | } 69 | protected override bool HandleWritable(byte[] source) 70 | { 71 | if (Finished) throw new System.ObjectDisposedException("Stream has ended", null as System.Exception); 72 | Backstore.Write(source, 0, source.Length); 73 | return PipeTransformer(); 74 | } 75 | 76 | public bool Finish() 77 | { 78 | lock (Sync) 79 | { 80 | if (Finished) return false; 81 | Transformer.Dispose(); 82 | Backstore.Dispose(); 83 | return Finished = true; 84 | } 85 | } 86 | } 87 | public class DeflateDecompressor : Decompressor 88 | { 89 | public DeflateDecompressor(int transferSize = TRANSFER_SIZE_DEFAULT) : base(transferSize) => Transformer = new DeflateStream(Backstore, CompressionMode.Decompress); 90 | } 91 | public class GzipDecompressor : Decompressor 92 | { 93 | public GzipDecompressor(int transferSize = TRANSFER_SIZE_DEFAULT) : base(transferSize) => Transformer = new GZipStream(Backstore, CompressionMode.Decompress); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /CSSockets/Streams/Primitive.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CSSockets.Streams 4 | { 5 | public class PrimitiveBuffer 6 | { 7 | /// 8 | /// Capacity modifier multiplier 9 | /// 10 | public const ulong CMM = 256; 11 | 12 | public byte[] Buffer = new byte[CMM]; 13 | public ulong Length { get; private set; } = 0; 14 | public ulong Capacity { get; private set; } = CMM; 15 | 16 | private unsafe bool EnsureCapacity(ulong targetLen) 17 | { 18 | ulong beforeLen = Length; 19 | Length = targetLen; 20 | ulong targetCap = Capacity; 21 | if (targetCap > targetLen) while (targetCap / CMM >= CMM && targetCap / CMM > targetLen) targetCap /= CMM; 22 | else while (targetCap < targetLen) targetCap *= CMM; 23 | if (targetCap == Capacity) return false; 24 | byte[] newBuf = new byte[targetCap]; 25 | fixed (byte* src = Buffer, dst = newBuf) Copy(src, 0, dst, 0, Math.Min(beforeLen, targetLen)); 26 | Buffer = newBuf; 27 | Capacity = targetCap; 28 | return true; 29 | } 30 | public unsafe byte[] Read(ulong length) => Read(new byte[length]); 31 | public unsafe byte[] Read(byte[] ret) => Read(ret, (ulong)ret.LongLength); 32 | public unsafe byte[] Read(byte[] ret, ulong length, ulong start = 0) 33 | { 34 | if (ret == null) throw new ArgumentNullException(nameof(ret)); 35 | if (length > Length) throw new ArgumentOutOfRangeException(nameof(length)); 36 | fixed (byte* src = Buffer, dst = ret) 37 | { 38 | Copy(src, 0, dst + start, 0, length); 39 | ulong remaining = Length - length; 40 | Copy(src, length, src, 0, remaining); 41 | EnsureCapacity(remaining); 42 | } 43 | return ret; 44 | } 45 | public unsafe bool Write(byte[] data) 46 | { 47 | ulong len = (ulong)data.LongLength; 48 | ulong start = Length; 49 | ulong end = Length + len; 50 | EnsureCapacity(end); 51 | fixed (byte* src = data, dst = Buffer) 52 | Copy(src, 0, dst, start, len); 53 | return true; 54 | } 55 | 56 | private static unsafe void MEMCPY(void* dest, void* src, ulong count) 57 | => System.Buffer.MemoryCopy(src, dest, count, count); 58 | private static unsafe void MEMCPY(void* dest, void* src, int count) 59 | => System.Buffer.MemoryCopy(src, dest, count, count); 60 | 61 | public static unsafe byte[] Slice(byte[] source, ulong start, ulong end) 62 | { 63 | byte[] srcChunked = new byte[end - start]; 64 | fixed (byte* src = source, dst = srcChunked) 65 | MEMCPY(dst, src + start, end - start); 66 | return srcChunked; 67 | } 68 | public static unsafe byte[] Slice(byte[] source, int start, int end) 69 | { 70 | byte[] srcChunked = new byte[end - start]; 71 | fixed (byte* srcp = source, dstp = srcChunked) 72 | MEMCPY(dstp, srcp + start, end - start); 73 | return srcChunked; 74 | } 75 | public static unsafe void Copy(byte* src, ulong srcStart, byte* dst, ulong dstStart, ulong length) 76 | => MEMCPY(dst + dstStart, src + srcStart, length); 77 | public static unsafe void Copy(byte[] src, ulong srcStart, byte[] dst, ulong dstStart, ulong length) 78 | { 79 | fixed (byte* srcp = src, dstp = dst) MEMCPY(dstp + dstStart, srcp + srcStart, length); 80 | } 81 | public static unsafe void Copy(byte[] src, int srcStart, byte[] dst, int dstStart, int length) 82 | { 83 | fixed (byte* srcp = src, dstp = dst) MEMCPY(dstp + dstStart, srcp + srcStart, length); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /CSSockets/Streams/Wrappers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace CSSockets.Streams 5 | { 6 | public sealed class SimpleStream : Stream 7 | { 8 | private readonly PrimitiveBuffer Buffer = new PrimitiveBuffer(); 9 | private readonly object Sync = new object(); 10 | 11 | public override bool CanRead => true; 12 | public override bool CanSeek => false; 13 | public override bool CanWrite => true; 14 | public override long Length => (long)Buffer.Length; 15 | public override long Position { get => 0; set => throw new InvalidOperationException("Cannot seek"); } 16 | 17 | public override void Flush() { } 18 | public override int Read(byte[] buffer, int offset, int count) 19 | { 20 | lock (Sync) 21 | { 22 | int reading = Math.Min((int)Length, count); 23 | Buffer.Read(buffer, (ulong)reading, (uint)offset); 24 | return reading; 25 | } 26 | } 27 | public override long Seek(long offset, SeekOrigin origin) 28 | => throw new InvalidOperationException("Cannot seek"); 29 | public override void SetLength(long value) 30 | => throw new InvalidOperationException("Cannot set length"); 31 | public override void Write(byte[] buffer, int offset, int count) 32 | { 33 | lock (Sync) Buffer.Write(PrimitiveBuffer.Slice(buffer, offset, offset + count)); 34 | } 35 | } 36 | public abstract class Readable : IReadable 37 | { 38 | protected readonly PrimitiveBuffer Buffer = new PrimitiveBuffer(); 39 | protected readonly object Sync = new object(); 40 | 41 | public IWritable PipedTo { get; private set; } = null; 42 | public ulong ReadCount { get; private set; } = 0; 43 | public ulong BufferedReadable => Buffer.Length; 44 | 45 | protected event DataHandler _OnData; 46 | public virtual event DataHandler OnData 47 | { 48 | add 49 | { 50 | lock (Sync) 51 | { 52 | _OnData += value; 53 | if (BufferedReadable > 0) HandleReadable(Read()); 54 | } 55 | } 56 | remove 57 | { 58 | lock (Sync) _OnData -= value; 59 | } 60 | } 61 | public virtual event ControlHandler OnFail; 62 | 63 | public virtual bool Pipe(IWritable to) 64 | { 65 | lock (Sync) 66 | { 67 | if (to == null) return false; 68 | PipedTo = to; 69 | if (BufferedReadable > 0) HandleReadable(Read()); 70 | return true; 71 | } 72 | } 73 | public virtual bool Burst(IWritable to) 74 | { 75 | lock (Sync) 76 | { 77 | if (to == null) return false; 78 | if (BufferedReadable > 0) return to.Write(Read()); 79 | return true; 80 | } 81 | } 82 | public virtual bool Unpipe() 83 | { 84 | lock (Sync) 85 | { 86 | PipedTo = null; 87 | return true; 88 | } 89 | } 90 | 91 | protected bool HandleReadable(byte[] source) 92 | { 93 | lock (Sync) 94 | { 95 | if (PipedTo != null) 96 | { 97 | if (!PipedTo.Write(source)) OnFail?.Invoke(); 98 | return true; 99 | } 100 | if (_OnData != null) { _OnData(source); return true; } 101 | return Buffer.Write(source); 102 | } 103 | } 104 | public virtual ulong Read(byte[] destination, ulong start = 0) 105 | { 106 | lock (Sync) 107 | { 108 | ulong length = BufferedReadable; 109 | byte[] data = Buffer.Read(length); 110 | PrimitiveBuffer.Copy(data, 0, destination, start, length); 111 | ReadCount += length; 112 | return length; 113 | } 114 | } 115 | public virtual byte[] Read(ulong length) 116 | { 117 | lock (Sync) 118 | { 119 | ulong retLength = Math.Min(BufferedReadable, length); 120 | if (retLength != length) return null; 121 | ReadCount += retLength; 122 | return Buffer.Read(length); 123 | } 124 | } 125 | public virtual byte[] Read() 126 | { 127 | lock (Sync) 128 | { 129 | ReadCount += BufferedReadable; 130 | return Buffer.Read(BufferedReadable); 131 | } 132 | } 133 | } 134 | 135 | public abstract class Writable : IWritable 136 | { 137 | protected readonly object Sync = new object(); 138 | 139 | public ulong WriteCount { get; private set; } = 0; 140 | 141 | public virtual bool Unpipe(IReadable from) 142 | { 143 | lock (Sync) return from.PipedTo == this ? from.Unpipe() : false; 144 | } 145 | 146 | protected abstract bool HandleWritable(byte[] source); 147 | public virtual bool Write(byte[] source) 148 | { 149 | lock (Sync) return HandleWritable(source); 150 | } 151 | public virtual bool Write(byte[] source, ulong start) 152 | => Write(PrimitiveBuffer.Slice(source, start, (ulong)source.LongLength)); 153 | public virtual bool Write(byte[] source, ulong start, ulong end) 154 | => Write(PrimitiveBuffer.Slice(source, start, end)); 155 | } 156 | 157 | public abstract class Duplex : IDuplex 158 | { 159 | protected readonly PrimitiveBuffer Readable = new PrimitiveBuffer(); 160 | protected readonly object Sync = new object(); 161 | 162 | public IWritable PipedTo { get; private set; } = null; 163 | public ulong ReadCount { get; private set; } = 0; 164 | public ulong WriteCount { get; private set; } = 0; 165 | public ulong BufferedReadable => Readable.Length; 166 | 167 | protected event DataHandler _OnData; 168 | public virtual event DataHandler OnData 169 | { 170 | add 171 | { 172 | lock (Sync) 173 | { 174 | _OnData += value; 175 | if (BufferedReadable > 0) HandleReadable(Read()); 176 | } 177 | } 178 | remove 179 | { 180 | lock (Sync) _OnData -= value; 181 | } 182 | } 183 | public virtual event ControlHandler OnFail; 184 | 185 | public virtual bool Pipe(IWritable to) 186 | { 187 | lock (Sync) 188 | { 189 | if (to == null) return false; 190 | PipedTo = to; 191 | if (BufferedReadable > 0) HandleReadable(Read()); 192 | return true; 193 | } 194 | } 195 | public virtual bool Burst(IWritable to) 196 | { 197 | lock (Sync) 198 | { 199 | if (to == null) return false; 200 | if (BufferedReadable > 0) return to.Write(Read()); 201 | return true; 202 | } 203 | } 204 | public virtual bool Unpipe() 205 | { 206 | lock (Sync) 207 | { 208 | PipedTo = null; 209 | return true; 210 | } 211 | } 212 | public virtual bool Unpipe(IReadable from) 213 | { 214 | lock (Sync) return from.PipedTo == this ? from.Unpipe() : false; 215 | } 216 | 217 | public virtual ulong Read(byte[] destination, ulong start = 0) 218 | { 219 | lock (Sync) 220 | { 221 | ulong length = Math.Min(BufferedReadable, (ulong)destination.LongLength); 222 | byte[] data = Readable.Read(length); 223 | PrimitiveBuffer.Copy(data, 0, destination, start, length); 224 | ReadCount += length; 225 | return length; 226 | } 227 | } 228 | public virtual byte[] Read(ulong length) 229 | { 230 | lock (Sync) 231 | { 232 | ulong retLength = Math.Min(BufferedReadable, length); 233 | if (retLength != length) return null; 234 | ReadCount += retLength; 235 | return Readable.Read(length); 236 | } 237 | } 238 | public virtual byte[] Read() 239 | { 240 | lock (Sync) 241 | { 242 | ReadCount += BufferedReadable; 243 | return Readable.Read(BufferedReadable); 244 | } 245 | } 246 | 247 | protected bool HandleReadable(byte[] source) 248 | { 249 | lock (Sync) 250 | { 251 | if (PipedTo != null) 252 | { 253 | if (!PipedTo.Write(source)) OnFail?.Invoke(); 254 | return true; 255 | } 256 | if (_OnData != null) { _OnData(source); return true; } 257 | return Readable.Write(source); 258 | } 259 | } 260 | protected abstract bool HandleWritable(byte[] source); 261 | public virtual bool Write(byte[] source) 262 | { 263 | lock (Sync) return HandleWritable(source); 264 | } 265 | public virtual bool Write(byte[] source, ulong start) 266 | => Write(PrimitiveBuffer.Slice(source, start, (ulong)source.LongLength)); 267 | public virtual bool Write(byte[] source, ulong start, ulong end) 268 | => Write(PrimitiveBuffer.Slice(source, start, end)); 269 | } 270 | 271 | public abstract class Transform : Readable, ITransform 272 | { 273 | public abstract bool Write(T source); 274 | } 275 | public abstract class Collector : Writable, ICollector 276 | { 277 | public event OutputHandler OnCollect; 278 | 279 | protected bool Pickup(T item) 280 | { 281 | if (OnCollect != null) { OnCollect(item); return true; } 282 | return false; 283 | } 284 | } 285 | public abstract class Translator : Duplex, ICollector 286 | { 287 | public event OutputHandler OnCollect; 288 | 289 | protected bool Pickup(T item) 290 | { 291 | if (OnCollect != null) { OnCollect(item); return true; } 292 | return false; 293 | } 294 | } 295 | 296 | public class MemoryDuplex : Duplex 297 | { 298 | protected override bool HandleWritable(byte[] source) => HandleReadable(source); 299 | } 300 | public class VoidWritable : Writable 301 | { 302 | public static readonly VoidWritable Default = new VoidWritable(); 303 | protected override bool HandleWritable(byte[] source) => true; 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /CSSockets/Tcp/Connection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using CSSockets.Streams; 4 | using System.Net.Sockets; 5 | 6 | namespace CSSockets.Tcp 7 | { 8 | public enum TcpSocketState : byte 9 | { 10 | Dormant, 11 | Connecting, 12 | Open, 13 | Closing, 14 | Closed 15 | } 16 | public sealed class Connection : Duplex, IEndable 17 | { 18 | private static readonly TcpSocketState[] STATE_TRANSLATE = new TcpSocketState[] 19 | { 20 | TcpSocketState.Dormant, 21 | TcpSocketState.Dormant, 22 | TcpSocketState.Closed, 23 | TcpSocketState.Closed, 24 | TcpSocketState.Closed, 25 | TcpSocketState.Dormant, 26 | TcpSocketState.Connecting, 27 | TcpSocketState.Open, 28 | TcpSocketState.Open, 29 | TcpSocketState.Open, 30 | TcpSocketState.Closing, 31 | TcpSocketState.Closed 32 | }; 33 | 34 | public SocketWrapper Base { get; private set; } 35 | 36 | public event ControlHandler OnOpen; 37 | public event ControlHandler OnTimeout; 38 | public event ControlHandler OnDrain; 39 | public event ControlHandler OnEnd; 40 | public event ControlHandler OnClose; 41 | public event SocketCodeHandler OnError; 42 | 43 | public bool AllowHalfOpen 44 | { 45 | get => Base.ClientAllowHalfOpen; 46 | set => Base.ClientAllowHalfOpen = value; 47 | } 48 | public bool Ended => Base.State == WrapperState.Destroyed; 49 | public ulong BufferedWritable => Writable.Length; 50 | public TcpSocketState State => STATE_TRANSLATE[(int)Base.State]; 51 | public TimeSpan? TimeoutAfter 52 | { 53 | get => Base.ClientTimeoutAfter; 54 | set => Base.ClientTimeoutAfter = value; 55 | } 56 | public IPAddress LocalAddress => Base.Local?.Address; 57 | public IPAddress RemoteAddress => Base.Remote?.Address; 58 | 59 | internal Connection(SocketWrapper wrapper) 60 | { 61 | Base = wrapper; 62 | Base.ClientOnConnect = _OnOpen; 63 | Base.ClientOnTimeout = _OnTimeout; 64 | Base.ClientOnShutdown = _OnRemoteShutdown; 65 | Base.ClientOnClose = _OnClose; 66 | Base.WrapperOnSocketError = _OnError; 67 | } 68 | public Connection() : this(new SocketWrapper()) 69 | { 70 | Base.WrapperBind(); 71 | Base.WrapperAddClient(this); 72 | } 73 | 74 | internal readonly PrimitiveBuffer Writable = new PrimitiveBuffer(); 75 | private void _OnOpen() => OnOpen?.Invoke(); 76 | private void _OnTimeout() 77 | { 78 | lock (Sync) 79 | { 80 | if (OnTimeout != null) OnTimeout(); 81 | else if (!End()) Terminate(); 82 | } 83 | } 84 | private void _OnClose() => OnClose?.Invoke(); 85 | private void _OnRemoteShutdown() => OnEnd?.Invoke(); 86 | private void _OnError(SocketError error) 87 | { 88 | lock (Sync) 89 | { 90 | OnError?.Invoke(error); 91 | Terminate(); 92 | } 93 | } 94 | 95 | internal bool _ReadableWrite(byte[] source) => HandleReadable(source); 96 | internal byte[] _WritableRead(ulong maximum) 97 | { 98 | lock (Sync) 99 | { 100 | ulong length = Math.Min(maximum, BufferedWritable); 101 | if (length == BufferedWritable) OnDrain?.Invoke(); 102 | return Writable.Read(length); 103 | } 104 | } 105 | 106 | public bool CanRead => Base.State == WrapperState.ClientOpen || Base.State == WrapperState.ClientReadonly; 107 | public override byte[] Read() => Readable.Read(BufferedReadable); 108 | public override byte[] Read(ulong length) => Readable.Read(length); 109 | 110 | public bool CanWrite => Base.State == WrapperState.ClientOpen || Base.State == WrapperState.ClientWriteonly; 111 | protected override bool HandleWritable(byte[] data) => Writable.Write(data); 112 | public override bool Write(byte[] source) 113 | { 114 | lock (Sync) 115 | { 116 | if (!CanWrite) return false; 117 | return base.Write(source); 118 | } 119 | } 120 | 121 | public bool End() 122 | { 123 | lock (Sync) 124 | { 125 | switch (Base.State) 126 | { 127 | case WrapperState.ClientOpen: 128 | case WrapperState.ClientWriteonly: 129 | Base.ClientShutdown(); 130 | break; 131 | case WrapperState.ClientDormant: 132 | case WrapperState.ClientConnecting: 133 | Base.ClientTerminate(); 134 | break; 135 | default: return false; 136 | } 137 | return true; 138 | } 139 | } 140 | public bool Terminate() 141 | { 142 | lock (Sync) 143 | { 144 | if (State == TcpSocketState.Closed) return false; 145 | Base.ClientTerminate(); 146 | return true; 147 | } 148 | } 149 | 150 | public bool Connect(EndPoint endPoint) 151 | { 152 | lock (Sync) 153 | { 154 | if (Base.State > WrapperState.ClientDormant) return false; 155 | Base.ClientConnect(endPoint); 156 | return true; 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /CSSockets/Tcp/IOControl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Net.Sockets; 5 | using System.Collections.Generic; 6 | using System.Collections.Concurrent; 7 | 8 | namespace CSSockets.Tcp 9 | { 10 | public static class IOControl 11 | { 12 | public static int OperationsPerThread = 2147483647; 13 | public static int SocketsPerThread = 200; 14 | public static int PollTimeMillisecs = 1; 15 | 16 | private static readonly List threads = new List(); 17 | private static readonly object m_lock = new object(); 18 | 19 | public static int ThreadCount 20 | { 21 | get 22 | { 23 | lock (m_lock) return threads.Count; 24 | } 25 | } 26 | public static int SocketCount 27 | { 28 | get 29 | { 30 | lock (m_lock) 31 | { 32 | int count = 0; 33 | for (int i = 0; i < threads.Count; i++) count += threads[i].SocketCount; 34 | return count; 35 | } 36 | } 37 | } 38 | public static int ConnectionCount 39 | { 40 | get 41 | { 42 | lock (m_lock) 43 | { 44 | int count = 0; 45 | for (int i = 0; i < threads.Count; i++) count += threads[i].ClientCount; 46 | return count; 47 | } 48 | } 49 | } 50 | public static int ListenerCount 51 | { 52 | get 53 | { 54 | lock (m_lock) 55 | { 56 | int count = 0; 57 | for (int i = 0; i < threads.Count; i++) count += threads[i].ServerCount; 58 | return count; 59 | } 60 | } 61 | } 62 | 63 | internal static IOThread GetBest() 64 | { 65 | lock (m_lock) 66 | { 67 | IOThread best = null; 68 | for (int i = 0; i < threads.Count; i++) 69 | { 70 | if (threads[i].Closing) continue; 71 | if (threads[i].OperationCount >= OperationsPerThread) continue; 72 | if (threads[i].SocketCount >= SocketsPerThread) continue; 73 | if (best == null || best.SocketCount < threads[i].SocketCount) 74 | best = threads[i]; 75 | } 76 | return best ?? Create(); 77 | } 78 | } 79 | 80 | internal static IOThread Create() 81 | { 82 | IOThread thread = new IOThread(); 83 | threads.Add(thread); 84 | return thread; 85 | } 86 | internal static bool Remove(IOThread thread) 87 | { 88 | lock (m_lock) return threads.Remove(thread); 89 | } 90 | } 91 | internal class IOThread 92 | { 93 | public volatile int OperationCount = 0; 94 | public volatile int SocketCount = 0; 95 | public volatile int ServerCount = 0; 96 | public volatile int ClientCount = 0; 97 | public volatile bool Closing = false; 98 | public volatile bool GotFirst = false; 99 | public Thread Thread = null; 100 | 101 | private List WrapperList = new List(); 102 | private Dictionary NativeLookup = new Dictionary(); 103 | private Dictionary ClientLookup = new Dictionary(); 104 | private Dictionary ServerLookup = new Dictionary(); 105 | 106 | private ConcurrentQueue OperationQueue = new ConcurrentQueue(); 107 | public bool Enqueue(IOOperation op) 108 | { 109 | OperationCount++; 110 | OperationQueue.Enqueue(op); 111 | return true; 112 | } 113 | 114 | private IPEndPoint Resolve(EndPoint endPoint) 115 | { 116 | if (endPoint is IPEndPoint) return endPoint as IPEndPoint; 117 | else if (endPoint is DnsEndPoint) 118 | { 119 | IPAddress[] addresses = Dns.GetHostAddresses((endPoint as DnsEndPoint).Host); 120 | if (addresses.Length == 0) return null; 121 | return new IPEndPoint(addresses[0].IsIPv4MappedToIPv6 ? addresses[0].MapToIPv4() : addresses[0], (endPoint as DnsEndPoint).Port); 122 | } 123 | return null; 124 | } 125 | private bool Bind(SocketWrapper wrapper) 126 | { 127 | SocketCount++; 128 | WrapperList.Add(wrapper); 129 | NativeLookup.Add(wrapper.Socket, wrapper); 130 | return GotFirst = true; 131 | } 132 | private bool Bind(Connection connection, SocketWrapper wrapper) 133 | { 134 | ClientCount++; 135 | ClientLookup.Add(wrapper, connection); 136 | return true; 137 | } 138 | private bool Bind(Listener listener, SocketWrapper wrapper) 139 | { 140 | ServerCount++; 141 | ServerLookup.Add(wrapper, listener); 142 | return true; 143 | } 144 | private bool Unbind(SocketWrapper wrapper, SocketError? code = null) 145 | { 146 | if (code != null) wrapper.WrapperOnSocketError?.Invoke(code.Value); 147 | if (!WrapperList.Remove(wrapper)) return false; 148 | NativeLookup.Remove(wrapper.Socket); 149 | wrapper.Local = wrapper.Remote = null; 150 | SocketCount--; 151 | if (ClientLookup.Remove(wrapper, out Connection connection)) 152 | { 153 | ClientCount--; 154 | wrapper.EndedReadable = true; 155 | wrapper.EndedWritable = true; 156 | wrapper.ClientOnClose?.Invoke(); 157 | } 158 | wrapper.State = WrapperState.Destroyed; 159 | if (ServerLookup.Remove(wrapper)) ServerCount--; 160 | return false; 161 | } 162 | 163 | public IOThread() => (Thread = new Thread(Loop) { Name = "TCP I/O thread" }).Start(); 164 | 165 | private void Loop() 166 | { 167 | List pollR = new List(), pollW = new List(), pollE = new List(); 168 | while (true) 169 | { 170 | while (OperationQueue.TryDequeue(out IOOperation operation)) 171 | ExecuteOperation(operation); 172 | 173 | pollR.Clear(); pollW.Clear(); pollE.Clear(); 174 | 175 | for (int i = 0; i < SocketCount; i++) 176 | { 177 | SocketWrapper wrapper = WrapperList[i]; 178 | Socket socket = wrapper.Socket; 179 | switch (wrapper.State) 180 | { 181 | case WrapperState.ClientConnecting: 182 | case WrapperState.ClientOpen: 183 | case WrapperState.ClientReadonly: 184 | case WrapperState.ClientWriteonly: 185 | ClientCheckTimeout(wrapper); 186 | break; 187 | } 188 | switch (wrapper.State) 189 | { 190 | case WrapperState.ServerListening: 191 | pollR.Add(socket); 192 | break; 193 | case WrapperState.ClientConnecting: 194 | pollW.Add(socket); 195 | pollE.Add(socket); 196 | break; 197 | case WrapperState.ClientOpen: 198 | pollR.Add(socket); 199 | if (ClientLookup[wrapper].BufferedWritable > 0) 200 | pollW.Add(socket); 201 | break; 202 | case WrapperState.ClientReadonly: 203 | pollR.Add(socket); 204 | if (!wrapper.EndedWritable && ClientLookup[wrapper].BufferedWritable > 0) 205 | pollW.Add(socket); 206 | else if (!wrapper.EndedWritable) ClientShutdown(wrapper, false, true); 207 | break; 208 | case WrapperState.ClientWriteonly: 209 | if (ClientLookup[wrapper].BufferedWritable > 0) 210 | pollW.Add(socket); 211 | break; 212 | case WrapperState.ClientLastWrite: 213 | if (!wrapper.EndedWritable && ClientLookup[wrapper].BufferedWritable > 0) 214 | pollW.Add(socket); 215 | else { ClientShutdown(wrapper, false, true); i--; } 216 | break; 217 | } 218 | } 219 | 220 | if (SocketCount == 0 && GotFirst) break; 221 | 222 | if (pollR.Count == 0 && pollW.Count == 0 && pollE.Count == 0) 223 | { 224 | Thread.Sleep(IOControl.PollTimeMillisecs); 225 | continue; 226 | } 227 | else Socket.Select(pollR, pollW, pollE, 1000 * IOControl.PollTimeMillisecs); 228 | 229 | for (int i = 0; i < pollR.Count; i++) 230 | { 231 | Socket socket = pollR[i]; 232 | SocketWrapper wrapper = NativeLookup[socket]; 233 | switch (wrapper.State) 234 | { 235 | case WrapperState.ServerListening: 236 | ServerAccept(wrapper); 237 | break; 238 | case WrapperState.ClientOpen: 239 | case WrapperState.ClientReadonly: 240 | ClientReceive(wrapper); 241 | break; 242 | } 243 | } 244 | 245 | for (int i = 0; i < pollW.Count; i++) 246 | { 247 | Socket socket = pollW[i]; 248 | SocketWrapper wrapper = NativeLookup[socket]; 249 | switch (wrapper.State) 250 | { 251 | case WrapperState.ClientConnecting: 252 | ClientOpen(wrapper); 253 | break; 254 | case WrapperState.ClientOpen: 255 | case WrapperState.ClientWriteonly: 256 | case WrapperState.ClientLastWrite: 257 | ClientSend(wrapper); 258 | break; 259 | } 260 | } 261 | 262 | for (int i = 0; i < pollE.Count; i++) 263 | { 264 | Socket socket = pollE[i]; 265 | SocketWrapper wrapper = NativeLookup[socket]; 266 | switch (wrapper.State) 267 | { 268 | case WrapperState.ClientConnecting: 269 | Unbind(wrapper, (SocketError)socket.GetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Error)); 270 | break; 271 | } 272 | } 273 | } 274 | Closing = true; 275 | IOControl.Remove(this); 276 | } 277 | 278 | private bool SucceedOperation(SocketWrapper wrapper, WrapperState? state = null) 279 | { 280 | if (state != null) wrapper.State = state.Value; 281 | return true; 282 | } 283 | private bool FailOperation(SocketWrapper wrapper, WrapperState state) 284 | { 285 | wrapper.State = state; 286 | return false; 287 | } 288 | private bool ExecuteOperation(IOOperation operation) 289 | { 290 | OperationCount--; 291 | SocketWrapper wrapper = operation.Callee; 292 | Socket socket = wrapper.Socket; 293 | IPEndPoint endPoint; 294 | SocketError code = SocketError.Success; 295 | switch (operation.Type) 296 | { 297 | case OperationType.Noop: 298 | return SucceedOperation(wrapper); 299 | 300 | case OperationType.WrapperBind: 301 | if (operation.AdvanceFrom != WrapperState.Unset) return Unbind(wrapper); 302 | return Bind(wrapper) 303 | ? SucceedOperation(wrapper, WrapperState.Dormant) 304 | : FailOperation(wrapper, WrapperState.Unset); 305 | 306 | case OperationType.WrapperAddServer: 307 | return Bind(operation.Listener, wrapper) 308 | ? SucceedOperation(wrapper, WrapperState.ServerDormant) 309 | : FailOperation(wrapper, WrapperState.Dormant); 310 | 311 | case OperationType.WrapperAddClient: 312 | if (operation.AdvanceFrom != WrapperState.Dormant) return Unbind(wrapper); 313 | return Bind(operation.Connection, wrapper) 314 | ? SucceedOperation(wrapper, WrapperState.ClientDormant) 315 | : FailOperation(wrapper, WrapperState.Dormant); 316 | 317 | case OperationType.ServerLookup: 318 | if (operation.AdvanceFrom != WrapperState.ServerDormant) return Unbind(wrapper); 319 | wrapper.Local = endPoint = Resolve(operation.Lookup); 320 | if (endPoint == null) return FailOperation(wrapper, WrapperState.ServerDormant); 321 | return SucceedOperation(wrapper, WrapperState.ServerBound); 322 | 323 | case OperationType.ServerListen: 324 | if (operation.AdvanceFrom == WrapperState.ServerDormant) return FailOperation(wrapper, WrapperState.ServerDormant); 325 | else if (operation.AdvanceFrom != WrapperState.ServerBound) return Unbind(wrapper); 326 | try 327 | { 328 | socket.ExclusiveAddressUse = wrapper.ServerExclusive; 329 | socket.Bind(wrapper.Local); 330 | socket.Listen(wrapper.ServerBacklog); 331 | } 332 | catch (SocketException ex) { code = ex.SocketErrorCode; } 333 | return code != SocketError.Success 334 | ? Unbind(wrapper, code) 335 | : SucceedOperation(wrapper, WrapperState.ServerListening); 336 | 337 | case OperationType.ServerTerminate: 338 | return Unbind(wrapper); 339 | 340 | case OperationType.ClientOpen: 341 | if (operation.AdvanceFrom != WrapperState.ClientDormant) return Unbind(wrapper); 342 | ClientOpen(wrapper, operation.Referer, operation.Connection); 343 | return true; 344 | 345 | case OperationType.ClientConnect: 346 | if (operation.AdvanceFrom != WrapperState.ClientDormant) return Unbind(wrapper); 347 | wrapper.Remote = endPoint = Resolve(operation.Lookup); 348 | if (endPoint == null) return Unbind(wrapper, SocketError.NetworkUnreachable); 349 | try { socket.Connect(endPoint); } 350 | catch (SocketException ex) { code = ex.SocketErrorCode; } 351 | return (code != SocketError.Success && code != SocketError.WouldBlock) 352 | ? Unbind(wrapper, code) 353 | : SucceedOperation(wrapper, WrapperState.ClientConnecting) && ClientExtendTimeout(wrapper); 354 | 355 | case OperationType.ClientShutdown: 356 | return ClientShutdown(wrapper, false, true); 357 | 358 | case OperationType.ClientTerminate: 359 | return Unbind(wrapper); 360 | 361 | default: throw new Exception(); 362 | } 363 | } 364 | 365 | private bool ServerAccept(SocketWrapper wrapper) 366 | { 367 | do 368 | { 369 | SocketWrapper newWrapper = new SocketWrapper(wrapper.Socket.Accept()); 370 | Connection connection = new Connection(newWrapper); 371 | newWrapper.WrapperBind(); 372 | newWrapper.WrapperAddClient(connection); 373 | newWrapper.ClientOpen(wrapper, connection); 374 | } 375 | while (wrapper.Socket.Poll(0, SelectMode.SelectRead)); 376 | return true; 377 | } 378 | 379 | private bool ClientExtendTimeout(SocketWrapper wrapper) 380 | { 381 | wrapper.ClientLastActivity = DateTime.UtcNow; 382 | wrapper.ClientCalledTimeout = false; 383 | return true; 384 | } 385 | private bool ClientCheckTimeout(SocketWrapper wrapper) 386 | { 387 | if (wrapper.ClientTimeoutAfter == null || wrapper.ClientCalledTimeout) return true; 388 | if (DateTime.UtcNow - wrapper.ClientLastActivity < wrapper.ClientTimeoutAfter) return true; 389 | wrapper.ClientOnTimeout?.Invoke(); 390 | wrapper.ClientCalledTimeout = true; 391 | return true; 392 | } 393 | private bool ClientOpen(SocketWrapper wrapper) 394 | { 395 | SucceedOperation(wrapper, WrapperState.ClientOpen); 396 | wrapper.Local = Resolve(wrapper.Socket.LocalEndPoint); 397 | wrapper.Remote = Resolve(wrapper.Socket.RemoteEndPoint); 398 | wrapper.ClientOnConnect?.Invoke(); 399 | return ClientExtendTimeout(wrapper); 400 | } 401 | private bool ClientOpen(SocketWrapper wrapper, SocketWrapper referer, Connection connection) 402 | { 403 | SucceedOperation(wrapper, WrapperState.ClientOpen); 404 | wrapper.Local = Resolve(wrapper.Socket.LocalEndPoint); 405 | wrapper.Remote = Resolve(wrapper.Socket.RemoteEndPoint); 406 | referer.ServerOnConnection?.Invoke(connection); 407 | return ClientExtendTimeout(wrapper); 408 | } 409 | private bool ClientReceive(SocketWrapper wrapper) 410 | { 411 | int available = wrapper.Socket.Available; 412 | if (available == 0) return ClientShutdown(wrapper, true, false); 413 | byte[] data = new byte[available]; 414 | wrapper.Socket.Receive(data, 0, available, SocketFlags.None, out SocketError code); 415 | if (code == SocketError.Success) 416 | return ClientLookup[wrapper]._ReadableWrite(data) && ClientExtendTimeout(wrapper); 417 | else return Unbind(wrapper, code); 418 | } 419 | private bool ClientSend(SocketWrapper wrapper) 420 | { 421 | byte[] data = ClientLookup[wrapper]._WritableRead(65536); 422 | wrapper.Socket.Send(data, 0, data.Length, SocketFlags.None, out SocketError code); 423 | if (code != SocketError.Success) return Unbind(wrapper, code); 424 | return ClientExtendTimeout(wrapper); 425 | } 426 | private SocketError ClientSocketShutdown(Socket socket, SocketShutdown how) 427 | { 428 | try { socket.Shutdown(how); } 429 | catch (SocketException ex) { return ex.SocketErrorCode; } 430 | return SocketError.Success; 431 | } 432 | private bool ClientShutdown(SocketWrapper wrapper, bool r, bool w) 433 | { 434 | SocketError code = SocketError.Success; 435 | bool halfOpen = wrapper.ClientAllowHalfOpen; 436 | switch (wrapper.State) 437 | { 438 | case WrapperState.ClientOpen: 439 | if (r && w) return Unbind(wrapper); 440 | if (r) return (wrapper.EndedReadable = true) && SucceedOperation(wrapper, halfOpen ? WrapperState.ClientWriteonly : WrapperState.ClientLastWrite); 441 | return SucceedOperation(wrapper, halfOpen ? WrapperState.ClientReadonly : WrapperState.ClientLastWrite); 442 | 443 | case WrapperState.ClientReadonly: 444 | if (w) 445 | { 446 | if (wrapper.EndedWritable) return Unbind(wrapper); 447 | ClientSocketShutdown(wrapper.Socket, SocketShutdown.Send); 448 | } 449 | code = ClientSocketShutdown(wrapper.Socket, SocketShutdown.Receive); 450 | if (code != SocketError.Success) return Unbind(wrapper, code); 451 | wrapper.ClientOnShutdown?.Invoke(); 452 | return (wrapper.EndedReadable = true) && SucceedOperation(wrapper, WrapperState.ClientLastWrite); 453 | 454 | case WrapperState.ClientWriteonly: 455 | if (r) return Unbind(wrapper); 456 | return SucceedOperation(wrapper, WrapperState.ClientLastWrite); 457 | 458 | case WrapperState.ClientLastWrite: 459 | if (!wrapper.EndedWritable) ClientSocketShutdown(wrapper.Socket, SocketShutdown.Send); 460 | return Unbind(wrapper); 461 | 462 | default: return Unbind(wrapper); 463 | } 464 | } 465 | } 466 | } 467 | -------------------------------------------------------------------------------- /CSSockets/Tcp/Listener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | 5 | namespace CSSockets.Tcp 6 | { 7 | public class Listener 8 | { 9 | private readonly object sync = new object(); 10 | private SocketWrapper _base = null; 11 | public SocketWrapper Base => (_base?.State == WrapperState.Destroyed ? Create() : _base) ?? Create(); 12 | 13 | private SocketWrapper Create() 14 | { 15 | IPEndPoint endPoint = _base?.Local; 16 | _base = new SocketWrapper(); 17 | _base.WrapperBind(); 18 | _base.WrapperAddServer(this); 19 | _base.ServerExclusive = Exclusive; 20 | _base.ServerBacklog = Backlog; 21 | _base.WrapperOnSocketError = _OnError; 22 | _base.ServerOnConnection = _OnConnection; 23 | if (endPoint != null) _base.ServerLookup(endPoint); 24 | return _base; 25 | } 26 | 27 | public bool Exclusive { get; set; } = SocketWrapper.SERVER_EXCLUSIVE; 28 | public int Backlog { get; set; } = SocketWrapper.SERVER_BACKLOG; 29 | public bool Bound => _base.State >= WrapperState.ServerBound; 30 | public bool Listening => _base.State == WrapperState.ServerListening; 31 | public EndPoint BindEndPoint 32 | { 33 | get => _base.Local; 34 | set => _base.ServerLookup(value ?? throw new ArgumentNullException(nameof(value))); 35 | } 36 | 37 | public event ConnectionHandler OnConnection; 38 | public event SocketCodeHandler OnError; 39 | 40 | public Listener() => Create(); 41 | public Listener(EndPoint endPoint) : this() => BindEndPoint = endPoint; 42 | 43 | private void _OnError(SocketError error) => OnError?.Invoke(error); 44 | private void _OnConnection(Connection newConnection) => OnConnection?.Invoke(newConnection); 45 | 46 | public void Start() 47 | { 48 | lock (sync) _base.ServerListen(); 49 | } 50 | public void Stop() 51 | { 52 | lock (sync) _base.ServerTerminate(); 53 | } 54 | public void Reset() 55 | { 56 | lock (sync) Create(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /CSSockets/Tcp/SocketWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using CSSockets.Streams; 4 | using System.Net.Sockets; 5 | 6 | namespace CSSockets.Tcp 7 | { 8 | public class SocketWrapper 9 | { 10 | public const bool SERVER_EXCLUSIVE = true; 11 | public const int SERVER_BACKLOG = 511; 12 | 13 | public Socket Socket { get; } 14 | internal IOThread BoundThread = null; 15 | public WrapperState State { get; internal set; } = WrapperState.Unset; 16 | public IPEndPoint Local { get; internal set; } = null; 17 | public IPEndPoint Remote { get; internal set; } = null; 18 | public bool EndedReadable { get; internal set; } = false; 19 | public bool EndedWritable { get; internal set; } = false; 20 | 21 | public SocketWrapper() : this(new Socket(SocketType.Stream, ProtocolType.Tcp)) { } 22 | public SocketWrapper(Socket socket) 23 | { 24 | Socket = socket; 25 | Socket.Blocking = false; 26 | Socket.NoDelay = true; 27 | } 28 | 29 | public void WrapperBind() 30 | => (BoundThread = IOControl.GetBest()).Enqueue(new IOOperation() 31 | { 32 | Callee = this, 33 | Type = OperationType.WrapperBind, 34 | }); 35 | public void WrapperAddClient(Connection connection) 36 | => BoundThread.Enqueue(new IOOperation() 37 | { 38 | Callee = this, 39 | Connection = connection, 40 | Type = OperationType.WrapperAddClient, 41 | }); 42 | public void WrapperAddServer(Listener listener) 43 | => BoundThread.Enqueue(new IOOperation() 44 | { 45 | Callee = this, 46 | Listener = listener, 47 | Type = OperationType.WrapperAddServer, 48 | }); 49 | 50 | public void ServerLookup(EndPoint endPoint) 51 | => BoundThread.Enqueue(new IOOperation() 52 | { 53 | Callee = this, 54 | Lookup = endPoint, 55 | Type = OperationType.ServerLookup, 56 | }); 57 | public void ServerListen() 58 | => BoundThread.Enqueue(new IOOperation() 59 | { 60 | Callee = this, 61 | Type = OperationType.ServerListen, 62 | }); 63 | public void ServerTerminate() 64 | => BoundThread.Enqueue(new IOOperation() 65 | { 66 | Callee = this, 67 | Type = OperationType.ServerTerminate, 68 | }); 69 | 70 | public void ClientConnect(EndPoint endPoint) 71 | => BoundThread.Enqueue(new IOOperation() 72 | { 73 | Callee = this, 74 | Lookup = endPoint, 75 | Type = OperationType.ClientConnect, 76 | }); 77 | public void ClientOpen(SocketWrapper referer, Connection connection) 78 | => BoundThread.Enqueue(new IOOperation() 79 | { 80 | Callee = this, 81 | Referer = referer, 82 | Connection = connection, 83 | Type = OperationType.ClientOpen, 84 | }); 85 | public void ClientShutdown() 86 | => BoundThread.Enqueue(new IOOperation() 87 | { 88 | Callee = this, 89 | Type = OperationType.ClientShutdown, 90 | }); 91 | public void ClientTerminate() 92 | => BoundThread.Enqueue(new IOOperation() 93 | { 94 | Callee = this, 95 | Type = OperationType.ClientTerminate, 96 | }); 97 | 98 | public SocketCodeHandler WrapperOnSocketError { get; set; } 99 | public ControlHandler WrapperOnUnbind { get; set; } 100 | 101 | public ConnectionHandler ServerOnConnection { get; set; } 102 | 103 | public ControlHandler ClientOnConnect { get; set; } 104 | public ControlHandler ClientOnTimeout { get; set; } 105 | public ControlHandler ClientOnShutdown { get; set; } 106 | public ControlHandler ClientOnClose { get; set; } 107 | 108 | public int ServerBacklog { get; set; } = SERVER_BACKLOG; 109 | public bool ServerExclusive { get; set; } = SERVER_EXCLUSIVE; 110 | 111 | public bool ClientAllowHalfOpen { get; set; } = false; 112 | public DateTime ClientLastActivity { get; internal set; } = DateTime.UtcNow; 113 | public TimeSpan? ClientTimeoutAfter { get; set; } = null; 114 | public bool ClientCalledTimeout { get; internal set; } = false; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /CSSockets/Tcp/Types.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Sockets; 3 | 4 | namespace CSSockets.Tcp 5 | { 6 | public delegate void SocketCodeHandler(SocketError error); 7 | public delegate void ConnectionHandler(Connection newConnection); 8 | 9 | public enum WrapperState : byte 10 | { 11 | Unset, // no owner 12 | Dormant, // no type 13 | ServerDormant, // server, no bind 14 | ServerBound, // server, has bind 15 | ServerListening, // server, listening 16 | ClientDormant, // client, no socket 17 | ClientConnecting, // client, has socket, is connecting 18 | ClientOpen, // client, has socket, can read/write 19 | ClientReadonly, // client, has socket, can only read 20 | ClientWriteonly, // client, has socket, can only write 21 | ClientLastWrite, // client, has socket, emptyin buffer 22 | Destroyed // nothing available 23 | } 24 | 25 | public enum OperationType : byte 26 | { 27 | Noop = 0, 28 | WrapperBind = 1, 29 | WrapperAddServer = 2, 30 | WrapperAddClient = 3, 31 | ServerLookup = 4, 32 | ServerListen = 5, 33 | ServerTerminate = 6, 34 | ClientOpen = 7, 35 | ClientConnect = 8, 36 | ClientShutdown = 9, 37 | ClientTerminate = 10 38 | } 39 | public struct IOOperation 40 | { 41 | public Socket Socket => Callee.Socket; 42 | public SocketWrapper Callee { get; set; } 43 | public SocketWrapper Referer { get; set; } 44 | public OperationType Type { get; set; } 45 | public EndPoint Lookup { get; set; } 46 | public Connection Connection { get; set; } 47 | public Listener Listener { get; set; } 48 | public WrapperState AdvanceFrom => Callee.State; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CSSockets/WebSockets/Definition/Connection.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using CSSockets.Tcp; 3 | using CSSockets.Streams; 4 | 5 | namespace CSSockets.WebSockets.Definition 6 | { 7 | public delegate void BinaryMessageHandler(byte[] data); 8 | public delegate void StringMessageHandler(string data); 9 | public delegate void CloseMessageHandler(ushort code, string reason); 10 | public abstract partial class Connection 11 | { 12 | public IMode Mode { get; } 13 | public Tcp.Connection Base { get; } 14 | public string Subprotocol { get; protected set; } 15 | public bool Opening => Base.State == TcpSocketState.Connecting; 16 | public bool Open => Base.State == TcpSocketState.Open; 17 | public bool Closing => SentClose; 18 | public bool Closed => RecvClose; 19 | 20 | public virtual event ControlHandler OnOpen; 21 | public virtual event BinaryMessageHandler OnBinary; 22 | public virtual event StringMessageHandler OnString; 23 | public virtual event BinaryMessageHandler OnPing; 24 | public virtual event BinaryMessageHandler OnPong; 25 | public virtual event CloseMessageHandler OnClose; 26 | 27 | public Connection(Tcp.Connection connection, IMode mode) 28 | { 29 | Mode = mode; 30 | Base = connection; 31 | Base.OnClose += FinishClose; 32 | Parser.OnCollect += OnParserCollect; 33 | Merger.OnCollect += OnMergerCollect; 34 | } 35 | public void SetSubprotocol(string subprotocol) 36 | { 37 | lock (Sync) Subprotocol = subprotocol; 38 | } 39 | public bool Initiate(byte[] trail) 40 | { 41 | lock (Sync) 42 | { 43 | if (Mode.FireOpen) OnOpen?.Invoke(); 44 | if (!Parser.Write(trail)) return false; 45 | return Base.Pipe(Parser); 46 | } 47 | } 48 | 49 | protected readonly FrameParser Parser = new FrameParser(); 50 | protected readonly FrameMerger Merger = new FrameMerger(); 51 | 52 | protected readonly object Sync = new object(); 53 | protected ushort CloseCode = 1006; 54 | protected string CloseReason = null; 55 | protected bool RecvClose = false; 56 | protected bool SentClose = false; 57 | protected bool StartClose(ushort code, string reason) 58 | { 59 | lock (Sync) 60 | { 61 | if (RecvClose || SentClose) return false; 62 | CloseCode = code; 63 | CloseReason = reason; 64 | return SentClose = true; 65 | } 66 | } 67 | protected void FinishClose() 68 | { 69 | lock (Sync) 70 | { 71 | RecvClose = true; 72 | OnClose?.Invoke(CloseCode = 1006, CloseReason = ""); 73 | Base.OnClose -= FinishClose; 74 | } 75 | } 76 | protected bool FinishClose(ushort code) 77 | { 78 | lock (Sync) 79 | { 80 | if (!SentClose || RecvClose) return false; 81 | if (CloseCode != code) return false; 82 | OnClose?.Invoke(CloseCode, CloseReason); 83 | Base.OnClose -= FinishClose; 84 | Base.End(); 85 | return RecvClose = true; 86 | } 87 | } 88 | protected bool FinishClose(ushort code, string reason) 89 | { 90 | lock (Sync) 91 | { 92 | if (RecvClose) return false; 93 | OnClose?.Invoke(CloseCode = code, CloseReason = reason); 94 | Base.OnClose -= FinishClose; 95 | SendClose(code); 96 | return Base.End() && (RecvClose = true); 97 | } 98 | } 99 | 100 | protected abstract void OnParserCollect(Frame frame); 101 | protected abstract void OnMergerCollect(Message message); 102 | protected bool HandleMessage(Message message) 103 | { 104 | lock (Sync) switch (message.Opcode) 105 | { 106 | case 1: OnString?.Invoke(Encoding.UTF8.GetString(message.Payload)); return true; 107 | case 2: OnBinary?.Invoke(message.Payload); return true; 108 | case 8: 109 | if (message.Length < 2) { FinishClose(); return true; } 110 | ushort code = (ushort)(message.Payload[0] * 256 + message.Payload[1]); 111 | string reason = Encoding.UTF8.GetString(PrimitiveBuffer.Slice(message.Payload, 2, message.Length)); 112 | if (RecvClose && message.Length > 2) return false; 113 | if (message.Length > 2) return FinishClose(code, reason); 114 | return FinishClose(code); 115 | case 9: OnPing?.Invoke(message.Payload); return true; 116 | case 10: OnPong?.Invoke(message.Payload); return true; 117 | default: return false; 118 | } 119 | } 120 | 121 | protected bool Send(Frame frame) 122 | { 123 | lock (Sync) 124 | { 125 | if (SentClose || RecvClose || !Base.CanWrite) return false; 126 | frame.SerializeTo(Base); 127 | return true; 128 | } 129 | } 130 | public abstract bool SendBinary(byte[] payload); 131 | public virtual bool SendBinary(byte[] payload, ushort start) => SendBinary(PrimitiveBuffer.Slice(payload, start, (ulong)payload.LongLength)); 132 | public virtual bool SendBinary(byte[] payload, ushort start, ulong end) => SendBinary(PrimitiveBuffer.Slice(payload, start, end)); 133 | public abstract bool SendString(string payload); 134 | public abstract bool SendPing(byte[] payload = null); 135 | public virtual bool SendPing(byte[] payload, ushort start) => SendPing(PrimitiveBuffer.Slice(payload, start, (ulong)payload.LongLength)); 136 | public virtual bool SendPing(byte[] payload, ushort start, ulong end) => SendPing(PrimitiveBuffer.Slice(payload, start, end)); 137 | protected abstract bool SendPong(byte[] payload); 138 | public abstract bool SendClose(ushort code, string reason = ""); 139 | protected abstract bool SendClose(ushort code); 140 | 141 | protected virtual bool Terminate(ushort code, string reason) 142 | { 143 | lock (Sync) 144 | { 145 | if (!Opening && !Open && !Closing) return false; 146 | Base.Terminate(); 147 | FinishClose(code, reason); 148 | return true; 149 | } 150 | } 151 | public virtual bool Terminate() 152 | { 153 | lock (Sync) 154 | { 155 | if (!Opening && !Open && !Closing) return false; 156 | Base.Terminate(); 157 | FinishClose(); 158 | return true; 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /CSSockets/WebSockets/Definition/ConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using CSSockets.Tcp; 3 | using CSSockets.Http.Reference; 4 | using CSSockets.Http.Definition; 5 | using System.Collections.Generic; 6 | using System.Security.Cryptography; 7 | 8 | namespace CSSockets.WebSockets.Definition 9 | { 10 | public abstract class ConnectionFactory : Negotiator where TConnection : Connection 11 | { 12 | protected override IEnumerable RespondExtensions(NegotiatingExtension[] requested) => null; 13 | 14 | public virtual TConnection Generate(Tcp.Connection connection, URL url, params string[] reqSubprotocols) 15 | { 16 | ClientConnection httpClient = new ClientConnection(connection); 17 | OutgoingRequest req = httpClient.Enqueue("HTTP/1.1", "GET", url); 18 | TConnection newConnection = GenerateConnection(connection, req.Head); 19 | 20 | string reqExtensions = NegotiatingExtension.Stringify(RequestExtensions()); 21 | string key = Secret.GenerateKey(); 22 | 23 | req["Connection"] = "Upgrade"; 24 | req["Upgrade"] = "websocket"; 25 | req["Sec-WebSocket-Version"] = "13"; 26 | req["Sec-WebSocket-Key"] = key; 27 | if (reqSubprotocols.Length > 0) req["Sec-WebSocket-Protocol"] = SubprotocolNegotiation.Stringify(reqSubprotocols); 28 | if (reqExtensions.Length > 0) req["Sec-WebSocket-Extensions"] = reqExtensions; 29 | 30 | req.OnResponse += (res) => 31 | { 32 | if (!VerifyResponseHead(res, key)) { httpClient.Terminate(); return; } 33 | string subprotocol = res["Sec-WebSocket-Protocol"]; 34 | if (subprotocol != null) 35 | { 36 | bool validSubprotocol = false; 37 | for (int i = 0; i < reqSubprotocols.Length; i++) 38 | if (reqSubprotocols[i] == subprotocol) { validSubprotocol = true; break; } 39 | if (!validSubprotocol) { httpClient.Terminate(); return; } 40 | } 41 | newConnection.SetSubprotocol(subprotocol); 42 | if ( 43 | !NegotiatingExtension.TryParse(res["Sec-WebSocket-Extensions"] ?? "", out NegotiatingExtension[] resExtensions) 44 | || !CheckExtensions(resExtensions) 45 | ) { httpClient.Terminate(); return; } 46 | 47 | byte[] trail = httpClient.Freeze(); 48 | if (!httpClient.End()) return; 49 | newConnection.Initiate(trail); 50 | }; 51 | 52 | return req.End() ? newConnection : null; 53 | } 54 | protected abstract TConnection GenerateConnection(Tcp.Connection connection, RequestHead req); 55 | protected virtual bool VerifyResponseHead(IncomingResponse res, string key) 56 | { 57 | if (res.StatusCode != 101) return false; 58 | if (res["Connection"] != "Upgrade") return false; 59 | if (res["Upgrade"] != "websocket") return false; 60 | if (res["Sec-WebSocket-Version"] != "13") return false; 61 | if (res["Sec-WebSocket-Accept"] != Secret.ComputeAccept(key)) return false; 62 | return true; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /CSSockets/WebSockets/Definition/ConnectionMode.cs: -------------------------------------------------------------------------------- 1 | namespace CSSockets.WebSockets.Definition 2 | { 3 | public abstract partial class Connection 4 | { 5 | public interface IMode 6 | { 7 | bool FireOpen { get; } 8 | bool IncomingMasked { get; } 9 | bool OutgoingMasked { get; } 10 | } 11 | public struct ClientMode : IMode 12 | { 13 | public bool FireOpen => true; 14 | public bool IncomingMasked => false; 15 | public bool OutgoingMasked => true; 16 | } 17 | public struct ServerMode : IMode 18 | { 19 | public bool FireOpen => false; 20 | public bool IncomingMasked => true; 21 | public bool OutgoingMasked => false; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /CSSockets/WebSockets/Definition/Headers.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Specialized; 3 | 4 | namespace CSSockets.WebSockets.Definition 5 | { 6 | public class NegotiatingExtension 7 | { 8 | public StringDictionary Parameters { get; } 9 | public string ExtensionName { get; set; } 10 | 11 | public static bool TryParse(string s, out NegotiatingExtension[] result) 12 | { 13 | result = null; 14 | if (s == "") { result = new NegotiatingExtension[0]; return true; } 15 | List parsed = new List(); 16 | string[] spl = s.Split(","); 17 | for (int i = 0; i < spl.Length; i++) 18 | { 19 | string ext = spl[i].Trim(); 20 | if (ext.Length == 0) return false; 21 | string[] spl2 = ext.Split(";"); 22 | NegotiatingExtension current = new NegotiatingExtension(); 23 | if ((current.ExtensionName = spl2[0].Trim()).Length == 0) 24 | return false; 25 | for (int j = 1; j < spl2.Length; j++) 26 | { 27 | string[] spl3 = spl2[j].Split("="); 28 | if (spl3.Length > 2) return false; 29 | current.Parameters.Add(spl3[0].Trim(), spl3.Length == 2 ? spl3[1].Trim() : ""); 30 | } 31 | parsed.Add(current); 32 | } 33 | result = parsed.ToArray(); 34 | return true; 35 | } 36 | 37 | public static string Stringify(IEnumerable extensions) 38 | { 39 | string s = ""; 40 | foreach (NegotiatingExtension ext in extensions) s += ext + ", "; 41 | return s.Length == 0 ? s : s.Substring(0, s.Length - 2); 42 | } 43 | 44 | public NegotiatingExtension() 45 | { 46 | Parameters = new StringDictionary(); 47 | ExtensionName = null; 48 | } 49 | 50 | public override string ToString() 51 | { 52 | string s = ExtensionName; 53 | if (Parameters.Count == 0) return s; 54 | s += "; "; 55 | string[] keys = new string[Parameters.Count]; 56 | Parameters.Keys.CopyTo(keys, 0); 57 | for (int i = 0; i < Parameters.Count; i++) 58 | { 59 | string param = Parameters[keys[i]]; 60 | s += keys[i] + (param == "" ? "" : "=" + param) + (i == Parameters.Count - 1 ? "" : "; "); 61 | } 62 | return s; 63 | } 64 | } 65 | public static class SubprotocolNegotiation 66 | { 67 | public static bool TryParse(string s, out string[] result) 68 | { 69 | result = null; 70 | string[] spl = s.Split(','); 71 | for (int i = 0; i < spl.Length; i++) spl[i] = spl[i].Trim(); 72 | result = spl; 73 | return true; 74 | } 75 | 76 | public static string Stringify(IEnumerable subprotocols) 77 | { 78 | string s = ""; 79 | foreach (string subp in subprotocols) s += subp + ", "; 80 | return s.Length == 0 ? s : s.Substring(0, s.Length - 2); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /CSSockets/WebSockets/Definition/Listener.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Text; 3 | using CSSockets.Http.Reference; 4 | using CSSockets.Http.Definition; 5 | using System.Security.Cryptography; 6 | using System.Collections.Generic; 7 | 8 | namespace CSSockets.WebSockets.Definition 9 | { 10 | public delegate bool ClientVerifier(IPAddress remote, string[] subprotocols, RequestHead head); 11 | public delegate string SubprotocolChooser(IPAddress remote, string[] subprotocols, RequestHead head); 12 | public abstract class Listener : Negotiator 13 | { 14 | public Http.Reference.Listener Base { get; } 15 | 16 | protected readonly object Sync = new object(); 17 | protected SHA1 Hasher; 18 | public ClientVerifier ClientVerifier { get; set; } = null; 19 | public SubprotocolChooser SubprotocolChooser { get; set; } = null; 20 | 21 | public Listener() 22 | { 23 | Base = new Http.Reference.Listener(); 24 | Base.OnRequest = _RequestHandler; 25 | } 26 | public Listener(EndPoint endPoint) 27 | { 28 | Base = new Http.Reference.Listener(endPoint); 29 | Base.OnRequest = _RequestHandler; 30 | } 31 | public Listener(Tcp.Listener listener) 32 | { 33 | Base = new Http.Reference.Listener(listener); 34 | Base.OnRequest = _RequestHandler; 35 | } 36 | public Listener(Http.Reference.Listener listener) 37 | { 38 | Base = new Http.Reference.Listener(); 39 | Base.OnRequest = _RequestHandler; 40 | } 41 | 42 | protected void _RequestHandler(IncomingRequest req, OutgoingResponse res) => RequestHandler(req, res); 43 | protected virtual bool RequestHandler(IncomingRequest req, OutgoingResponse res) 44 | { 45 | if (!CheckHeaders(req, res)) return false; 46 | IPAddress remote = res.Connection.Base.RemoteAddress; 47 | 48 | if (!SubprotocolNegotiation.TryParse(req["Sec-WebSocket-Protocol"] ?? "", out string[] subprotocols)) 49 | return DropRequest(res, 400, "Bad Request", "Could not parse subprotocols"); 50 | if (ClientVerifier != null && !ClientVerifier(remote, subprotocols, req.Head)) 51 | return DropRequest(res, 403, "Forbidden"); 52 | if (!NegotiatingExtension.TryParse(req["Sec-WebSocket-Extensions"] ?? "", out NegotiatingExtension[] requestedExtensions)) 53 | return DropRequest(res, 400, "Bad Request", "Could not parse extensions"); 54 | if (!CheckExtensions(requestedExtensions)) 55 | return DropRequest(res, 400, "Bad Request", "Extensions have been rejected"); 56 | 57 | string subprotocol = null; 58 | if (subprotocols.Length > 0 && SubprotocolChooser != null) 59 | { 60 | subprotocol = SubprotocolChooser(remote, subprotocols, req.Head); 61 | if (subprotocol == null) DropRequest(res, 501, "Not Implemented"); 62 | res["Sec-WebSocket-Protocol"] = subprotocol; 63 | } 64 | string respondedExtensions = NegotiatingExtension.Stringify(RespondExtensions(requestedExtensions)); 65 | if (respondedExtensions.Length > 0) 66 | res["Sec-WebSocket-Extensions"] = respondedExtensions; 67 | 68 | res["Connection"] = "Upgrade"; 69 | res["Upgrade"] = "websocket"; 70 | res["Sec-WebSocket-Version"] = "13"; 71 | res["Sec-WebSocket-Accept"] = Secret.ComputeAccept(req["Sec-WebSocket-Key"]); 72 | res.End(101, "Switching Protocols"); 73 | 74 | byte[] trail = res.Connection.Freeze(); 75 | if (!res.Connection.End()) return false; 76 | FireConnection(res.Connection.Base, req.Head, subprotocol, trail); 77 | return true; 78 | } 79 | protected abstract void FireConnection(Tcp.Connection connection, RequestHead req, string subprotocol, byte[] trail); 80 | protected override IEnumerable RequestExtensions() => null; 81 | 82 | protected bool DropRequest(OutgoingResponse res, ushort code, string reason, string asciiBody = null, params Header[] headers) 83 | { 84 | for (int i = 0; i < headers.Length; i++) res[headers[i].Key] = headers[i].Value; 85 | res["Content-Length"] = asciiBody?.Length.ToString() ?? "0"; 86 | res.SendHead(code, reason); 87 | if (asciiBody != null) res.Write(Encoding.ASCII.GetBytes(asciiBody)); 88 | return false; 89 | } 90 | protected bool CheckHeaders(IncomingRequest req, OutgoingResponse res) 91 | { 92 | if (req.Version != "HTTP/1.1") 93 | return DropRequest(res, 505, "HTTP Version Not Supported", "Use HTTP/1.1"); 94 | if (req.Method != "GET") 95 | return DropRequest(res, 405, "Method Not Allowed", "Use GET"); 96 | 97 | BodyType? type = BodyType.TryDetectFor(req.Head, true); 98 | if (type == null) 99 | return DropRequest(res, 400, "Bad Request", "Cannot detect body type"); 100 | if (type.Value.Encoding != TransferEncoding.None) 101 | return DropRequest(res, 400, "Bad Request", "No body allowed"); 102 | 103 | if (req["Connection"] != "Upgrade") 104 | return DropRequest(res, 426, "Upgrade Required", "Upgrade required"); 105 | if (req["Upgrade"] != "websocket") 106 | return DropRequest(res, 400, "Bad Request", "Upgrade to websocket"); 107 | 108 | if (req["Sec-WebSocket-Version"] != "13") 109 | return DropRequest(res, 400, "Bad Request", "Unsupported WebSocket version", new Header("Sec-WebSocket-Version", "13")); 110 | if (req["Sec-WebSocket-Key"] == null) 111 | return DropRequest(res, 400, "Bad Request", "Sec-WebSocket-Key not given"); 112 | return true; 113 | } 114 | 115 | public void Start() 116 | { 117 | lock (Sync) 118 | { 119 | Hasher = SHA1.Create(); 120 | Base.Start(); 121 | } 122 | } 123 | public void Stop() 124 | { 125 | lock (Sync) 126 | { 127 | Base.Stop(); 128 | Hasher.Dispose(); 129 | Hasher = null; 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /CSSockets/WebSockets/Definition/Negotiator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace CSSockets.WebSockets.Definition 4 | { 5 | public abstract class Negotiator 6 | { 7 | protected abstract bool CheckExtensions(NegotiatingExtension[] extensions); 8 | protected abstract IEnumerable RequestExtensions(); 9 | protected abstract IEnumerable RespondExtensions(NegotiatingExtension[] requested); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CSSockets/WebSockets/Definition/Protocol.cs: -------------------------------------------------------------------------------- 1 | using CSSockets.Binary; 2 | using CSSockets.Streams; 3 | 4 | namespace CSSockets.WebSockets.Definition 5 | { 6 | public struct Frame 7 | { 8 | public static byte GetXLengthSize(byte length) 9 | => length == 127 ? (byte)8 : length == 126 ? (byte)2 : (byte)0; 10 | public static byte GetLengthFromXLength(ulong xLength) 11 | => xLength >= 65536 ? (byte)127 : xLength >= 126 ? (byte)126 : (byte)xLength; 12 | 13 | public bool FIN { get; set; } 14 | public byte Opcode { get; set; } 15 | public bool RSV1 { get; set; } 16 | public bool RSV2 { get; set; } 17 | public bool RSV3 { get; set; } 18 | public byte Length { get; set; } 19 | public ulong ExtendedLength { get; set; } 20 | public bool Masked { get; set; } 21 | public byte[] Mask { get; set; } 22 | public byte[] Payload { get; set; } 23 | 24 | public Frame(byte head1, byte head2) : this() 25 | { 26 | FIN = (head1 & 128) == 128; 27 | RSV1 = (head1 & 64) == 64; 28 | RSV2 = (head1 & 32) == 32; 29 | RSV3 = (head1 & 16) == 16; 30 | Opcode = (byte)(head1 - (FIN ? 128 : 0) - (RSV1 ? 64 : 0) - (RSV2 ? 32 : 0) - (RSV3 ? 16 : 0)); 31 | Masked = (head2 & 128) == 128; 32 | Length = (byte)(head2 - (Masked ? 128 : 0)); 33 | Payload = null; 34 | } 35 | public Frame(bool fin, byte opcode, bool rsv1, bool rsv2, bool rsv3, bool masked, byte[] payload) : this() 36 | { 37 | FIN = fin; 38 | Opcode = opcode; 39 | RSV1 = rsv1; 40 | RSV2 = rsv2; 41 | RSV3 = rsv3; 42 | ExtendedLength = (ulong)payload.LongLength; 43 | Length = GetLengthFromXLength(ExtendedLength); 44 | if (Masked = masked) Mask = Secret.GenerateMask(); 45 | Payload = payload; 46 | } 47 | public Frame(bool fin, byte opcode, bool rsv1, bool rsv2, bool rsv3, byte[] mask, byte[] payload) : this() 48 | { 49 | FIN = fin; 50 | Opcode = opcode; 51 | RSV1 = rsv1; 52 | RSV2 = rsv2; 53 | RSV3 = rsv3; 54 | ExtendedLength = (ulong)payload.LongLength; 55 | Length = GetLengthFromXLength(ExtendedLength); 56 | Masked = true; 57 | Mask = mask; 58 | Payload = payload; 59 | } 60 | 61 | public void SerializeTo(IWritable writable) 62 | { 63 | StreamWriter writer = new StreamWriter(writable); 64 | writer.WriteUInt8((byte)(Opcode + (FIN ? 128 : 0) + (RSV1 ? 64 : 0) + (RSV2 ? 32 : 0) + (RSV3 ? 16 : 0))); 65 | writer.WriteUInt8((byte)(Length + (Mask != null ? 128 : 0))); 66 | switch (GetXLengthSize(Length)) 67 | { 68 | case 0: break; 69 | case 2: writer.WriteUInt16LE((ushort)ExtendedLength); break; 70 | case 8: writer.WriteUInt64LE(ExtendedLength); break; 71 | } 72 | if (Mask != null) { writable.Write(Mask); writable.Write(FlipMask(Payload, Mask)); } 73 | else writable.Write(Payload); 74 | } 75 | 76 | public static byte[] FlipMask(byte[] payload, byte[] mask) 77 | { 78 | byte[] copied = PrimitiveBuffer.Slice(payload, 0, (ulong)payload.LongLength); 79 | for (long i = 0; i < copied.LongLength; i++) copied[i] ^= mask[i & 3]; 80 | return copied; 81 | } 82 | } 83 | 84 | public struct Message 85 | { 86 | public byte Opcode { get; } 87 | public byte[] Payload { get; } 88 | public ulong Length { get; } 89 | 90 | public Message(byte opcode, byte[] payload, ulong length) : this() 91 | { 92 | Opcode = opcode; 93 | Payload = payload; 94 | Length = length; 95 | } 96 | } 97 | 98 | public sealed class FrameParser : Collector 99 | { 100 | enum ParseState : byte 101 | { 102 | Head, 103 | ExtendedLen2, 104 | ExtendedLen8, 105 | Mask, 106 | Payload 107 | } 108 | 109 | private Frame Incoming = new Frame(); 110 | private readonly MemoryDuplex Buffer; 111 | private readonly StreamReader Reader; 112 | private ParseState State = ParseState.Head; 113 | 114 | public FrameParser() => Reader = new StreamReader(Buffer = new MemoryDuplex()); 115 | 116 | protected override bool HandleWritable(byte[] source) 117 | { 118 | Buffer.Write(source); ulong buffered; 119 | while ((buffered = Buffer.BufferedReadable) > 0) 120 | switch (State) 121 | { 122 | case ParseState.Head: 123 | if (buffered < 2) return true; 124 | Incoming = new Frame(Reader.ReadUInt8(), Reader.ReadUInt8()); 125 | switch (Frame.GetXLengthSize(Incoming.Length)) 126 | { 127 | case 0: 128 | Incoming.ExtendedLength = Incoming.Length; 129 | if (Incoming.Masked) State = ParseState.Mask; 130 | else if (Incoming.Length > 0) State = ParseState.Payload; 131 | else Reset(); 132 | break; 133 | case 2: State = ParseState.ExtendedLen2; break; 134 | case 8: State = ParseState.ExtendedLen8; break; 135 | } 136 | break; 137 | case ParseState.ExtendedLen2: 138 | if (buffered < 2) return true; 139 | Incoming.ExtendedLength = Reader.ReadUInt16LE(); 140 | if (Incoming.Masked) State = ParseState.Mask; 141 | else if (Incoming.ExtendedLength > 0) State = ParseState.Payload; 142 | else Reset(); 143 | break; 144 | case ParseState.ExtendedLen8: 145 | if (buffered < 8) return true; 146 | Incoming.ExtendedLength = Reader.ReadUInt64LE(); 147 | if (Incoming.Masked) State = ParseState.Mask; 148 | else if (Incoming.ExtendedLength > 0) State = ParseState.Payload; 149 | else Reset(); 150 | break; 151 | case ParseState.Mask: 152 | if (buffered < 4) return true; 153 | Incoming.Mask = Buffer.Read(4); 154 | State = ParseState.Payload; 155 | break; 156 | case ParseState.Payload: 157 | if (buffered < Incoming.ExtendedLength) return true; 158 | Incoming.Payload = Buffer.Read(Incoming.ExtendedLength); 159 | Reset(); 160 | break; 161 | } 162 | return true; 163 | } 164 | 165 | private bool Reset() 166 | { 167 | Incoming.Payload = Incoming.Payload ?? new byte[0]; 168 | Pickup(Incoming); 169 | Incoming = new Frame(); 170 | State = ParseState.Head; 171 | return true; 172 | } 173 | } 174 | 175 | public delegate void MessageHandler(Message message); 176 | public sealed class FrameMerger 177 | { 178 | public enum Response : byte 179 | { 180 | OK, 181 | Reset, 182 | MessageNotStarted, 183 | MessageNotFinished 184 | } 185 | private readonly object Sync = new object(); 186 | 187 | public event MessageHandler OnCollect; 188 | private byte IncomingOpcode = 0; 189 | private readonly MemoryDuplex IncomingMessage = new MemoryDuplex(); 190 | 191 | public Response Push(Frame frame) 192 | { 193 | lock (Sync) 194 | { 195 | if (frame.Opcode == 0 && IncomingOpcode == 0) 196 | return Response.MessageNotStarted; 197 | if (frame.Opcode != 0 && IncomingOpcode != 0) 198 | return Response.MessageNotFinished; 199 | if (frame.Opcode != 0) IncomingOpcode = frame.Opcode; 200 | IncomingMessage.Write(frame.Masked ? Frame.FlipMask(frame.Payload, frame.Mask) : frame.Payload); 201 | if (!frame.FIN) return Response.OK; 202 | ulong length = IncomingMessage.BufferedReadable; 203 | OnCollect?.Invoke(new Message(IncomingOpcode, IncomingMessage.Read(), length)); 204 | IncomingOpcode = 0; 205 | return Response.Reset; 206 | } 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /CSSockets/WebSockets/Definition/Secret.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Security.Cryptography; 4 | 5 | namespace CSSockets.WebSockets.Definition 6 | { 7 | internal static class Secret 8 | { 9 | public static readonly SHA1 Hasher = SHA1.Create(); 10 | public static readonly RandomNumberGenerator RNG = RandomNumberGenerator.Create(); 11 | public static readonly string Magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 12 | 13 | public static byte[] GenerateMask() 14 | { 15 | byte[] key = new byte[4]; 16 | RNG.GetBytes(key); 17 | return key; 18 | } 19 | public static string GenerateKey() 20 | { 21 | byte[] key = new byte[4]; 22 | RNG.GetBytes(key); 23 | return Convert.ToBase64String(key); 24 | } 25 | public static string ComputeAccept(string key) 26 | { 27 | byte[] accept = Hasher.ComputeHash(Encoding.UTF8.GetBytes(key + Magic)); 28 | return Convert.ToBase64String(accept); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /CSSockets/WebSockets/Primitive/Connection.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using CSSockets.Streams; 3 | using CSSockets.Http.Reference; 4 | using CSSockets.WebSockets.Definition; 5 | 6 | namespace CSSockets.WebSockets.Primitive 7 | { 8 | public class Connection : Definition.Connection 9 | { 10 | public RequestHead Req { get; } 11 | 12 | public Connection(Tcp.Connection connection, RequestHead req, IMode mode) : base(connection, mode) => Req = req; 13 | 14 | public override bool SendBinary(byte[] payload) 15 | { 16 | lock (Sync) return Send(new Frame(true, 2, false, false, false, Mode.OutgoingMasked, payload)); 17 | } 18 | public override bool SendClose(ushort code, string reason = "") 19 | { 20 | lock (Sync) 21 | { 22 | MemoryDuplex buffer = new MemoryDuplex(); 23 | buffer.Write(new byte[2] { (byte)(code / 256), (byte)(code % 256) }); 24 | buffer.Write(Encoding.UTF8.GetBytes(reason ?? "")); 25 | return Send(new Frame(true, 8, false, false, false, Mode.OutgoingMasked, buffer.Read())) && StartClose(code, reason); 26 | } 27 | } 28 | protected override bool SendClose(ushort code) 29 | { 30 | lock (Sync) return Send(new Frame(true, 8, false, false, false, Mode.OutgoingMasked, new byte[2] { (byte)(code / 256), (byte)(code % 256) })); 31 | } 32 | public override bool SendPing(byte[] payload) 33 | { 34 | lock (Sync) return Send(new Frame(true, 9, false, false, false, Mode.OutgoingMasked, payload ?? new byte[0])); 35 | } 36 | public override bool SendString(string payload) 37 | { 38 | lock (Sync) return Send(new Frame(true, 1, false, false, false, Mode.OutgoingMasked, Encoding.UTF8.GetBytes(payload))); 39 | } 40 | protected override void OnMergerCollect(Message message) 41 | { 42 | lock (Sync) if (!HandleMessage(message)) Terminate(1002, ""); 43 | } 44 | protected override void OnParserCollect(Frame frame) 45 | { 46 | lock (Sync) 47 | { 48 | if (frame.Masked != Mode.IncomingMasked) { Terminate(1002, ""); return; } 49 | if (frame.RSV1 || frame.RSV2 || frame.RSV3) { Terminate(1002, ""); return; } 50 | switch (Merger.Push(frame)) 51 | { 52 | case FrameMerger.Response.OK: 53 | case FrameMerger.Response.Reset: 54 | break; 55 | default: Terminate(1002, ""); break; 56 | } 57 | } 58 | } 59 | protected override bool SendPong(byte[] payload) 60 | { 61 | lock (Sync) return Send(new Frame(true, 10, false, false, false, Mode.OutgoingMasked, payload)); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CSSockets/WebSockets/Primitive/ConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using CSSockets.Http.Reference; 3 | using System.Collections.Generic; 4 | using CSSockets.WebSockets.Definition; 5 | 6 | namespace CSSockets.WebSockets.Primitive 7 | { 8 | public class ConnectionFactory : ConnectionFactory 9 | { 10 | public static readonly ConnectionFactory Default = new ConnectionFactory(); 11 | 12 | protected override bool CheckExtensions(NegotiatingExtension[] requested) => requested.Length == 0; 13 | protected override IEnumerable RequestExtensions() => Enumerable.Empty(); 14 | protected override Connection GenerateConnection(Tcp.Connection connection, RequestHead req) => new Connection(connection, req, new Definition.Connection.ClientMode()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CSSockets/WebSockets/Primitive/Listener.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Linq; 3 | using CSSockets.Http.Reference; 4 | using System.Collections.Generic; 5 | using CSSockets.WebSockets.Definition; 6 | 7 | namespace CSSockets.WebSockets.Primitive 8 | { 9 | public delegate void ConnectionHandler(Connection connection); 10 | public class Listener : Definition.Listener 11 | { 12 | public event ConnectionHandler OnConnection; 13 | 14 | public Listener() : base() { } 15 | public Listener(EndPoint endPoint) : base(endPoint) { } 16 | public Listener(Tcp.Listener listener) : base(listener) { } 17 | public Listener(Http.Reference.Listener listener) : base(listener) { } 18 | 19 | protected override bool CheckExtensions(NegotiatingExtension[] extensions) => true; 20 | protected override IEnumerable RespondExtensions(NegotiatingExtension[] requested) => Enumerable.Empty(); 21 | protected override void FireConnection(Tcp.Connection connection, RequestHead req, string subprotocol, byte[] trail) 22 | { 23 | Connection newConnection = new Connection(connection, req, new Definition.Connection.ServerMode()); 24 | newConnection.SetSubprotocol(subprotocol); 25 | OnConnection?.Invoke(newConnection); 26 | newConnection.Initiate(trail); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Luka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSSockets 2 | 3 | An implementation of event-based sockets for .NET Core 2.0. 4 | Includes highly scalable, wrapped TCP, low-level HTTP and raw WebSockets, all being thread-safe, yet internally use the least amount of threads. 5 | Data handling is done with Node.js-inspired reinvented streams - see CSSockets.Streams. 6 | This is a shaky but functioning library - bug hunting is encouraged. 7 | 8 | This project uses object-oriented programming to a large extent to enable heavy customization: 9 | - All streams inherit either interfaces directly or base classes. 10 | - Sockets are wrapped then accessed with events and stream methods however it's left exposed if you want to do magic. 11 | - All the base HTTP classes are built on generics so you can even make your own HTTP version, albeit not in the form of HTTP/2. 12 | 13 | The performance focus is around parallelization of heavy workloads but with minimal cross-thread tampering: 14 | - Calls to Readable, Writable, Duplex and Compressors stream implemenatations don't cross or make new threads. 15 | - Accepted sockets made by CSSockets.Tcp.Listener are handled with Socket.Select to ensure minimal thread usage. 16 | - Multiple socket I/O processor threads will be opened for more than X sockets - even that is open to change. 17 | 18 | Implementations: 19 | 20 | - [X] Base stream classes 21 | - [X] Readable 22 | - [X] Writable 23 | - [X] Duplex 24 | - [X] Transform (UnifiedDuplex is capable of this) 25 | - [X] TCP 26 | - [X] Client 27 | - [X] Server 28 | - [X] HTTP 29 | - [X] Base HTTP classes 30 | - [X] Header 31 | - [X] Version 32 | - [X] Query tokens 33 | - [X] Paths 34 | - [X] Head parsing 35 | - [X] Request head 36 | - [X] Response head 37 | - [X] Head serializing 38 | - [X] Request head 39 | - [X] Response head 40 | - [X] Body parsing 41 | - [X] Binary 42 | - [X] Chunked 43 | - [X] Compressed binary 44 | - [X] Compressed chunked 45 | - [X] Body serializing 46 | - [X] Binary 47 | - [X] Chunked 48 | - [X] Compressed binary 49 | - [X] Compressed chunked 50 | - [X] HTTP connections 51 | - [X] Client-side connection 52 | - [X] Server-side connection 53 | - [X] Support for custom upgrading 54 | - [X] HTTP listener 55 | - [X] WebSockets 56 | - [X] Message parsing 57 | - [X] Message serialization 58 | - [X] WebSocket connections 59 | - [X] Client-side connection 60 | - [X] Server-side connection 61 | - [X] Support for custom extensions 62 | - Implement permessage-deflate? 63 | - HTTPS? 64 | - Secure WebSockets? 65 | --------------------------------------------------------------------------------