├── .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 |
--------------------------------------------------------------------------------