├── .nuget ├── NuGet.exe ├── NuGet.Config └── NuGet.targets ├── Switchboard.Server ├── packages.config ├── Utils │ ├── VoidTypeStruct.cs │ ├── HttpParser │ │ ├── IHttpResponseHandler.cs │ │ └── HttpResponseParser.cs │ ├── MaxReadStream.cs │ ├── StartAvailableStream.cs │ ├── ChunkedStream.cs │ └── RedirectingStream.cs ├── Handlers │ └── ISwitchboardRequestHandler.cs ├── Connection │ ├── SwitchboardConnection.cs │ ├── SecureOutboundConnection.cs │ ├── SecureInboundConnection.cs │ ├── OutboundConnection.cs │ └── InboundConnection.cs ├── Server │ ├── SecureSwitchboardServer.cs │ └── SwitchboardServer.cs ├── Extensions │ └── TaskExtensions.cs ├── Properties │ └── AssemblyInfo.cs ├── Response │ ├── SwitchboardResponse.cs │ └── SwitchboardResponseParser.cs ├── Request │ ├── SwitchboardRequest.cs │ └── SwitchboardRequestParser.cs ├── Switchboard.Server.csproj └── Context │ └── SwitchboardContext.cs ├── .gitignore ├── Samples └── Switchboard.ConsoleHost │ ├── App.config │ ├── Program.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── Logging │ └── ConsoleLogger.cs │ ├── Switchboard.ConsoleHost.csproj │ └── SimpleReverseProxyHandler.cs ├── LICENSE ├── Switchboard.Server.Tests ├── ChunkedStreamTests.cs ├── Properties │ └── AssemblyInfo.cs └── Switchboard.Server.Tests.csproj ├── Switchboard.sln └── Readme.md /.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niik/switchboard/HEAD/.nuget/NuGet.exe -------------------------------------------------------------------------------- /Switchboard.Server/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | *.user 4 | /TestResults 5 | *.vspscc 6 | *.vssscc 7 | *.suo 8 | *.[Cc]ache 9 | *.csproj.user 10 | /packages/ 11 | msbuild.log 12 | *.psess 13 | -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Switchboard.Server/Utils/VoidTypeStruct.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Switchboard.Server.Utils 4 | { 5 | // http://blogs.msdn.com/b/pfxteam/archive/2011/11/10/10235834.aspx 6 | internal struct VoidTypeStruct { } 7 | } 8 | -------------------------------------------------------------------------------- /Samples/Switchboard.ConsoleHost/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Switchboard.Server/Handlers/ISwitchboardRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Switchboard.Server 4 | { 5 | public interface ISwitchboardRequestHandler 6 | { 7 | Task GetResponseAsync(SwitchboardContext context, SwitchboardRequest request); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Switchboard.Server/Connection/SwitchboardConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Switchboard.Server.Connection 8 | { 9 | public abstract class SwitchboardConnection 10 | { 11 | public abstract bool IsSecure { get; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Switchboard.Server/Utils/HttpParser/IHttpResponseHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Switchboard.Server.Utils.HttpParser 4 | { 5 | public interface IHttpResponseHandler 6 | { 7 | void OnResponseBegin(); 8 | void OnStatusLine(Version protocolVersion, int statusCode, string statusDescription); 9 | void OnHeader(string name, string value); 10 | void OnHeadersEnd(); 11 | void OnEntityStart(); 12 | void OnEntityData(byte[] buffer, int offset, int count); 13 | void OnEntityEnd(); 14 | void OnResponseEnd(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Samples/Switchboard.ConsoleHost/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Net; 4 | using Switchboard.ConsoleHost.Logging; 5 | using Switchboard.Server; 6 | 7 | namespace Switchboard.ConsoleHost 8 | { 9 | class Program 10 | { 11 | static void Main(string[] args) 12 | { 13 | // Dump all debug data to the console, coloring it if possible 14 | Trace.Listeners.Add(new ConsoleLogger()); 15 | 16 | var endPoint = new IPEndPoint(IPAddress.Loopback, 8080); 17 | var handler = new SimpleReverseProxyHandler("http://www.nytimes.com"); 18 | var server = new SwitchboardServer(endPoint, handler); 19 | 20 | server.Start(); 21 | 22 | Console.WriteLine("Point your browser at http://{0}", endPoint); 23 | 24 | Console.ReadLine(); 25 | } 26 | } 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Switchboard.Server/Server/SecureSwitchboardServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Sockets; 6 | using System.Security.Cryptography.X509Certificates; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Switchboard.Server.Connection; 10 | 11 | namespace Switchboard.Server 12 | { 13 | public class SecureSwitchboardServer : SwitchboardServer 14 | { 15 | private X509Certificate certificate; 16 | public SecureSwitchboardServer(IPEndPoint endPoint, ISwitchboardRequestHandler handler, X509Certificate certificate) 17 | : base(endPoint, handler) 18 | { 19 | this.certificate = certificate; 20 | } 21 | 22 | protected override Task CreateInboundConnection(TcpClient client) 23 | { 24 | return Task.FromResult(new SecureInboundConnection(client, this.certificate)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Markus Olsson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Switchboard.Server/Extensions/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Switchboard.Server.Extensions 8 | { 9 | public static class TaskExtensions 10 | { 11 | // http://blogs.msdn.com/b/pfxteam/archive/2011/11/10/10235834.aspx 12 | public static void MarshalTaskResults(this Task source, TaskCompletionSource proxy) 13 | { 14 | switch (source.Status) 15 | { 16 | case TaskStatus.Faulted: 17 | proxy.TrySetException(source.Exception); 18 | break; 19 | case TaskStatus.Canceled: 20 | proxy.TrySetCanceled(); 21 | break; 22 | case TaskStatus.RanToCompletion: 23 | Task castedSource = source as Task; 24 | proxy.TrySetResult( 25 | castedSource == null ? default(TResult) : // source is a Task 26 | castedSource.Result); // source is a Task 27 | break; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Switchboard.Server.Tests/ChunkedStreamTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using Switchboard.Server.Utils; 5 | 6 | namespace Switchboard.Server.Tests 7 | { 8 | [TestClass] 9 | public class ChunkedStreamTests 10 | { 11 | [TestMethod] 12 | public void TestMethod1() 13 | { 14 | var ms = new MemoryStream(); 15 | var sw = new StreamWriter(ms); 16 | sw.NewLine = "\r\n"; 17 | 18 | var sizes = new[] { 10, 30, 10, 100, 20 }; 19 | 20 | foreach (var size in sizes) 21 | { 22 | sw.WriteLine(size.ToString("X")); 23 | sw.WriteLine(new string('Y', size)); 24 | } 25 | sw.Flush(); 26 | 27 | ms.Seek(0, SeekOrigin.Begin); 28 | 29 | var chunkedStream = new ChunkedStream(ms); 30 | 31 | var ms2 = new MemoryStream(); 32 | chunkedStream.CopyToAsync(ms2).Wait(); 33 | 34 | Assert.AreEqual(ms.Length, ms2.Length); 35 | 36 | var buf1 = ms.ToArray(); 37 | var buf2 = ms2.ToArray(); 38 | 39 | for (int i = 0; i < ms.Length; i++) 40 | Assert.AreEqual(buf1[i], buf2[i]); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Switchboard.Server/Connection/SecureOutboundConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Security; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace Switchboard.Server.Connection 11 | { 12 | public class SecureOutboundConnection : OutboundConnection 13 | { 14 | public string TargetHost { get; set; } 15 | protected SslStream SslStream { get; private set; } 16 | public override bool IsSecure { get { return true; } } 17 | 18 | public SecureOutboundConnection(string targetHost, IPEndPoint ep) 19 | : base(ep) 20 | { 21 | this.TargetHost = targetHost; 22 | } 23 | 24 | public override async Task OpenAsync(System.Threading.CancellationToken ct) 25 | { 26 | await base.OpenAsync(ct); 27 | 28 | this.SslStream = CreateSslStream(base.networkStream); 29 | 30 | await this.SslStream.AuthenticateAsClientAsync(this.TargetHost); 31 | } 32 | 33 | protected virtual SslStream CreateSslStream(Stream innerStream) 34 | { 35 | return new SslStream(base.networkStream, leaveInnerStreamOpen: true); 36 | } 37 | 38 | protected override Stream GetWriteStream() 39 | { 40 | return this.SslStream; 41 | } 42 | 43 | protected override Stream GetReadStream() 44 | { 45 | return this.SslStream; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Switchboard.Server.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Switchboard.Server.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Switchboard.Server.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("c7cf0bed-0f36-49a6-8832-42db83d30242")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /Samples/Switchboard.ConsoleHost/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Switchboard.ConsoleHost")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Switchboard.ConsoleHost")] 13 | [assembly: AssemblyCopyright("Copyright © 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("dda8a16f-5d87-4287-bcba-2488a7ddb9b8")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /Switchboard.Server/Connection/SecureInboundConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Security; 6 | using System.Net.Sockets; 7 | using System.Security.Cryptography.X509Certificates; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace Switchboard.Server.Connection 12 | { 13 | public class SecureInboundConnection : InboundConnection 14 | { 15 | private X509Certificate certificate; 16 | protected SslStream SslStream { get; private set; } 17 | public override bool IsSecure { get { return true; } } 18 | 19 | public SecureInboundConnection(TcpClient client, X509Certificate certificate) 20 | : base(client) 21 | { 22 | this.certificate = certificate; 23 | } 24 | 25 | public override async Task OpenAsync(System.Threading.CancellationToken ct) 26 | { 27 | await base.OpenAsync(ct); 28 | 29 | this.SslStream = CreateSslStream(base.networkStream); 30 | 31 | await this.SslStream.AuthenticateAsServerAsync(certificate); 32 | } 33 | 34 | protected virtual SslStream CreateSslStream(Stream innerStream) 35 | { 36 | return new SslStream(base.networkStream, leaveInnerStreamOpen: true); 37 | } 38 | 39 | protected override Stream GetWriteStream() 40 | { 41 | return this.SslStream; 42 | } 43 | 44 | protected override Stream GetReadStream() 45 | { 46 | return this.SslStream; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Switchboard.Server/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Switchboard.Server")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Switchboard.Server")] 13 | [assembly: AssemblyCopyright("Copyright © 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("86a32763-b9af-49e0-8e79-62e03b90c238")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | [assembly: InternalsVisibleTo("Switchboard.Server.Tests")] -------------------------------------------------------------------------------- /Samples/Switchboard.ConsoleHost/Logging/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace Switchboard.ConsoleHost.Logging 6 | { 7 | /// 8 | /// Prints debug messages straight to the console. Will color a message 9 | /// based on IP-address and port if it contains one. 10 | /// 11 | public class ConsoleLogger : TraceListener 12 | { 13 | private readonly object syncRoot = new object(); 14 | private static Regex ipPortRe = new Regex(@"(?:[0-9]{1,3}\.){3}[0-9]{1,3}:\d{1,5}"); 15 | 16 | public override void Write(string message) 17 | { 18 | lock (syncRoot) 19 | System.Console.Write(message); 20 | } 21 | 22 | public override void WriteLine(string message) 23 | { 24 | var m = ipPortRe.Match(message); 25 | 26 | if (m.Success) 27 | { 28 | uint h = (uint)m.Value.GetHashCode(); 29 | ConsoleColor c; 30 | 31 | do 32 | { 33 | c = (ConsoleColor)(h % 16); 34 | h++; 35 | } while (c == ConsoleColor.Black || c == ConsoleColor.Gray); 36 | 37 | lock (syncRoot) 38 | { 39 | var current = System.Console.ForegroundColor; 40 | Console.ForegroundColor = c; 41 | Console.WriteLine(message); 42 | Console.ForegroundColor = current; 43 | } 44 | 45 | return; 46 | } 47 | 48 | lock (syncRoot) 49 | { 50 | System.Console.WriteLine(message); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Switchboard.Server/Response/SwitchboardResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Switchboard.Server 8 | { 9 | public class SwitchboardResponse 10 | { 11 | private static long responseCounter; 12 | 13 | public long ResponseId { get; private set; } 14 | 15 | public Version ProtocolVersion { get; set; } 16 | 17 | public string Method { get; set; } 18 | 19 | public WebHeaderCollection Headers { get; set; } 20 | 21 | public Uri RequestUri { get; set; } 22 | 23 | public Stream ResponseBody { get; set; } 24 | 25 | public bool IsResponseBuffered { get; private set; } 26 | 27 | static SwitchboardResponse() 28 | { 29 | } 30 | 31 | public SwitchboardResponse() 32 | { 33 | this.Headers = new WebHeaderCollection(); 34 | this.ResponseId = Interlocked.Increment(ref responseCounter); 35 | } 36 | 37 | public object StatusCode { get; set; } 38 | 39 | public object StatusDescription { get; set; } 40 | 41 | public int ContentLength 42 | { 43 | get 44 | { 45 | var clHeader = Headers.Get("Content-Length"); 46 | 47 | if (clHeader == null) 48 | return 0; 49 | 50 | int cl; 51 | 52 | if (!int.TryParse(clHeader, out cl)) 53 | return 0; 54 | 55 | return cl; 56 | } 57 | } 58 | 59 | public async Task BufferResponseAsync() 60 | { 61 | if (IsResponseBuffered) 62 | return; 63 | 64 | if (this.ResponseBody == null) 65 | return; 66 | 67 | var ms = new MemoryStream(); 68 | 69 | await this.ResponseBody.CopyToAsync(ms); 70 | 71 | this.ResponseBody = ms; 72 | ms.Seek(0, SeekOrigin.Begin); 73 | 74 | this.IsResponseBuffered = true; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Switchboard.Server/Request/SwitchboardRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Switchboard.Server 8 | { 9 | public class SwitchboardRequest 10 | { 11 | private static long requestCounter; 12 | 13 | public long RequestId { get; private set; } 14 | 15 | public Version ProtocolVersion { get; set; } 16 | 17 | public string Method { get; set; } 18 | 19 | public WebHeaderCollection Headers { get; set; } 20 | 21 | public string RequestUri { get; set; } 22 | 23 | public Stream RequestBody { get; set; } 24 | public bool IsRequestBuffered { get; private set; } 25 | 26 | public int ContentLength 27 | { 28 | get 29 | { 30 | var clHeader = Headers.Get("Content-Length"); 31 | 32 | if (clHeader == null) 33 | return 0; 34 | 35 | int cl; 36 | 37 | if (!int.TryParse(clHeader, out cl)) 38 | return 0; 39 | 40 | return cl; 41 | } 42 | } 43 | 44 | public SwitchboardRequest() 45 | { 46 | this.Headers = new WebHeaderCollection(); 47 | this.RequestId = Interlocked.Increment(ref requestCounter); 48 | } 49 | 50 | public async Task CloseAsync() 51 | { 52 | if (this.ContentLength > 0 && this.RequestBody != null && this.RequestBody.CanRead) 53 | { 54 | var buf = new byte[8192]; 55 | 56 | int c; 57 | 58 | while ((c = await this.RequestBody.ReadAsync(buf, 0, buf.Length).ConfigureAwait(false)) > 0) 59 | continue; 60 | } 61 | } 62 | 63 | public async Task BufferRequestAsync() 64 | { 65 | if (IsRequestBuffered) 66 | return; 67 | 68 | if (this.RequestBody == null) 69 | return; 70 | 71 | var ms = new MemoryStream(); 72 | 73 | await this.RequestBody.CopyToAsync(ms); 74 | 75 | this.RequestBody = ms; 76 | ms.Seek(0, SeekOrigin.Begin); 77 | 78 | this.IsRequestBuffered = true; 79 | } 80 | 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Switchboard.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Switchboard.Server", "Switchboard.Server\Switchboard.Server.csproj", "{29ECD129-85AD-417A-A690-FDEE2C97AFEA}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Switchboard.Server.Tests", "Switchboard.Server.Tests\Switchboard.Server.Tests.csproj", "{985186E3-E8F0-485E-8F3C-855DFD31279E}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{C869D716-6957-4E98-9967-35C0BCE4FB6D}" 9 | ProjectSection(SolutionItems) = preProject 10 | .nuget\NuGet.exe = .nuget\NuGet.exe 11 | .nuget\NuGet.targets = .nuget\NuGet.targets 12 | EndProjectSection 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Switchboard.ConsoleHost", "Samples\Switchboard.ConsoleHost\Switchboard.ConsoleHost.csproj", "{3BF99ED4-6966-4B2B-BFCB-DB69B39D999A}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {29ECD129-85AD-417A-A690-FDEE2C97AFEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {29ECD129-85AD-417A-A690-FDEE2C97AFEA}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {29ECD129-85AD-417A-A690-FDEE2C97AFEA}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {29ECD129-85AD-417A-A690-FDEE2C97AFEA}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {3BF99ED4-6966-4B2B-BFCB-DB69B39D999A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {3BF99ED4-6966-4B2B-BFCB-DB69B39D999A}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {3BF99ED4-6966-4B2B-BFCB-DB69B39D999A}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {3BF99ED4-6966-4B2B-BFCB-DB69B39D999A}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {985186E3-E8F0-485E-8F3C-855DFD31279E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {985186E3-E8F0-485E-8F3C-855DFD31279E}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {985186E3-E8F0-485E-8F3C-855DFD31279E}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {985186E3-E8F0-485E-8F3C-855DFD31279E}.Release|Any CPU.Build.0 = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(SolutionProperties) = preSolution 36 | HideSolutionNode = FALSE 37 | EndGlobalSection 38 | EndGlobal 39 | -------------------------------------------------------------------------------- /Samples/Switchboard.ConsoleHost/Switchboard.ConsoleHost.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {3BF99ED4-6966-4B2B-BFCB-DB69B39D999A} 8 | Exe 9 | Properties 10 | Switchboard.ConsoleHost 11 | Switchboard.ConsoleHost 12 | v4.5 13 | 512 14 | 15 | 16 | AnyCPU 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | {29ecd129-85ad-417a-a690-fdee2c97afea} 55 | Switchboard.Server 56 | 57 | 58 | 59 | 66 | -------------------------------------------------------------------------------- /Samples/Switchboard.ConsoleHost/SimpleReverseProxyHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | using Switchboard.Server; 6 | 7 | namespace Switchboard.ConsoleHost 8 | { 9 | /// 10 | /// Sample implementation of a reverse proxy. Streams requests and responses (no buffering). 11 | /// No support for location header rewriting. 12 | /// 13 | public class SimpleReverseProxyHandler : ISwitchboardRequestHandler 14 | { 15 | private Uri backendUri; 16 | 17 | public bool RewriteHost { get; set; } 18 | public bool AddForwardedForHeader { get; set; } 19 | 20 | public SimpleReverseProxyHandler(string backendUri) 21 | : this(new Uri(backendUri)) 22 | { 23 | } 24 | 25 | public SimpleReverseProxyHandler(Uri backendUri) 26 | { 27 | this.backendUri = backendUri; 28 | 29 | this.RewriteHost = true; 30 | this.AddForwardedForHeader = true; 31 | } 32 | 33 | public async Task GetResponseAsync(SwitchboardContext context, SwitchboardRequest request) 34 | { 35 | var originalHost = request.Headers["Host"]; 36 | 37 | if (this.RewriteHost) 38 | request.Headers["Host"] = this.backendUri.Host + (this.backendUri.IsDefaultPort ? string.Empty : ":" + this.backendUri.Port); 39 | 40 | if (this.AddForwardedForHeader) 41 | SetForwardedForHeader(context, request); 42 | 43 | var sw = Stopwatch.StartNew(); 44 | 45 | IPAddress ip; 46 | 47 | if(this.backendUri.HostNameType == UriHostNameType.IPv4) { 48 | ip = IPAddress.Parse(this.backendUri.Host); 49 | } 50 | else { 51 | var ipAddresses = await Dns.GetHostAddressesAsync(this.backendUri.Host); 52 | ip = ipAddresses[0]; 53 | } 54 | 55 | var backendEp = new IPEndPoint(ip, this.backendUri.Port); 56 | 57 | Debug.WriteLine("{0}: Resolved upstream server to {1} in {2}ms, opening connection", context.InboundConnection.RemoteEndPoint, backendEp, sw.Elapsed.TotalMilliseconds); 58 | 59 | if (this.backendUri.Scheme != "https") 60 | await context.OpenOutboundConnectionAsync(backendEp); 61 | else 62 | await context.OpenSecureOutboundConnectionAsync(backendEp, this.backendUri.Host); 63 | 64 | Debug.WriteLine("{0}: Outbound connection established, sending request", context.InboundConnection.RemoteEndPoint); 65 | sw.Restart(); 66 | await context.OutboundConnection.WriteRequestAsync(request); 67 | Debug.WriteLine("{0}: Handler sent request in {1}ms", context.InboundConnection.RemoteEndPoint, sw.Elapsed.TotalMilliseconds); 68 | 69 | var response = await context.OutboundConnection.ReadResponseAsync(); 70 | 71 | return response; 72 | } 73 | 74 | private void SetForwardedForHeader(SwitchboardContext context, SwitchboardRequest request) 75 | { 76 | string remoteAddress = context.InboundConnection.RemoteEndPoint.Address.ToString(); 77 | string currentForwardedFor = request.Headers["X-Forwarded-For"]; 78 | 79 | request.Headers["X-Forwarded-For"] = string.IsNullOrEmpty(currentForwardedFor) ? remoteAddress : currentForwardedFor + ", " + remoteAddress; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Switchboard.Server/Connection/OutboundConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Net; 5 | using System.Net.Sockets; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace Switchboard.Server.Connection 11 | { 12 | public class OutboundConnection : SwitchboardConnection 13 | { 14 | protected static readonly Encoding headerEncoding = Encoding.GetEncoding("us-ascii"); 15 | 16 | public IPEndPoint RemoteEndPoint { get; private set; } 17 | public override bool IsSecure { get { return false; } } 18 | 19 | public bool IsConnected 20 | { 21 | get { return connection.Connected; } 22 | } 23 | 24 | protected TcpClient connection; 25 | protected NetworkStream networkStream; 26 | 27 | public OutboundConnection(IPEndPoint endPoint) 28 | { 29 | this.RemoteEndPoint = endPoint; 30 | this.connection = new TcpClient(); 31 | } 32 | 33 | public Task OpenAsync() 34 | { 35 | return OpenAsync(CancellationToken.None); 36 | } 37 | 38 | public virtual async Task OpenAsync(CancellationToken ct) 39 | { 40 | ct.ThrowIfCancellationRequested(); 41 | 42 | await this.connection.ConnectAsync(this.RemoteEndPoint.Address, this.RemoteEndPoint.Port); 43 | 44 | this.networkStream = this.connection.GetStream(); 45 | } 46 | 47 | public Task WriteRequestAsync(SwitchboardRequest request) 48 | { 49 | return WriteRequestAsync(request, CancellationToken.None); 50 | } 51 | 52 | public async Task WriteRequestAsync(SwitchboardRequest request, CancellationToken ct) 53 | { 54 | var writeStream = this.GetWriteStream(); 55 | 56 | var ms = new MemoryStream(); 57 | var sw = new StreamWriter(ms, headerEncoding); 58 | 59 | sw.NewLine = "\r\n"; 60 | sw.WriteLine("{0} {1} HTTP/1.{2}", request.Method, request.RequestUri, request.ProtocolVersion.Minor); 61 | 62 | for (int i = 0; i < request.Headers.Count; i++) 63 | sw.WriteLine("{0}: {1}", request.Headers.GetKey(i), request.Headers.Get(i)); 64 | 65 | sw.WriteLine(); 66 | sw.Flush(); 67 | 68 | await writeStream.WriteAsync(ms.GetBuffer(), 0, (int)ms.Length) 69 | .ConfigureAwait(continueOnCapturedContext: false); 70 | 71 | if (request.RequestBody != null) 72 | { 73 | await request.RequestBody.CopyToAsync(writeStream) 74 | .ConfigureAwait(continueOnCapturedContext: false); 75 | } 76 | 77 | await writeStream.FlushAsync() 78 | .ConfigureAwait(continueOnCapturedContext: false); 79 | } 80 | 81 | protected virtual Stream GetWriteStream() 82 | { 83 | return this.networkStream; 84 | } 85 | 86 | protected virtual Stream GetReadStream() 87 | { 88 | return this.networkStream; 89 | } 90 | 91 | public Task ReadResponseAsync() 92 | { 93 | var parser = new SwitchboardResponseParser(); 94 | return parser.ParseAsync(this.GetReadStream()); 95 | } 96 | 97 | public void Close() 98 | { 99 | connection.Close(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Switchboard.Server/Request/SwitchboardRequestParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using HttpMachine; 6 | using Switchboard.Server.Connection; 7 | using Switchboard.Server.Utils; 8 | 9 | namespace Switchboard.Server 10 | { 11 | internal class SwitchboardRequestParser 12 | { 13 | private sealed class ParseDelegate : IHttpParserHandler 14 | { 15 | private string headerName; 16 | 17 | public SwitchboardRequest request = new SwitchboardRequest(); 18 | public ArraySegment requestBodyStart; 19 | public bool complete; 20 | public bool headerComplete; 21 | 22 | void IHttpParserHandler.OnBody(HttpParser parser, ArraySegment data) { requestBodyStart = data; } 23 | void IHttpParserHandler.OnFragment(HttpParser parser, string fragment) { } 24 | void IHttpParserHandler.OnHeaderName(HttpParser parser, string name) { headerName = name; } 25 | void IHttpParserHandler.OnHeaderValue(HttpParser parser, string value) { request.Headers.Add(headerName, value); } 26 | void IHttpParserHandler.OnHeadersEnd(HttpParser parser) { this.headerComplete = true; } 27 | void IHttpParserHandler.OnMessageBegin(HttpParser parser) { } 28 | void IHttpParserHandler.OnMessageEnd(HttpParser parser) { this.complete = true; } 29 | void IHttpParserHandler.OnMethod(HttpParser parser, string method) { request.Method = method; } 30 | void IHttpParserHandler.OnQueryString(HttpParser parser, string queryString) { } 31 | void IHttpParserHandler.OnRequestUri(HttpParser parser, string requestUri) { request.RequestUri = requestUri; } 32 | } 33 | 34 | public SwitchboardRequestParser() 35 | { 36 | } 37 | 38 | public async Task ParseAsync(InboundConnection conn, Stream stream) 39 | { 40 | var del = new ParseDelegate(); 41 | var parser = new HttpParser(del); 42 | 43 | int read; 44 | int readTotal = 0; 45 | byte[] buffer = new byte[8192]; 46 | 47 | Debug.WriteLine(string.Format("{0}: RequestParser starting", conn.RemoteEndPoint)); 48 | 49 | while ((read = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) 50 | { 51 | readTotal += read; 52 | 53 | if (parser.Execute(new ArraySegment(buffer, 0, read)) != read) 54 | throw new FormatException("Parse error in request"); 55 | 56 | if (del.headerComplete) 57 | break; 58 | } 59 | 60 | Debug.WriteLine(string.Format("{0}: RequestParser read enough ({1} bytes)", conn.RemoteEndPoint, readTotal)); 61 | 62 | if (readTotal == 0) 63 | return null; 64 | 65 | if (!del.headerComplete) 66 | throw new FormatException("Parse error in request"); 67 | 68 | var request = del.request; 69 | 70 | request.ProtocolVersion = new Version(parser.MajorVersion, parser.MinorVersion); 71 | 72 | int cl = request.ContentLength; 73 | 74 | if (cl > 0) 75 | { 76 | if (del.requestBodyStart.Count > 0) 77 | { 78 | request.RequestBody = new MaxReadStream(new StartAvailableStream(del.requestBodyStart, stream), cl); 79 | } 80 | else 81 | { 82 | request.RequestBody = new MaxReadStream(stream, cl); 83 | } 84 | } 85 | 86 | return request; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Switchboard.Server/Server/SwitchboardServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Switchboard.Server.Connection; 8 | 9 | namespace Switchboard.Server 10 | { 11 | public class SwitchboardServer 12 | { 13 | private ISwitchboardRequestHandler handler; 14 | private TcpListener server; 15 | private Task workTask; 16 | private bool stopping; 17 | private Timer connectivityTimer; 18 | 19 | public SwitchboardServer(IPEndPoint listenEp, ISwitchboardRequestHandler handler) 20 | { 21 | this.server = new TcpListener(listenEp); 22 | this.handler = handler; 23 | } 24 | 25 | public void Start() 26 | { 27 | this.server.Start(); 28 | this.workTask = Run(CancellationToken.None); 29 | } 30 | 31 | private async Task Run(CancellationToken ct) 32 | { 33 | while (!ct.IsCancellationRequested) 34 | { 35 | var client = await this.server.AcceptTcpClientAsync(); 36 | 37 | var inbound = await CreateInboundConnection(client); 38 | await inbound.OpenAsync(); 39 | 40 | Debug.WriteLine(string.Format("{0}: Connected", inbound.RemoteEndPoint)); 41 | 42 | var context = new SwitchboardContext(inbound); 43 | 44 | HandleSession(context); 45 | } 46 | } 47 | 48 | protected virtual Task CreateInboundConnection(TcpClient client) 49 | { 50 | return Task.FromResult(new InboundConnection(client)); 51 | } 52 | 53 | private async void HandleSession(SwitchboardContext context) 54 | { 55 | try 56 | { 57 | Debug.WriteLine("{0}: Starting session", context.InboundConnection.RemoteEndPoint); 58 | 59 | do 60 | { 61 | var request = await context.InboundConnection.ReadRequestAsync().ConfigureAwait(false); 62 | 63 | if (request == null) 64 | return; 65 | 66 | Debug.WriteLine(string.Format("{0}: Got {1} request for {2}", context.InboundConnection.RemoteEndPoint, request.Method, request.RequestUri)); 67 | 68 | var response = await handler.GetResponseAsync(context, request).ConfigureAwait(false); 69 | Debug.WriteLine(string.Format("{0}: Got response from handler ({1})", context.InboundConnection.RemoteEndPoint, response.StatusCode)); 70 | 71 | await context.InboundConnection.WriteResponseAsync(response).ConfigureAwait(false); 72 | Debug.WriteLine(string.Format("{0}: Wrote response to client", context.InboundConnection.RemoteEndPoint)); 73 | 74 | if (context.OutboundConnection != null && !context.OutboundConnection.IsConnected) 75 | context.Close(); 76 | 77 | } while (context.InboundConnection.IsConnected); 78 | } 79 | catch (Exception exc) 80 | { 81 | Debug.WriteLine(string.Format("{0}: Error: {1}", context.InboundConnection.RemoteEndPoint, exc.Message)); 82 | context.Close(); 83 | Debug.WriteLine(string.Format("{0}: Closed context", context.InboundConnection.RemoteEndPoint, exc.Message)); 84 | } 85 | finally 86 | { 87 | context.Dispose(); 88 | } 89 | } 90 | 91 | private Task AcceptOneClient() 92 | { 93 | throw new NotImplementedException(); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Switchboard # 2 | 3 | Fully asynchronous C# 5 / .NET4.5 HTTP intermediary server. 4 | 5 | Uses [HttpMachine](https://github.com/bvanderveen/httpmachine) for parsing incoming HTTP requests and a naive custom built response parser. 6 | 7 | Supports SSL for inbound and outbound connections. 8 | 9 | ## Uses/Why? ## 10 | 11 | I wrote it cause I needed to transparently manipulate requests going from one application to a web service. The application made Http requests to a server and I needed a quick-fix solution for tweaking the requests/responses without either end knowing about it. Since the requests could potentially be rather large I decided it would be a good time to dig into the async goodness in C# 5 and make the middle man server fully asynchronous. 12 | 13 | The hack evolved until it had a life of it's own so I'm putting it out there in case someone has similar problems. 14 | 15 | ## Is it a web server? 16 | 17 | TL;DR: no 18 | 19 | It's more than capable of parsing requests and generating responses so there's nothing stopping you from using it as a stand-alone web server. But the primary use case is to read requests and (with or without modification) send them to a proper web server and deliver the response back. 20 | 21 | As such Switchboard can rely on a competent fully capable http server such as IIS, lighttpd or Apache to deal with all the intricacies of the HTTP protocol and deal with shuffling data and tweaking requests and responses. 22 | 23 | ### Potential uses 24 | The lib is still really early in development and it's lacking in several aspects but here's some potential _future_ use cases. 25 | 26 | * Load balancing/reverse proxy 27 | * Reverse proxy with cache (coupled with a good cache provider) 28 | * In flight message logging for web services either for temporary debugging or more permanent logging when there's zero or little control over the endpoints. 29 | * AJAX proxy 30 | 31 | ### Notes/TODO ### 32 | 33 | There are CancellationTokens sprinkled throughout but they won't do any smart cancellation as of yet. 34 | 35 | There's currently no proper logging support, only the debug log. 36 | 37 | No timeout support for connections which never gets around to making a request. 38 | 39 | The original purpose of Switchboard was to run in a friendly environment. Security hardening is planned but for now it's probably not suited environments facing malicious requests/responses. This is especially true for malicious responses since we currently have our own parser for that. 40 | 41 | Future improvment: Ability to establish outbound connection immediately after 42 | inbound connection is established (before request is read) 43 | thread safe openoutboundconnection 44 | 45 | Chunked transfer support is currently limited. It works great for streaming but there's no support for merging chunks into a coherent response. Beware. 46 | 47 | Documentation is severely lacking. 48 | 49 | ## License ## 50 | 51 | Licensed under the MIT License 52 | 53 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 54 | 55 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 56 | 57 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Switchboard.Server/Response/SwitchboardResponseParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Switchboard.Server.Utils; 5 | using Switchboard.Server.Utils.HttpParser; 6 | 7 | namespace Switchboard.Server 8 | { 9 | internal class SwitchboardResponseParser 10 | { 11 | private sealed class ParseDelegate : IHttpResponseHandler 12 | { 13 | public bool headerComplete; 14 | public SwitchboardResponse response = new SwitchboardResponse(); 15 | public ArraySegment responseBodyStart; 16 | 17 | public void OnResponseBegin() { } 18 | 19 | public void OnStatusLine(Version protocolVersion, int statusCode, string statusDescription) 20 | { 21 | response.ProtocolVersion = protocolVersion; 22 | response.StatusCode = statusCode; 23 | response.StatusDescription = statusDescription; 24 | } 25 | 26 | public void OnHeader(string name, string value) 27 | { 28 | response.Headers.Add(name, value); 29 | } 30 | 31 | public void OnEntityStart() 32 | { 33 | } 34 | 35 | public void OnHeadersEnd() 36 | { 37 | this.headerComplete = true; 38 | } 39 | 40 | public void OnEntityData(byte[] buffer, int offset, int count) 41 | { 42 | this.responseBodyStart = new ArraySegment(buffer, offset, count); 43 | } 44 | 45 | public void OnEntityEnd() 46 | { 47 | } 48 | 49 | public void OnResponseEnd() 50 | { 51 | } 52 | } 53 | 54 | public SwitchboardResponseParser() 55 | { 56 | } 57 | 58 | public async Task ParseAsync(Stream stream) 59 | { 60 | var del = new ParseDelegate(); 61 | var parser = new HttpResponseParser(del); 62 | 63 | int read; 64 | byte[] buffer = new byte[8192]; 65 | 66 | while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0) 67 | { 68 | parser.Execute(buffer, 0, read); 69 | 70 | if (del.headerComplete) 71 | break; 72 | } 73 | 74 | if (!del.headerComplete) 75 | throw new FormatException("Parse error in response"); 76 | 77 | var response = del.response; 78 | int cl = response.ContentLength; 79 | 80 | if (cl > 0) 81 | { 82 | if (del.responseBodyStart.Count > 0) 83 | { 84 | response.ResponseBody = new MaxReadStream(new StartAvailableStream(del.responseBodyStart, stream), cl); 85 | } 86 | else 87 | { 88 | response.ResponseBody = new MaxReadStream(stream, cl); 89 | } 90 | } 91 | else if (response.Headers["Transfer-Encoding"] == "chunked") 92 | { 93 | if (response.Headers["Connection"] == "close") 94 | { 95 | if (del.responseBodyStart.Count > 0) 96 | { 97 | response.ResponseBody = new StartAvailableStream(del.responseBodyStart, stream); 98 | } 99 | else 100 | { 101 | response.ResponseBody = stream; 102 | } 103 | } 104 | else 105 | { 106 | if (del.responseBodyStart.Count > 0) 107 | { 108 | response.ResponseBody = new ChunkedStream(new StartAvailableStream(del.responseBodyStart, stream)); 109 | } 110 | else 111 | { 112 | response.ResponseBody = new ChunkedStream(stream); 113 | } 114 | } 115 | } 116 | 117 | return response; 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Switchboard.Server/Switchboard.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {29ECD129-85AD-417A-A690-FDEE2C97AFEA} 8 | Library 9 | Properties 10 | Switchboard.Server 11 | Switchboard.Server 12 | v4.5 13 | 512 14 | ..\ 15 | true 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | ..\packages\HttpMachine.0.9.0.0\lib\HttpMachine.dll 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 83 | -------------------------------------------------------------------------------- /Switchboard.Server/Utils/MaxReadStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Switchboard.Server.Utils 7 | { 8 | /// 9 | /// Simple wrapping stream which prevents reading more than the specified maximum length. 10 | /// Also prevents seeking. Support sync and async reads. 11 | /// 12 | internal class MaxReadStream : RedirectingStream 13 | { 14 | private class EmptyAsyncResult : IAsyncResult 15 | { 16 | public object AsyncState { get; set; } 17 | public WaitHandle AsyncWaitHandle { get; set; } 18 | public bool CompletedSynchronously { get { return true; } } 19 | public bool IsCompleted { get { return true; } } 20 | } 21 | 22 | int read = 0; 23 | int maxLength; 24 | 25 | private int Left { get { return maxLength - read; } } 26 | 27 | public MaxReadStream(Stream innerStream, int maxLength) 28 | : base(innerStream) 29 | { 30 | this.maxLength = maxLength; 31 | } 32 | 33 | public override int Read(byte[] buffer, int offset, int count) 34 | { 35 | int left = this.Left; 36 | 37 | if (left <= 0) 38 | return 0; 39 | 40 | if (count > left) 41 | count = left; 42 | 43 | int c = base.Read(buffer, offset, count); 44 | read += c; 45 | 46 | return c; 47 | } 48 | 49 | public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) 50 | { 51 | int left = this.Left; 52 | 53 | if (left <= 0) 54 | { 55 | var ar = new EmptyAsyncResult(); 56 | ar.AsyncState = state; 57 | ar.AsyncWaitHandle = new ManualResetEvent(true); 58 | 59 | callback(ar); 60 | return ar; 61 | } 62 | 63 | if (count > left) 64 | count = left; 65 | 66 | return base.BeginRead(buffer, offset, count, callback, state); 67 | } 68 | 69 | public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 70 | { 71 | int left = this.Left; 72 | 73 | if (left <= 0) 74 | return 0; 75 | 76 | if (count > left) 77 | count = left; 78 | 79 | int c = await base.ReadAsync(buffer, offset, count, cancellationToken); 80 | 81 | this.read += c; 82 | 83 | return c; 84 | } 85 | 86 | public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) 87 | { 88 | byte[] buffer = new byte[bufferSize]; 89 | 90 | int c; 91 | 92 | while ((c = await this.ReadAsync(buffer, 0, buffer.Length)) > 0) 93 | { 94 | cancellationToken.ThrowIfCancellationRequested(); 95 | await destination.WriteAsync(buffer, 0, c); 96 | cancellationToken.ThrowIfCancellationRequested(); 97 | } 98 | 99 | } 100 | 101 | public override bool CanRead 102 | { 103 | get 104 | { 105 | return this.Left > 0; 106 | } 107 | } 108 | 109 | public override int EndRead(IAsyncResult asyncResult) 110 | { 111 | if (asyncResult is EmptyAsyncResult) 112 | return 0; 113 | 114 | int c = base.EndRead(asyncResult); 115 | read += c; 116 | 117 | return c; 118 | } 119 | 120 | public override int ReadByte() 121 | { 122 | if (Left > 0) 123 | { 124 | read++; 125 | return base.ReadByte(); 126 | } 127 | else 128 | { 129 | throw new EndOfStreamException(); 130 | } 131 | } 132 | 133 | public override bool CanSeek 134 | { 135 | get 136 | { 137 | return false; 138 | } 139 | } 140 | 141 | public override long Seek(long offset, SeekOrigin origin) 142 | { 143 | throw new NotSupportedException(); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Switchboard.Server/Utils/StartAvailableStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Switchboard.Server.Utils 10 | { 11 | /// 12 | /// Used internally by request/response parsers to merge a piece of a buffer and the 13 | /// rest of the request/response stream. 14 | /// 15 | internal class StartAvailableStream : Stream 16 | { 17 | private Stream stream; 18 | 19 | private bool inStream; 20 | private MemoryStream buffer; 21 | 22 | public StartAvailableStream(ArraySegment startBuffer, Stream continuationStream) 23 | : this(startBuffer.Array, startBuffer.Offset, startBuffer.Count, continuationStream) 24 | { 25 | } 26 | 27 | public StartAvailableStream(byte[] startBuffer, int offset, int count, Stream continuationStream) 28 | { 29 | this.buffer = new MemoryStream(startBuffer, offset, count); 30 | this.stream = continuationStream; 31 | } 32 | 33 | public override bool CanRead 34 | { 35 | get { return !inStream || stream.CanRead; } 36 | } 37 | 38 | public override bool CanSeek 39 | { 40 | get { return false; } 41 | } 42 | 43 | public override bool CanWrite 44 | { 45 | get { return false; } 46 | } 47 | 48 | public override void Flush() 49 | { 50 | } 51 | 52 | public override Task FlushAsync(CancellationToken cancellationToken) 53 | { 54 | return Task.FromResult(default(VoidTypeStruct)); 55 | } 56 | 57 | public override long Length 58 | { 59 | get { return this.buffer.Length + this.stream.Length; } 60 | } 61 | 62 | public override long Position 63 | { 64 | get 65 | { 66 | throw new NotSupportedException(); 67 | } 68 | set 69 | { 70 | throw new NotSupportedException(); 71 | } 72 | } 73 | 74 | public override int Read(byte[] buffer, int offset, int count) 75 | { 76 | if (!inStream && this.buffer.Position == this.buffer.Length) 77 | inStream = true; 78 | 79 | if (inStream) 80 | return this.stream.Read(buffer, offset, count); 81 | else 82 | return this.buffer.Read(buffer, offset, count); 83 | } 84 | 85 | public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) 86 | { 87 | if (!inStream && this.buffer.Position == this.buffer.Length) 88 | inStream = true; 89 | 90 | if (inStream) 91 | { 92 | return stream.BeginRead(buffer, offset, count, callback, state); 93 | } 94 | else 95 | { 96 | return this.buffer.BeginRead(buffer, offset, count, callback, state); 97 | } 98 | } 99 | 100 | public override int EndRead(IAsyncResult asyncResult) 101 | { 102 | if (inStream) 103 | { 104 | return stream.EndRead(asyncResult); 105 | } 106 | else 107 | { 108 | return this.buffer.EndRead(asyncResult); 109 | } 110 | } 111 | 112 | public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 113 | { 114 | if (!inStream && this.buffer.Position == this.buffer.Length) 115 | inStream = true; 116 | 117 | if (inStream) 118 | return stream.ReadAsync(buffer, offset, count); 119 | else 120 | return this.buffer.ReadAsync(buffer, offset, count); 121 | } 122 | 123 | public override long Seek(long offset, SeekOrigin origin) 124 | { 125 | throw new NotSupportedException(); 126 | } 127 | 128 | public override void SetLength(long value) 129 | { 130 | throw new NotSupportedException(); 131 | } 132 | 133 | public override void Write(byte[] buffer, int offset, int count) 134 | { 135 | throw new NotSupportedException(); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Switchboard.Server/Context/SwitchboardContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Net; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Switchboard.Server.Connection; 7 | 8 | namespace Switchboard.Server 9 | { 10 | public class SwitchboardContext 11 | { 12 | private static long contextCounter; 13 | public long ContextId { get; private set; } 14 | 15 | public InboundConnection InboundConnection { get; private set; } 16 | public OutboundConnection OutboundConnection { get; private set; } 17 | 18 | private Timer CheckForDisconnectTimer; 19 | 20 | public SwitchboardContext(InboundConnection client) 21 | { 22 | this.InboundConnection = client; 23 | this.ContextId = Interlocked.Increment(ref contextCounter); 24 | this.CheckForDisconnectTimer = new Timer(CheckForDisconnect, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); 25 | } 26 | 27 | private void CheckForDisconnect(object state) 28 | { 29 | // TODO 30 | } 31 | 32 | public Task OpenSecureOutboundConnectionAsync(IPEndPoint endPoint, string targetHost) 33 | { 34 | return OpenOutboundConnectionAsync(endPoint, true, (ep) => new SecureOutboundConnection(targetHost, ep)); 35 | } 36 | 37 | public Task OpenOutboundConnectionAsync(IPEndPoint endPoint) 38 | { 39 | return OpenOutboundConnectionAsync(endPoint, false, (ep) => new OutboundConnection(ep)); 40 | } 41 | 42 | private async Task OpenOutboundConnectionAsync(IPEndPoint endPoint, bool secure, Func connectionFactory) where T: OutboundConnection 43 | { 44 | if (this.OutboundConnection != null) 45 | { 46 | if (!this.OutboundConnection.RemoteEndPoint.Equals(endPoint)) 47 | { 48 | Debug.WriteLine("{0}: Current outbound connection is for {1}, can't reuse for {2}", InboundConnection.RemoteEndPoint, this.OutboundConnection.RemoteEndPoint, endPoint); 49 | this.OutboundConnection.Close(); 50 | this.OutboundConnection = null; 51 | } 52 | else if (this.OutboundConnection.IsSecure != secure) 53 | { 54 | Debug.WriteLine("{0}: Current outbound connection {0} secure, can't reuse", InboundConnection.RemoteEndPoint, this.OutboundConnection.IsSecure ? "is" : "is not"); 55 | this.OutboundConnection.Close(); 56 | this.OutboundConnection = null; 57 | } 58 | else 59 | { 60 | if (this.OutboundConnection.IsConnected) 61 | { 62 | Debug.WriteLine("{0}: Reusing outbound connection to {1}", InboundConnection.RemoteEndPoint, this.OutboundConnection.RemoteEndPoint); 63 | return this.OutboundConnection; 64 | } 65 | else 66 | { 67 | Debug.WriteLine("{0}: Detected stale outbound connection, recreating", InboundConnection.RemoteEndPoint, this.OutboundConnection.RemoteEndPoint); 68 | this.OutboundConnection.Close(); 69 | this.OutboundConnection = null; 70 | } 71 | } 72 | } 73 | 74 | var conn = connectionFactory(endPoint); 75 | 76 | await conn.OpenAsync().ConfigureAwait(false); 77 | 78 | Debug.WriteLine("{0}: Outbound connection to {1} established", InboundConnection.RemoteEndPoint, conn.RemoteEndPoint); 79 | 80 | this.OutboundConnection = conn; 81 | 82 | return conn; 83 | } 84 | 85 | public async Task OpenOutboundConnectionAsync(Task openTask) 86 | { 87 | var conn = await openTask.ConfigureAwait(false); 88 | 89 | this.OutboundConnection = conn; 90 | 91 | return conn; 92 | } 93 | 94 | internal void Close() 95 | { 96 | if (this.InboundConnection.IsConnected) 97 | this.InboundConnection.Close(); 98 | 99 | if (this.OutboundConnection != null && this.OutboundConnection.IsConnected) 100 | this.OutboundConnection.Close(); 101 | } 102 | 103 | internal void Dispose() 104 | { 105 | this.Close(); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Switchboard.Server.Tests/Switchboard.Server.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | {985186E3-E8F0-485E-8F3C-855DFD31279E} 7 | Library 8 | Properties 9 | Switchboard.Server.Tests 10 | Switchboard.Server.Tests 11 | v4.5 12 | 512 13 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 14 | 10.0 15 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 16 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 17 | False 18 | UnitTest 19 | 20 | 21 | true 22 | full 23 | false 24 | bin\Debug\ 25 | DEBUG;TRACE 26 | prompt 27 | 4 28 | 29 | 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {29ecd129-85ad-417a-a690-fdee2c97afea} 59 | Switchboard.Server 60 | 61 | 62 | 63 | 64 | 65 | 66 | False 67 | 68 | 69 | False 70 | 71 | 72 | False 73 | 74 | 75 | False 76 | 77 | 78 | 79 | 80 | 81 | 82 | 89 | -------------------------------------------------------------------------------- /Switchboard.Server/Connection/InboundConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Net.Sockets; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Switchboard.Server.Utils; 12 | 13 | namespace Switchboard.Server.Connection 14 | { 15 | public class InboundConnection : SwitchboardConnection 16 | { 17 | private static long connectionCounter; 18 | protected static readonly Encoding headerEncoding = Encoding.GetEncoding("us-ascii"); 19 | public override bool IsSecure { get { return false; } } 20 | 21 | public long ConnectionId; 22 | 23 | protected TcpClient connection; 24 | protected NetworkStream networkStream; 25 | 26 | public bool IsConnected 27 | { 28 | get 29 | { 30 | if (!connection.Connected) 31 | return false; 32 | 33 | try 34 | { 35 | return !(connection.Client.Poll(1, SelectMode.SelectRead) && connection.Client.Available == 0); 36 | } 37 | catch (SocketException) { return false; } 38 | //return connection.Connected; 39 | } 40 | } 41 | 42 | public IPEndPoint RemoteEndPoint { get; private set; } 43 | 44 | public InboundConnection(TcpClient connection) 45 | { 46 | this.connection = connection; 47 | this.networkStream = connection.GetStream(); 48 | this.ConnectionId = Interlocked.Increment(ref connectionCounter); 49 | this.RemoteEndPoint = (IPEndPoint)connection.Client.RemoteEndPoint; 50 | } 51 | 52 | public virtual Task OpenAsync() 53 | { 54 | return this.OpenAsync(CancellationToken.None); 55 | } 56 | 57 | public virtual Task OpenAsync(CancellationToken ct) 58 | { 59 | return Task.FromResult(default(VoidTypeStruct)); 60 | } 61 | 62 | protected virtual Stream GetReadStream() 63 | { 64 | return this.networkStream; 65 | } 66 | 67 | protected virtual Stream GetWriteStream() 68 | { 69 | return this.networkStream; 70 | } 71 | 72 | 73 | public Task ReadRequestAsync() 74 | { 75 | return ReadRequestAsync(CancellationToken.None); 76 | } 77 | 78 | public Task ReadRequestAsync(CancellationToken ct) 79 | { 80 | var requestParser = new SwitchboardRequestParser(); 81 | 82 | return requestParser.ParseAsync(this, this.GetReadStream()); 83 | } 84 | 85 | public async Task WriteResponseAsync(SwitchboardResponse response) 86 | { 87 | await WriteResponseAsync(response, CancellationToken.None) 88 | .ConfigureAwait(false); 89 | } 90 | 91 | public async Task WriteResponseAsync(SwitchboardResponse response, CancellationToken ct) 92 | { 93 | var ms = new MemoryStream(); 94 | var sw = new StreamWriter(ms, headerEncoding); 95 | 96 | sw.NewLine = "\r\n"; 97 | sw.WriteLine("HTTP/{0} {1} {2}", response.ProtocolVersion, response.StatusCode, response.StatusDescription); 98 | 99 | for (int i = 0; i < response.Headers.Count; i++) 100 | sw.WriteLine("{0}: {1}", response.Headers.GetKey(i), response.Headers.Get(i)); 101 | 102 | sw.WriteLine(); 103 | sw.Flush(); 104 | 105 | var writeStream = this.GetWriteStream(); 106 | 107 | await writeStream.WriteAsync(ms.GetBuffer(), 0, (int)ms.Length).ConfigureAwait(false); 108 | Debug.WriteLine("{0}: Wrote headers ({1}b)", this.RemoteEndPoint, ms.Length); 109 | 110 | if (response.ResponseBody != null && response.ResponseBody.CanRead) 111 | { 112 | byte[] buffer = new byte[8192]; 113 | int read; 114 | long written = 0; 115 | 116 | while ((read = await response.ResponseBody.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) 117 | { 118 | written += read; 119 | Debug.WriteLine("{0}: Read {1:N0} bytes from response body", this.RemoteEndPoint, read); 120 | await writeStream.WriteAsync(buffer, 0, read).ConfigureAwait(false); 121 | Debug.WriteLine("{0}: Wrote {1:N0} bytes to client", this.RemoteEndPoint, read); 122 | } 123 | 124 | Debug.WriteLine("{0}: Wrote response body ({1:N0} bytes) to client", this.RemoteEndPoint, written); 125 | 126 | } 127 | 128 | await writeStream.FlushAsync().ConfigureAwait(false); 129 | } 130 | 131 | public void Close() 132 | { 133 | connection.Close(); 134 | } 135 | 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | false 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 27 | $([System.IO.Path]::Combine($(ProjectDir), "packages.config")) 28 | $([System.IO.Path]::Combine($(SolutionDir), "packages")) 29 | 30 | 31 | 32 | 33 | $(SolutionDir).nuget 34 | packages.config 35 | $(SolutionDir)packages 36 | 37 | 38 | 39 | 40 | $(NuGetToolsPath)\nuget.exe 41 | @(PackageSource) 42 | 43 | "$(NuGetExePath)" 44 | mono --runtime=v4.0.30319 $(NuGetExePath) 45 | 46 | $(TargetDir.Trim('\\')) 47 | 48 | 49 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" -o "$(PackagesDir)" 50 | $(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols 51 | 52 | 53 | 54 | RestorePackages; 55 | $(BuildDependsOn); 56 | 57 | 58 | 59 | 60 | $(BuildDependsOn); 61 | BuildPackage; 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /Switchboard.Server/Utils/ChunkedStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Switchboard.Server.Utils 7 | { 8 | internal class ChunkedStream : Stream 9 | { 10 | private Stream innerStream; 11 | private bool inChunkHeader = true; 12 | private int chunkHeaderPosition; 13 | private bool inChunkHeaderLength = true; 14 | private int chunkLength; 15 | private int chunkRead; 16 | private bool done; 17 | private bool inChunkTrailingCrLf; 18 | private int chunkTrailingCrLfPosition; 19 | private bool inChunk; 20 | 21 | private int chunkLeft { get { return chunkLength - chunkRead; } } 22 | 23 | public override bool CanRead { get { return !this.done; } } 24 | public override bool CanSeek { get { return false; } } 25 | public override bool CanWrite { get { return false; } } 26 | 27 | public ChunkedStream(Stream innerStream) 28 | { 29 | this.innerStream = innerStream; 30 | } 31 | 32 | public override void Flush() 33 | { 34 | } 35 | 36 | public override long Length 37 | { 38 | get { throw new NotSupportedException(); } 39 | } 40 | 41 | public override long Position 42 | { 43 | get { throw new NotImplementedException(); } 44 | set { throw new NotImplementedException(); } 45 | } 46 | 47 | public override int Read(byte[] buffer, int offset, int count) 48 | { 49 | throw new NotImplementedException(); 50 | } 51 | 52 | public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) 53 | { 54 | throw new NotImplementedException(); 55 | } 56 | 57 | public override int EndRead(IAsyncResult asyncResult) 58 | { 59 | throw new NotImplementedException(); 60 | } 61 | 62 | public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 63 | { 64 | if (this.done) 65 | return 0; 66 | 67 | count = Math.Min(OptimizeCount(count), count); 68 | 69 | int read = await this.innerStream.ReadAsync(buffer, offset, count, cancellationToken); 70 | 71 | this.Execute(buffer, offset, read); 72 | 73 | return read; 74 | } 75 | 76 | private int OptimizeCount(int count) 77 | { 78 | if (!inChunkHeader) 79 | { 80 | if (count > chunkLeft + 2) 81 | count = chunkLeft + 2; 82 | } 83 | else 84 | { 85 | if (chunkHeaderPosition == 0) 86 | count = 3; 87 | else 88 | { 89 | if (inChunkHeaderLength) 90 | count = 2; 91 | else 92 | count = chunkLength + 3; 93 | } 94 | } 95 | return count; 96 | } 97 | 98 | private void Execute(byte[] buffer, int offset, int count) 99 | { 100 | for (int i = offset; i < offset + count; i++) 101 | { 102 | if (this.done) 103 | break; 104 | 105 | byte b = buffer[i]; 106 | 107 | if (this.inChunkHeader) 108 | { 109 | for (; i < offset + count; i++) 110 | { 111 | b = buffer[i]; 112 | 113 | if (this.inChunkHeaderLength) 114 | { 115 | if (b == 13) 116 | this.inChunkHeaderLength = false; 117 | else 118 | this.chunkLength = (this.chunkLength << 4) + FromHex(b); 119 | 120 | this.chunkHeaderPosition++; 121 | } 122 | else 123 | { 124 | if (b != 10) 125 | throw new FormatException("Malformed chunk header"); 126 | 127 | this.inChunkHeader = false; 128 | this.inChunk = true; 129 | this.chunkHeaderPosition = 0; 130 | 131 | break; 132 | } 133 | } 134 | } 135 | else if (this.inChunkTrailingCrLf) 136 | { 137 | if (this.chunkTrailingCrLfPosition == 0 && b != 13 || this.chunkTrailingCrLfPosition == 1 && b != 10) 138 | throw new FormatException("Malformed chunk header"); 139 | 140 | if (this.chunkTrailingCrLfPosition == 1) 141 | { 142 | this.inChunkTrailingCrLf = false; 143 | this.chunkTrailingCrLfPosition = 0; 144 | 145 | this.inChunkHeader = true; 146 | this.inChunkHeaderLength = true; 147 | 148 | if (chunkLength == 0) 149 | this.done = true; 150 | 151 | this.chunkLength = 0; 152 | } 153 | else 154 | { 155 | this.chunkTrailingCrLfPosition++; 156 | } 157 | 158 | } 159 | else if (this.inChunk) 160 | { 161 | for (; i < offset + count; i++) 162 | { 163 | this.chunkRead++; 164 | 165 | if (chunkRead == this.chunkLength) 166 | { 167 | this.inChunk = false; 168 | this.inChunkTrailingCrLf = true; 169 | this.chunkRead = 0; 170 | break; 171 | } 172 | } 173 | } 174 | } 175 | } 176 | 177 | private int FromHex(byte b) 178 | { 179 | // 0-9 180 | if (b >= 48 && b <= 57) 181 | return b - 48; 182 | 183 | // A-F 184 | if (b >= 65 && b <= 70) 185 | return 10 + (b - 65); 186 | 187 | // a-f 188 | if (b >= 97 && b <= 102) 189 | return 10 + (b - 97); 190 | 191 | throw new FormatException("Not hex"); 192 | } 193 | 194 | public override long Seek(long offset, SeekOrigin origin) 195 | { 196 | throw new NotImplementedException(); 197 | } 198 | 199 | public override void SetLength(long value) 200 | { 201 | throw new NotImplementedException(); 202 | } 203 | 204 | public override void Write(byte[] buffer, int offset, int count) 205 | { 206 | throw new NotImplementedException(); 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /Switchboard.Server/Utils/HttpParser/HttpResponseParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace Switchboard.Server.Utils.HttpParser 7 | { 8 | /// 9 | /// Simple and naive http response parser. No support for chunked transfers or 10 | /// line folding in headers. Should probably not be used to read responses from non-friendly 11 | /// servers yet. 12 | /// 13 | /// TODO: 14 | /// * Proper chunked transfer support 15 | /// * Proper support for RFC tokens, adhere to the BNF 16 | /// * Hardening against maliciously crafted responses. 17 | /// * 18 | /// 19 | public class HttpResponseParser 20 | { 21 | private bool inEntityData; 22 | private bool inHeaders; 23 | 24 | private bool hasEntityData; 25 | 26 | private bool hasStarted; 27 | private bool isCompleted; 28 | 29 | private byte[] parseBuffer; 30 | private int parseBufferWritten; 31 | 32 | private IHttpResponseHandler handler; 33 | 34 | private int contentLength = -1; 35 | private int entityDataWritten = 0; 36 | 37 | public HttpResponseParser(IHttpResponseHandler handler) 38 | { 39 | this.handler = handler; 40 | this.parseBuffer = new byte[64 * 1024]; 41 | } 42 | 43 | public void Execute(byte[] buffer, int offset, int count) 44 | { 45 | if (isCompleted) 46 | throw new InvalidOperationException("Parser is done"); 47 | 48 | if (!hasStarted) 49 | { 50 | inHeaders = true; 51 | hasStarted = true; 52 | 53 | this.handler.OnResponseBegin(); 54 | } 55 | 56 | if (!inHeaders) 57 | { 58 | if (!hasEntityData) 59 | { 60 | this.isCompleted = true; 61 | this.handler.OnResponseEnd(); 62 | return; 63 | } 64 | 65 | if (!inEntityData) 66 | { 67 | inEntityData = true; 68 | handler.OnEntityStart(); 69 | } 70 | 71 | if (count > 0) 72 | { 73 | this.handler.OnEntityData(buffer, offset, count); 74 | this.entityDataWritten += count; 75 | } 76 | 77 | if (count == 0 || this.entityDataWritten == this.contentLength) 78 | { 79 | inEntityData = false; 80 | isCompleted = true; 81 | this.handler.OnEntityEnd(); 82 | this.handler.OnResponseEnd(); 83 | } 84 | 85 | return; 86 | } 87 | 88 | int bufferLeft = parseBuffer.Length - parseBufferWritten; 89 | 90 | if (bufferLeft <= 0) 91 | throw new FormatException("Response headers exceeded maximum allowed length"); 92 | 93 | if (count > bufferLeft) 94 | { 95 | this.Execute(buffer, offset, bufferLeft); 96 | this.Execute(buffer, offset + bufferLeft, count - bufferLeft); 97 | 98 | return; 99 | } 100 | 101 | Array.Copy(buffer, offset, parseBuffer, parseBufferWritten, count); 102 | parseBufferWritten += count; 103 | 104 | int endOfHeaders = IndexOf(parseBuffer, 0, parseBufferWritten, 13, 10, 13, 10); 105 | 106 | if (endOfHeaders >= 0) 107 | { 108 | ParseHeaders(parseBuffer, 0, endOfHeaders + 4); 109 | 110 | this.inHeaders = false; 111 | 112 | if (endOfHeaders + 4 < parseBufferWritten) 113 | this.Execute(parseBuffer, endOfHeaders + 4, parseBufferWritten - (endOfHeaders + 4)); 114 | else 115 | { 116 | if (!hasEntityData) 117 | { 118 | this.isCompleted = true; 119 | this.handler.OnResponseEnd(); 120 | return; 121 | } 122 | } 123 | } 124 | } 125 | 126 | private void ParseHeaders(byte[] buffer, int offset, int count) 127 | { 128 | using (var ms = new MemoryStream(buffer, offset, count)) 129 | using (var sr = new StreamReader(ms, Encoding.GetEncoding("us-ascii"))) 130 | { 131 | ParseStatusLine(sr.ReadLine()); 132 | 133 | string line; 134 | 135 | while (!string.IsNullOrEmpty(line = sr.ReadLine())) 136 | ParseHeaderLine(line); 137 | 138 | this.handler.OnHeadersEnd(); 139 | 140 | hasEntityData = this.contentLength > 0 || chunkedTransfer; 141 | } 142 | } 143 | 144 | private static Regex StatusLineRegex = new Regex(@"^HTTP/(?\d\.\d) (?\d{3}) (?.*)"); 145 | private bool chunkedTransfer; 146 | 147 | private void ParseStatusLine(string line) 148 | { 149 | if (line == null) 150 | throw new ArgumentNullException("line"); 151 | 152 | var m = StatusLineRegex.Match(line); 153 | 154 | if (!m.Success) 155 | throw new FormatException("Malformed status line"); 156 | 157 | var version = m.Groups["version"].Value; 158 | 159 | if (version != "1.1" && version != "1.0") 160 | throw new FormatException("Unknown http version"); 161 | 162 | int statusCode = int.Parse(m.Groups["statusCode"].Value); 163 | string statusDescription = m.Groups["statusDescription"].Value; 164 | 165 | this.handler.OnStatusLine(new Version(version), statusCode, statusDescription); 166 | } 167 | 168 | private void ParseHeaderLine(string line) 169 | { 170 | var parts = line.Split(new[] { ':' }, 2); 171 | 172 | if (parts.Length != 2) 173 | throw new FormatException("Malformed header line"); 174 | 175 | parts[1] = parts[1].Trim(); 176 | 177 | if (parts[0] == "Content-Length") 178 | { 179 | int cl; 180 | if (int.TryParse(parts[1].Trim(), out cl)) 181 | this.contentLength = cl; 182 | } 183 | else if (parts[0] == "Transfer-Encoding") 184 | { 185 | if (parts[1] == "chunked") 186 | this.chunkedTransfer = true; 187 | } 188 | 189 | this.handler.OnHeader(parts[0], parts[1]); 190 | } 191 | 192 | private int IndexOf(byte[] buffer, int offset, int count, params byte[] elements) 193 | { 194 | for (int i = offset; i < offset + count; i++) 195 | { 196 | int j = 0; 197 | for (; j < elements.Length && i + j < offset + count && buffer[i + j] == elements[j]; j++) ; 198 | 199 | if (j == elements.Length) 200 | return i; 201 | } 202 | 203 | return -1; 204 | } 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /Switchboard.Server/Utils/RedirectingStream.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2009 Markus Olsson 3 | * var mail = string.Join(".", new string[] {"j", "markus", "olsson"}) + string.Concat('@', "gmail.com"); 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software without 7 | * restriction, including without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be 12 | * included in all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 15 | * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | */ 20 | 21 | using System; 22 | using System.Collections.Generic; 23 | using System.IO; 24 | using System.Linq; 25 | using System.Text; 26 | using System.Threading.Tasks; 27 | 28 | namespace Switchboard.Server.Utils 29 | { 30 | /// 31 | /// An implementation of a Stream that transparently redirects all 32 | /// stream-related method calls to the supplied inner stream. Makes 33 | /// it easy to implement the subset of stream functionality required 34 | /// for your stream. 35 | /// 36 | internal abstract class RedirectingStream : Stream 37 | { 38 | protected readonly Stream innerStream; 39 | 40 | public RedirectingStream(Stream innerStream) 41 | { 42 | this.innerStream = innerStream; 43 | } 44 | 45 | public override bool CanRead { get { return this.innerStream.CanRead; } } 46 | 47 | public override bool CanSeek { get { return this.innerStream.CanSeek; } } 48 | 49 | public override bool CanWrite { get { return this.innerStream.CanWrite; } } 50 | 51 | public override void Flush() 52 | { 53 | this.innerStream.Flush(); 54 | } 55 | 56 | public override long Length 57 | { 58 | get { return this.innerStream.Length; } 59 | } 60 | 61 | public override long Position 62 | { 63 | get { return this.innerStream.Position; } 64 | set { this.innerStream.Position = value; } 65 | } 66 | 67 | public override int Read(byte[] buffer, int offset, int count) 68 | { 69 | return this.innerStream.Read(buffer, offset, count); 70 | } 71 | 72 | public override long Seek(long offset, SeekOrigin origin) 73 | { 74 | return this.innerStream.Seek(offset, origin); 75 | } 76 | 77 | public override void SetLength(long value) 78 | { 79 | this.innerStream.SetLength(value); 80 | } 81 | 82 | public override void Write(byte[] buffer, int offset, int count) 83 | { 84 | this.innerStream.Write(buffer, offset, count); 85 | } 86 | 87 | public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) 88 | { 89 | return this.innerStream.BeginRead(buffer, offset, count, callback, state); 90 | } 91 | 92 | public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) 93 | { 94 | return this.innerStream.BeginWrite(buffer, offset, count, callback, state); 95 | } 96 | 97 | public override void Close() 98 | { 99 | this.innerStream.Close(); 100 | } 101 | 102 | public override Task CopyToAsync(Stream destination, int bufferSize, System.Threading.CancellationToken cancellationToken) 103 | { 104 | return this.innerStream.CopyToAsync(destination, bufferSize, cancellationToken); 105 | } 106 | 107 | public override bool CanTimeout 108 | { 109 | get 110 | { 111 | return this.innerStream.CanTimeout; 112 | } 113 | } 114 | 115 | public override System.Runtime.Remoting.ObjRef CreateObjRef(Type requestedType) 116 | { 117 | throw new NotSupportedException(); 118 | } 119 | 120 | [Obsolete] 121 | protected override System.Threading.WaitHandle CreateWaitHandle() 122 | { 123 | throw new NotSupportedException(); 124 | } 125 | 126 | protected override void Dispose(bool disposing) 127 | { 128 | this.innerStream.Dispose(); 129 | } 130 | 131 | public override int EndRead(IAsyncResult asyncResult) 132 | { 133 | return this.innerStream.EndRead(asyncResult); 134 | } 135 | 136 | public override void EndWrite(IAsyncResult asyncResult) 137 | { 138 | this.innerStream.EndWrite(asyncResult); 139 | } 140 | 141 | public override bool Equals(object obj) 142 | { 143 | return this.innerStream.Equals(obj); 144 | } 145 | 146 | public override Task FlushAsync(System.Threading.CancellationToken cancellationToken) 147 | { 148 | return this.innerStream.FlushAsync(cancellationToken); 149 | } 150 | 151 | public override int GetHashCode() 152 | { 153 | return this.innerStream.GetHashCode(); 154 | } 155 | 156 | public override object InitializeLifetimeService() 157 | { 158 | throw new NotSupportedException(); 159 | } 160 | 161 | public override Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) 162 | { 163 | return this.innerStream.ReadAsync(buffer, offset, count, cancellationToken); 164 | } 165 | 166 | public override int ReadByte() 167 | { 168 | return this.innerStream.ReadByte(); 169 | } 170 | 171 | public override int ReadTimeout 172 | { 173 | get 174 | { 175 | return this.innerStream.ReadTimeout; 176 | } 177 | set 178 | { 179 | this.innerStream.ReadTimeout = value; 180 | } 181 | } 182 | 183 | public override string ToString() 184 | { 185 | return this.innerStream.ToString(); 186 | } 187 | 188 | public override Task WriteAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) 189 | { 190 | return this.innerStream.WriteAsync(buffer, offset, count, cancellationToken); 191 | } 192 | 193 | public override void WriteByte(byte value) 194 | { 195 | this.innerStream.WriteByte(value); 196 | } 197 | 198 | public override int WriteTimeout 199 | { 200 | get 201 | { 202 | return this.innerStream.WriteTimeout; 203 | } 204 | set 205 | { 206 | this.innerStream.WriteTimeout = value; 207 | } 208 | } 209 | 210 | } 211 | } 212 | --------------------------------------------------------------------------------