├── src └── SharpReverseProxy │ ├── ProxyStatus.cs │ ├── ProxyResult.cs │ ├── ProxyRule.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── ProxyOptions.cs │ ├── ProxyServerExtension.cs │ ├── ProxyResultBuilder.cs │ ├── SharpReverseProxy.csproj │ └── ProxyMiddleware.cs ├── samples ├── SampleApi1 │ ├── appsettings.json │ ├── web.config │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Controllers │ │ └── ValuesController.cs │ ├── SampleApi1.csproj │ └── Startup.cs ├── SampleApi2 │ ├── appsettings.json │ ├── web.config │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Controllers │ │ └── ValuesController.cs │ ├── SampleApi2.csproj │ └── Startup.cs ├── SampleApiAuthentication │ ├── appsettings.json │ ├── Authentication │ │ ├── TokenOptions.cs │ │ └── TokenProviderService.cs │ ├── web.config │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── SampleApiAuthentication.csproj │ ├── Controllers │ │ └── AuthenticationController.cs │ └── Startup.cs └── SampleWeb │ ├── web.config │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── SampleWeb.csproj │ ├── Authentication │ └── CustomJwtDataFormat.cs │ └── Startup.cs ├── test └── SharpReverseProxy.Tests │ ├── HttpContextFakes │ ├── FakeHttpMessageHandler.cs │ ├── HttpContextFake.cs │ ├── HttpResponseFake.cs │ ├── HttpRequestFake.cs │ └── HeaderDictionaryFake.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── SharpReverseProxy.Tests.csproj │ └── ProxyTests.cs ├── LICENSE ├── SharpReverseProxy.sln ├── .gitignore └── README.md /src/SharpReverseProxy/ProxyStatus.cs: -------------------------------------------------------------------------------- 1 | namespace SharpReverseProxy { 2 | public enum ProxyStatus { 3 | NotProxied, 4 | Proxied, 5 | NotAuthenticated 6 | } 7 | } -------------------------------------------------------------------------------- /samples/SampleApi1/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/SampleApi2/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/SampleApiAuthentication/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/SampleApiAuthentication/Authentication/TokenOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SampleApiAuthentication.Authentication { 4 | public class TokenOptions { 5 | public string Issuer { get; set; } = "Application"; 6 | public string Audience { get; set; } = "DefaultClient"; 7 | public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(15); 8 | } 9 | } -------------------------------------------------------------------------------- /samples/SampleApi1/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /samples/SampleApi2/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /samples/SampleWeb/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/SharpReverseProxy/ProxyResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace SharpReverseProxy { 5 | public class ProxyResult { 6 | public ProxyStatus ProxyStatus { get; set; } 7 | public int HttpStatusCode { get; set; } 8 | public Uri OriginalUri { get; set; } 9 | public Uri ProxiedUri { get; set; } 10 | public TimeSpan Elapsed { get; set; } 11 | [Obsolete("Elipsed property is deprecated, please use Elapsed instead.")] 12 | public TimeSpan Elipsed { 13 | get { return Elapsed; } 14 | set { Elapsed = value; } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /samples/SampleApiAuthentication/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /samples/SampleWeb/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Hosting; 7 | 8 | namespace SampleWeb 9 | { 10 | public class Program 11 | { 12 | public static void Main(string[] args) 13 | { 14 | var host = new WebHostBuilder() 15 | .UseKestrel() 16 | .UseContentRoot(Directory.GetCurrentDirectory()) 17 | .UseIISIntegration() 18 | .UseStartup() 19 | .Build(); 20 | 21 | host.Run(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/SharpReverseProxy/ProxyRule.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System; 3 | using System.Net.Http; 4 | using System.Security.Claims; 5 | using System.Threading.Tasks; 6 | 7 | namespace SharpReverseProxy { 8 | public class ProxyRule { 9 | public Func Matcher { get; set; } = uri => false; 10 | public Action Modifier { get; set; } = (msg, user) => { }; 11 | public Func ResponseModifier { get; set; } = null; 12 | public bool PreProcessResponse { get; set; } = true; 13 | public bool RequiresAuthentication { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/SampleApiAuthentication/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Builder; 8 | 9 | namespace SampleApiAuthentication 10 | { 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | var host = new WebHostBuilder() 16 | .UseKestrel() 17 | .UseContentRoot(Directory.GetCurrentDirectory()) 18 | .UseIISIntegration() 19 | .UseStartup() 20 | .Build(); 21 | 22 | host.Run(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/SampleApi1/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Builder; 8 | 9 | namespace SampleApi1 10 | { 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | var host = new WebHostBuilder() 16 | .UseKestrel() 17 | .UseContentRoot(Directory.GetCurrentDirectory()) 18 | .UseIISIntegration() 19 | .UseStartup() 20 | .UseUrls("http://localhost:5001") 21 | .Build(); 22 | 23 | host.Run(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/SampleApi2/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Builder; 8 | 9 | namespace SampleApi2 10 | { 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | var host = new WebHostBuilder() 16 | .UseKestrel() 17 | .UseContentRoot(Directory.GetCurrentDirectory()) 18 | .UseIISIntegration() 19 | .UseStartup() 20 | .UseUrls("http://localhost:5002") 21 | .Build(); 22 | 23 | host.Run(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/SampleWeb/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:21233/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "SampleWeb": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /test/SharpReverseProxy.Tests/HttpContextFakes/FakeHttpMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace SharpReverseProxy.Tests.HttpContextFakes { 7 | public class FakeHttpMessageHandler : HttpMessageHandler { 8 | public HttpRequestMessage RequestMessage { get; private set; } 9 | 10 | public HttpResponseMessage ResponseMessageToReturn { get; set; } = new HttpResponseMessage(HttpStatusCode.OK); 11 | 12 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { 13 | RequestMessage = request; 14 | return Task.FromResult(ResponseMessageToReturn); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /samples/SampleApi1/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:5001/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "launchUrl": "api/values", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "SampleApi1": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "http://localhost:5000/api/values", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /samples/SampleApi2/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:21258/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "launchUrl": "api/values", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "SampleApi2": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "http://localhost:5000/api/values", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /samples/SampleApiAuthentication/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:18981/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "launchUrl": "api/values", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "SampleApiAuthentication": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "http://localhost:5000/api/values", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/SharpReverseProxy/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: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("SharpReverseProxy")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("9058d441-59f9-43ba-a84a-72e51f78cfe7")] 20 | -------------------------------------------------------------------------------- /test/SharpReverseProxy.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: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("SharpReverseProxy.Tests")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("02e9a359-2493-44d0-b136-65254f83153f")] 20 | -------------------------------------------------------------------------------- /src/SharpReverseProxy/ProxyOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | 6 | namespace SharpReverseProxy { 7 | public class ProxyOptions { 8 | public List ProxyRules { get; set; } = new List(); 9 | public HttpMessageHandler BackChannelMessageHandler { get; set; } 10 | public Action Reporter { get; set; } = result => { }; 11 | 12 | public bool FollowRedirects { get; set; } = true; 13 | public bool AddForwardedHeader { get; set; } = false; 14 | 15 | public ProxyOptions() {} 16 | 17 | public ProxyOptions(List rules, Action reporter = null) { 18 | ProxyRules = rules; 19 | if (reporter != null) { 20 | Reporter = reporter; 21 | } 22 | } 23 | 24 | public void AddProxyRule(ProxyRule rule) { 25 | ProxyRules.Add(rule); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/SharpReverseProxy/ProxyServerExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace SharpReverseProxy { 7 | public static class ProxyExtension { 8 | 9 | /// 10 | /// Sends request to remote server as specified in options 11 | /// 12 | /// 13 | /// Options and rules for proxy actions 14 | /// 15 | public static IApplicationBuilder UseProxy(this IApplicationBuilder app, ProxyOptions proxyOptions) { 16 | return app.UseMiddleware(Options.Create(proxyOptions)); 17 | } 18 | 19 | public static IApplicationBuilder UseProxy(this IApplicationBuilder app, List rules, Action reporter = null) { 20 | return app.UseMiddleware(Options.Create(new ProxyOptions(rules, reporter))); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/SampleApi1/Controllers/ValuesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace SampleApi1.Controllers { 8 | [Route("api/[controller]")] 9 | public class ValuesController : Controller { 10 | // GET api/values 11 | [HttpGet] 12 | public IEnumerable Get() { 13 | return new string[] { "api1", "api1" }; 14 | } 15 | 16 | // GET api/values/5 17 | [HttpGet("{id}")] 18 | public string Get(int id) { 19 | return "value"; 20 | } 21 | 22 | // POST api/values 23 | [HttpPost] 24 | public string Post([FromBody]string value) { 25 | return value; 26 | } 27 | 28 | // PUT api/values/5 29 | [HttpPut("{id}")] 30 | public void Put(int id, [FromBody]string value) { 31 | } 32 | 33 | // DELETE api/values/5 34 | [HttpDelete("{id}")] 35 | public void Delete(int id) { 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 SharpTools 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /samples/SampleApi2/Controllers/ValuesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace SampleApi2.Controllers 8 | { 9 | [Route("api/[controller]")] 10 | public class ValuesController : Controller 11 | { 12 | // GET api/values 13 | [HttpGet] 14 | public IEnumerable Get() 15 | { 16 | return new string[] { "api2", "value2" }; 17 | } 18 | 19 | // GET api/values/5 20 | [HttpGet("{id}")] 21 | public string Get(int id) 22 | { 23 | return "value"; 24 | } 25 | 26 | // POST api/values 27 | [HttpPost] 28 | public void Post([FromBody]string value) 29 | { 30 | } 31 | 32 | // PUT api/values/5 33 | [HttpPut("{id}")] 34 | public void Put(int id, [FromBody]string value) 35 | { 36 | } 37 | 38 | // DELETE api/values/5 39 | [HttpDelete("{id}")] 40 | public void Delete(int id) 41 | { 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/SharpReverseProxy/ProxyResultBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace SharpReverseProxy { 5 | public class ProxyResultBuilder { 6 | private ProxyResult _result; 7 | private DateTime _start; 8 | public ProxyResultBuilder(Uri originalUri) { 9 | _result = new ProxyResult { 10 | OriginalUri = originalUri 11 | }; 12 | _start = DateTime.Now; 13 | } 14 | 15 | public ProxyResult Proxied(Uri proxiedUri, int statusCode) { 16 | Finish(ProxyStatus.Proxied); 17 | _result.ProxiedUri = proxiedUri; 18 | _result.HttpStatusCode = statusCode; 19 | return _result; 20 | } 21 | 22 | public ProxyResult NotProxied(int statusCode) { 23 | Finish(ProxyStatus.NotProxied); 24 | _result.HttpStatusCode = statusCode; 25 | return _result; 26 | } 27 | 28 | public ProxyResult NotAuthenticated() { 29 | Finish(ProxyStatus.NotAuthenticated); 30 | _result.HttpStatusCode = StatusCodes.Status401Unauthorized; 31 | return _result; 32 | } 33 | 34 | private ProxyResult Finish(ProxyStatus proxyStatus) { 35 | _result.ProxyStatus = proxyStatus; 36 | _result.Elapsed = DateTime.Now - _start; 37 | return _result; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /samples/SampleWeb/SampleWeb.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp1.1 5 | true 6 | SampleWeb 7 | Exe 8 | SampleWeb 9 | 1.0.4 10 | $(PackageTargetFallback);dotnet5.6;portable-net45+win8 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/SharpReverseProxy.Tests/HttpContextFakes/HttpContextFake.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Claims; 4 | using System.Threading; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Http.Authentication; 7 | using Microsoft.AspNetCore.Http.Features; 8 | 9 | namespace SharpReverseProxy.Tests.HttpContextFakes { 10 | public class HttpContextFake : HttpContext { 11 | public HttpContextFake(HttpRequestFake request, HttpResponseFake response = null) { 12 | request.SetHttpContext(this); 13 | Request = request; 14 | Response = response ?? new HttpResponseFake(); 15 | } 16 | 17 | public override void Abort() {} 18 | public override IFeatureCollection Features { get; } 19 | public override HttpRequest Request { get; } 20 | public override HttpResponse Response { get; } 21 | public override ConnectionInfo Connection { get; } 22 | public override WebSocketManager WebSockets { get; } 23 | public override AuthenticationManager Authentication { get; } 24 | public override ClaimsPrincipal User { get; set; } 25 | public override IDictionary Items { get; set; } 26 | public override IServiceProvider RequestServices { get; set; } 27 | public override CancellationToken RequestAborted { get; set; } 28 | public override string TraceIdentifier { get; set; } 29 | public override ISession Session { get; set; } 30 | } 31 | } -------------------------------------------------------------------------------- /src/SharpReverseProxy/SharpReverseProxy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Powerful Reverse Proxy written as OWIN Middleware. Perfect for asp.net, web.api, microservices, etc. 5 | 1.3.0 6 | Andre Carlucci 7 | netstandard1.6 8 | SharpReverseProxy 9 | SharpReverseProxy 10 | aspnetcore;microservices;proxy;apigateway 11 | git 12 | https://github.com/SharpTools/SharpReverseProxy 13 | $(PackageTargetFallback);dnxcore50 14 | false 15 | false 16 | false 17 | True 18 | https://github.com/SharpTools/SharpReverseProxy/blob/master/LICENSE 19 | https://github.com/SharpTools/SharpReverseProxy 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/SharpReverseProxy.Tests/SharpReverseProxy.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp1.1 4 | SharpReverseProxy.Tests 5 | SharpReverseProxy.Tests 6 | true 7 | $(PackageTargetFallback);netcoreapp1.0;portable-net45+win8 8 | false 9 | false 10 | false 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /samples/SampleApi1/SampleApi1.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp1.1 5 | true 6 | SampleApi1 7 | Exe 8 | SampleApi1 9 | 1.0.4 10 | $(PackageTargetFallback);dotnet5.6;portable-net45+win8 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /samples/SampleApi2/SampleApi2.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp1.1 5 | true 6 | SampleApi2 7 | Exe 8 | SampleApi2 9 | 1.0.4 10 | $(PackageTargetFallback);dotnet5.6;portable-net45+win8 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/SharpReverseProxy.Tests/HttpContextFakes/HttpResponseFake.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace SharpReverseProxy.Tests.HttpContextFakes { 7 | public class HttpResponseFake : HttpResponse { 8 | private HttpContext _httpContext; 9 | public override int StatusCode { get; set; } 10 | public override IHeaderDictionary Headers { get; } 11 | public override Stream Body { get; set; } = new MemoryStream(); 12 | public override long? ContentLength { get; set; } 13 | public override string ContentType { get; set; } 14 | public override IResponseCookies Cookies { get; } 15 | private bool _onStartedCalled; 16 | public override bool HasStarted { 17 | get { 18 | return Body.Length > 0 || _onStartedCalled; 19 | } 20 | } 21 | public void SetHttpContext(HttpContext context) { 22 | _httpContext = context; 23 | } 24 | public override HttpContext HttpContext => _httpContext; 25 | 26 | public HttpResponseFake() { 27 | Headers = new HeaderDictionaryFake(this); 28 | var stream = new MemoryStream(); 29 | 30 | } 31 | public override void OnStarting(Func callback, object state) { 32 | _onStartedCalled = true; 33 | } 34 | 35 | public override void OnCompleted(Func callback, object state) { 36 | 37 | } 38 | 39 | public override void Redirect(string location, bool permanent) { 40 | 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /samples/SampleApi1/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace SampleApi1 12 | { 13 | public class Startup 14 | { 15 | public Startup(IHostingEnvironment env) 16 | { 17 | var builder = new ConfigurationBuilder() 18 | .SetBasePath(env.ContentRootPath) 19 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 20 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) 21 | .AddEnvironmentVariables(); 22 | Configuration = builder.Build(); 23 | } 24 | 25 | public IConfigurationRoot Configuration { get; } 26 | 27 | // This method gets called by the runtime. Use this method to add services to the container. 28 | public void ConfigureServices(IServiceCollection services) 29 | { 30 | // Add framework services. 31 | services.AddMvc(); 32 | } 33 | 34 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 35 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 36 | { 37 | loggerFactory.AddConsole(Configuration.GetSection("Logging")); 38 | loggerFactory.AddDebug(); 39 | 40 | app.UseMvc(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /samples/SampleApi2/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace SampleApi2 12 | { 13 | public class Startup 14 | { 15 | public Startup(IHostingEnvironment env) 16 | { 17 | var builder = new ConfigurationBuilder() 18 | .SetBasePath(env.ContentRootPath) 19 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 20 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) 21 | .AddEnvironmentVariables(); 22 | Configuration = builder.Build(); 23 | } 24 | 25 | public IConfigurationRoot Configuration { get; } 26 | 27 | // This method gets called by the runtime. Use this method to add services to the container. 28 | public void ConfigureServices(IServiceCollection services) 29 | { 30 | // Add framework services. 31 | services.AddMvc(); 32 | } 33 | 34 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 35 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 36 | { 37 | loggerFactory.AddConsole(Configuration.GetSection("Logging")); 38 | loggerFactory.AddDebug(); 39 | 40 | app.UseMvc(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /samples/SampleApiAuthentication/SampleApiAuthentication.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp1.1 5 | true 6 | SampleApiAuthentication 7 | Exe 8 | SampleApiAuthentication 9 | 1.0.4 10 | $(PackageTargetFallback);dotnet5.6;portable-net45+win8 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /samples/SampleApiAuthentication/Controllers/AuthenticationController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Logging; 5 | using SampleApiAuthentication.Authentication; 6 | 7 | namespace SampleApiAuthentication.Controllers 8 | { 9 | [Route("api/[controller]")] 10 | public class AuthenticationController : Controller { 11 | private readonly ILogger _logger; 12 | private readonly TokenOptions _tokenOptions; 13 | private readonly TokenProviderService _tokenProviderService; 14 | 15 | public AuthenticationController(TokenOptions tokenOptions, 16 | TokenProviderService tokenProviderService) { 17 | _tokenOptions = tokenOptions; 18 | _tokenProviderService = tokenProviderService; 19 | } 20 | 21 | [HttpGet] //I know, it's for testing 22 | [Route("login")] 23 | public async Task Login(string username, string password, string returnurl) { 24 | //always true, it's for testing 25 | var token = await _tokenProviderService.GenerateToken(username, "admin", _tokenOptions); 26 | Response.Cookies.Append(TokenProviderService.AccessTokenName, token); 27 | if (!string.IsNullOrEmpty(returnurl)) { 28 | return Redirect(returnurl); 29 | } 30 | return new JsonResult(new { 31 | access_token = token, 32 | expires_in = (int)TimeSpan.FromDays(365).TotalSeconds 33 | }); 34 | } 35 | 36 | [HttpGet] //I know, it's for testing 37 | [Route("logout")] 38 | public IActionResult Logout(string returnurl) { 39 | Response.Cookies.Delete(TokenProviderService.AccessTokenName); 40 | if (!string.IsNullOrEmpty(returnurl)) { 41 | return Redirect(returnurl); 42 | } 43 | return Ok(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /samples/SampleApiAuthentication/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | using SampleApiAuthentication.Authentication; 11 | 12 | namespace SampleApiAuthentication { 13 | public class Startup { 14 | public Startup(IHostingEnvironment env) { 15 | var builder = new ConfigurationBuilder() 16 | .SetBasePath(env.ContentRootPath) 17 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 18 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) 19 | .AddEnvironmentVariables(); 20 | Configuration = builder.Build(); 21 | } 22 | 23 | public IConfigurationRoot Configuration { get; } 24 | 25 | // This method gets called by the runtime. Use this method to add services to the container. 26 | public void ConfigureServices(IServiceCollection services) { 27 | services.AddSingleton(new TokenProviderService("foofoofoofoofoobar")); 28 | var tokenOptions = new TokenOptions { 29 | Expiration = TimeSpan.FromDays(365), 30 | Issuer = "someIssuer", 31 | Audience = "Browser" 32 | }; 33 | services.AddSingleton(tokenOptions); 34 | services.AddMvc(); 35 | } 36 | 37 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 38 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { 39 | loggerFactory.AddConsole(Configuration.GetSection("Logging")); 40 | loggerFactory.AddDebug(); 41 | 42 | app.UseMvc(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /samples/SampleWeb/Authentication/CustomJwtDataFormat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IdentityModel.Tokens.Jwt; 4 | using System.Linq; 5 | using System.Security.Claims; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.AspNetCore.Http.Authentication; 9 | using Microsoft.IdentityModel.Tokens; 10 | 11 | namespace SampleWeb.Authentication 12 | { 13 | public class CustomJwtDataFormat : ISecureDataFormat { 14 | private readonly string _algorithm; 15 | private readonly TokenValidationParameters _validationParameters; 16 | 17 | public CustomJwtDataFormat(string algorithm, TokenValidationParameters validationParameters) { 18 | _algorithm = algorithm; 19 | _validationParameters = validationParameters; 20 | } 21 | 22 | public AuthenticationTicket Unprotect(string protectedText) 23 | => Unprotect(protectedText, null); 24 | 25 | public AuthenticationTicket Unprotect(string protectedText, string purpose) { 26 | var handler = new JwtSecurityTokenHandler(); 27 | handler.InboundClaimTypeMap[JwtRegisteredClaimNames.Sub] = ClaimTypes.Name; 28 | ClaimsPrincipal principal; 29 | try { 30 | SecurityToken validToken; 31 | principal = handler.ValidateToken(protectedText, _validationParameters, out validToken); 32 | var validJwt = validToken as JwtSecurityToken; 33 | if (validJwt == null) { 34 | throw new ArgumentException("Invalid JWT"); 35 | } 36 | if (!validJwt.Header.Alg.Equals(_algorithm, StringComparison.Ordinal)) { 37 | throw new ArgumentException($"Algorithm must be '{_algorithm}'"); 38 | } 39 | } 40 | catch (SecurityTokenValidationException) { 41 | return null; 42 | } 43 | catch (ArgumentException) { 44 | return null; 45 | } 46 | return new AuthenticationTicket(principal, new AuthenticationProperties(), "Cookie"); 47 | } 48 | 49 | public string Protect(AuthenticationTicket data) { 50 | throw new NotImplementedException(); 51 | } 52 | 53 | public string Protect(AuthenticationTicket data, string purpose) { 54 | throw new NotImplementedException(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /samples/SampleApiAuthentication/Authentication/TokenProviderService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IdentityModel.Tokens.Jwt; 3 | using System.Security.Claims; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Microsoft.IdentityModel.Tokens; 7 | 8 | namespace SampleApiAuthentication.Authentication { 9 | public class TokenProviderService { 10 | public static readonly string AccessTokenName = "access_token"; 11 | public SecurityKey SecurityKey { get; } 12 | private SigningCredentials _signingCredentials; 13 | 14 | public TokenProviderService(string secretKey) { 15 | var key = Encoding.ASCII.GetBytes(secretKey); 16 | if (key.Length < 16) { 17 | throw new ArgumentException( 18 | $"The secret key for the algorithm: 'HS256' cannot have less than: '128' bits. KeySize is: '{key.Length * 8}'.", 19 | nameof(secretKey)); 20 | } 21 | SecurityKey = new SymmetricSecurityKey(key); 22 | _signingCredentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256); 23 | } 24 | 25 | public async Task GenerateToken(string username, string role, TokenOptions options) { 26 | var now = DateTime.UtcNow; 27 | // Specifically add the jti (nonce), iat (issued timestamp), and sub (subject/user) claims. 28 | // You can add other claims here, if you want: 29 | var claims = new[] { 30 | new Claim(JwtRegisteredClaimNames.Sub, username), 31 | new Claim(JwtRegisteredClaimNames.Jti, await GenerateNonce()), 32 | new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(now).ToString(), ClaimValueTypes.Integer64), 33 | new Claim("roles", role), 34 | }; 35 | // Create the JWT and write it to a string 36 | var jwt = new JwtSecurityToken( 37 | options.Issuer, 38 | options.Audience, 39 | claims, 40 | now, 41 | now.Add(options.Expiration), 42 | _signingCredentials); 43 | return new JwtSecurityTokenHandler().WriteToken(jwt); 44 | } 45 | 46 | public virtual async Task GenerateNonce() { 47 | return await Task.FromResult(Guid.NewGuid().ToString()); 48 | } 49 | 50 | public static long ToUnixEpochDate(DateTime date) 51 | => 52 | (long) 53 | Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds); 54 | } 55 | } -------------------------------------------------------------------------------- /test/SharpReverseProxy.Tests/HttpContextFakes/HttpRequestFake.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Http.Extensions; 8 | using Microsoft.AspNetCore.Http.Internal; 9 | using Microsoft.Extensions.Primitives; 10 | 11 | namespace SharpReverseProxy.Tests.HttpContextFakes { 12 | public class HttpRequestFake : HttpRequest { 13 | private HttpContext _httpContext; 14 | private string _scheme; 15 | private Uri _uri; 16 | 17 | public HttpRequestFake() { 18 | 19 | } 20 | 21 | public HttpRequestFake(Uri uri) { 22 | SetUrl(uri); 23 | } 24 | 25 | public void SetUrl(Uri uri) { 26 | string scheme; 27 | HostString hostString; 28 | PathString pathString; 29 | QueryString queryString; 30 | FragmentString fragmentString; 31 | UriHelper.FromAbsolute(uri.ToString(), out scheme, out hostString, out pathString, out queryString, 32 | out fragmentString); 33 | 34 | Host = hostString; 35 | Path = pathString; 36 | QueryString = queryString; 37 | Scheme = scheme; 38 | Uri = uri; 39 | } 40 | 41 | public Uri Uri { get; private set; } 42 | 43 | public override Task ReadFormAsync( 44 | CancellationToken cancellationToken = new CancellationToken()) { 45 | return Task.FromResult(Form); 46 | } 47 | 48 | public void SetHttpContext(HttpContext context) { 49 | _httpContext = context; 50 | } 51 | 52 | public override HttpContext HttpContext => _httpContext; 53 | public override string Method { get; set; } = "GET"; 54 | 55 | public override string Scheme { 56 | get { return _scheme; } 57 | set { 58 | _scheme = value; 59 | IsHttps = _scheme.ToLower() == "https"; 60 | } 61 | } 62 | 63 | public override bool IsHttps { get; set; } 64 | public override HostString Host { get; set; } = new HostString("myserver", 80); 65 | public override PathString PathBase { get; set; } 66 | public override PathString Path { get; set; } 67 | public override QueryString QueryString { get; set; } 68 | public override IQueryCollection Query { get; set; } = new QueryCollection(); 69 | public override string Protocol { get; set; } 70 | public override IHeaderDictionary Headers { get; } = new HeaderDictionary(); 71 | public override IRequestCookieCollection Cookies { get; set; } = new RequestCookieCollection(); 72 | public override long? ContentLength { get; set; } 73 | public override string ContentType { get; set; } 74 | public override Stream Body { get; set; }= new MemoryStream(); 75 | public override bool HasFormContentType { get; } 76 | public override IFormCollection Form { get; set; } = new FormCollection(new Dictionary()); 77 | } 78 | } -------------------------------------------------------------------------------- /test/SharpReverseProxy.Tests/HttpContextFakes/HeaderDictionaryFake.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Primitives; 6 | 7 | namespace SharpReverseProxy.Tests.HttpContextFakes { 8 | public class HeaderDictionaryFake : IHeaderDictionary { 9 | 10 | private Dictionary _headers = new Dictionary(); 11 | private HttpResponseFake _parentResponse; 12 | 13 | public HeaderDictionaryFake(HttpResponseFake parentResponse) { 14 | _parentResponse = parentResponse; 15 | } 16 | 17 | public StringValues this[string key] { 18 | get => ContainsKey(key) ? _headers[key] : StringValues.Empty; 19 | set => _headers[key] = value; 20 | } 21 | 22 | public bool IsReadOnly => false; 23 | 24 | public ICollection Keys => _headers.Keys; 25 | 26 | public ICollection Values => _headers.Values; 27 | 28 | public int Count => _headers.Count; 29 | 30 | public void Add(string key, StringValues value) { 31 | if(_parentResponse.HasStarted) { 32 | ThrowReponseAlreadyStartedException(); 33 | } 34 | _headers.Add(key, value); 35 | } 36 | 37 | public void Add(KeyValuePair item) { 38 | Add(item.Key, item.Value); 39 | } 40 | 41 | public void Clear() { 42 | if (_parentResponse.HasStarted) { 43 | ThrowReponseAlreadyStartedException(); 44 | } 45 | _headers.Clear(); 46 | } 47 | 48 | public bool Contains(KeyValuePair item) { 49 | var hasKey = _headers.ContainsKey(item.Key); 50 | if (!hasKey) { 51 | return false; 52 | } 53 | return _headers[item.Key].Equals(item.Value); 54 | } 55 | 56 | public bool ContainsKey(string key) { 57 | return _headers.ContainsKey(key); 58 | } 59 | 60 | public void CopyTo(KeyValuePair[] array, int arrayIndex) { 61 | throw new NotImplementedException(); 62 | } 63 | 64 | public IEnumerator> GetEnumerator() { 65 | return _headers.GetEnumerator(); 66 | } 67 | 68 | public bool Remove(string key) { 69 | if (_parentResponse.HasStarted) { 70 | ThrowReponseAlreadyStartedException(); 71 | } 72 | return _headers.Remove(key); 73 | } 74 | 75 | public bool Remove(KeyValuePair item) { 76 | return Remove(item.Key); 77 | } 78 | 79 | public bool TryGetValue(string key, out StringValues value) => _headers.TryGetValue(key, out value); 80 | 81 | IEnumerator IEnumerable.GetEnumerator() => _headers.GetEnumerator(); 82 | 83 | private void ThrowReponseAlreadyStartedException() { 84 | throw new InvalidOperationException("Headers are read - only, response has already started."); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /SharpReverseProxy.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{45306927-8BF0-4B67-A17E-BC2B9E13185E}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0B37799F-0188-4F8A-978F-0A3BE98F15E0}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleWeb", "samples\SampleWeb\SampleWeb.csproj", "{E1D41DEA-CF67-4D53-9F8F-890AF9558322}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApi1", "samples\SampleApi1\SampleApi1.csproj", "{5FA15D8F-B1B9-425D-9535-47F54530BEFC}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApi2", "samples\SampleApi2\SampleApi2.csproj", "{7D85A9EC-3C09-42FB-91BC-04E95E890F37}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpReverseProxy", "src\SharpReverseProxy\SharpReverseProxy.csproj", "{9058D441-59F9-43BA-A84A-72E51F78CFE7}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApiAuthentication", "samples\SampleApiAuthentication\SampleApiAuthentication.csproj", "{59C31257-CE3D-4BF2-8F57-F01CD6830B57}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpReverseProxy.Tests", "test\SharpReverseProxy.Tests\SharpReverseProxy.Tests.csproj", "{02E9A359-2493-44D0-B136-65254F83153F}" 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{15CD97C7-EBC2-4850-AA37-2A37C4F87CF6}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {E1D41DEA-CF67-4D53-9F8F-890AF9558322}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {E1D41DEA-CF67-4D53-9F8F-890AF9558322}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {E1D41DEA-CF67-4D53-9F8F-890AF9558322}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {E1D41DEA-CF67-4D53-9F8F-890AF9558322}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {5FA15D8F-B1B9-425D-9535-47F54530BEFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {5FA15D8F-B1B9-425D-9535-47F54530BEFC}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {5FA15D8F-B1B9-425D-9535-47F54530BEFC}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {5FA15D8F-B1B9-425D-9535-47F54530BEFC}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {7D85A9EC-3C09-42FB-91BC-04E95E890F37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {7D85A9EC-3C09-42FB-91BC-04E95E890F37}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {7D85A9EC-3C09-42FB-91BC-04E95E890F37}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {7D85A9EC-3C09-42FB-91BC-04E95E890F37}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {9058D441-59F9-43BA-A84A-72E51F78CFE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {9058D441-59F9-43BA-A84A-72E51F78CFE7}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {9058D441-59F9-43BA-A84A-72E51F78CFE7}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {9058D441-59F9-43BA-A84A-72E51F78CFE7}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {59C31257-CE3D-4BF2-8F57-F01CD6830B57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {59C31257-CE3D-4BF2-8F57-F01CD6830B57}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {59C31257-CE3D-4BF2-8F57-F01CD6830B57}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {59C31257-CE3D-4BF2-8F57-F01CD6830B57}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {02E9A359-2493-44D0-B136-65254F83153F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {02E9A359-2493-44D0-B136-65254F83153F}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {02E9A359-2493-44D0-B136-65254F83153F}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {02E9A359-2493-44D0-B136-65254F83153F}.Release|Any CPU.Build.0 = Release|Any CPU 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(NestedProjects) = preSolution 59 | {E1D41DEA-CF67-4D53-9F8F-890AF9558322} = {15CD97C7-EBC2-4850-AA37-2A37C4F87CF6} 60 | {5FA15D8F-B1B9-425D-9535-47F54530BEFC} = {15CD97C7-EBC2-4850-AA37-2A37C4F87CF6} 61 | {7D85A9EC-3C09-42FB-91BC-04E95E890F37} = {15CD97C7-EBC2-4850-AA37-2A37C4F87CF6} 62 | {9058D441-59F9-43BA-A84A-72E51F78CFE7} = {45306927-8BF0-4B67-A17E-BC2B9E13185E} 63 | {59C31257-CE3D-4BF2-8F57-F01CD6830B57} = {15CD97C7-EBC2-4850-AA37-2A37C4F87CF6} 64 | {02E9A359-2493-44D0-B136-65254F83153F} = {0B37799F-0188-4F8A-978F-0A3BE98F15E0} 65 | EndGlobalSection 66 | GlobalSection(ExtensibilityGlobals) = postSolution 67 | SolutionGuid = {91EA9292-73B3-43A1-9C1A-7A63601FB97C} 68 | EndGlobalSection 69 | EndGlobal 70 | -------------------------------------------------------------------------------- /samples/SampleWeb/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.IdentityModel.Tokens; 10 | using SampleWeb.Authentication; 11 | using SharpReverseProxy; 12 | 13 | namespace SampleWeb { 14 | public class Startup { 15 | // This method gets called by the runtime. Use this method to add services to the container. 16 | // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 17 | public void ConfigureServices(IServiceCollection services) { 18 | services.AddAuthentication(); 19 | } 20 | 21 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 22 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { 23 | loggerFactory.AddConsole(); 24 | loggerFactory.AddDebug(LogLevel.Debug); 25 | var logger = loggerFactory.CreateLogger("Middleware"); 26 | 27 | if (env.IsDevelopment()) { 28 | app.UseDeveloperExceptionPage(); 29 | } 30 | 31 | ConfigureAuthentication(app); 32 | 33 | var proxyOptions = new ProxyOptions { 34 | ProxyRules = new List { 35 | new ProxyRule { 36 | Matcher = uri => uri.AbsoluteUri.Contains("/api1"), 37 | Modifier = (msg ,user) => { 38 | var uri = new UriBuilder(msg.RequestUri) { 39 | Port = 5001, 40 | Path = "/api/values" 41 | }; 42 | msg.RequestUri = uri.Uri; 43 | } 44 | }, 45 | new ProxyRule { 46 | Matcher = uri => uri.AbsoluteUri.Contains("/api2"), 47 | Modifier = (msg ,user) => { 48 | var uri = new UriBuilder(msg.RequestUri) { 49 | Port = 5002, 50 | Path = "/api/values" 51 | }; 52 | msg.RequestUri = uri.Uri; 53 | }, 54 | RequiresAuthentication = true 55 | }, 56 | new ProxyRule { 57 | Matcher = uri => uri.AbsoluteUri.Contains("/authenticate"), 58 | Modifier = (msg ,user) => { 59 | var uri = new UriBuilder(msg.RequestUri) { 60 | Port = 5000 61 | }; 62 | msg.RequestUri = uri.Uri; 63 | } 64 | } 65 | }, 66 | Reporter = r => { 67 | logger.LogDebug($"Proxy: {r.ProxyStatus} Url: {r.OriginalUri} Time: {r.Elapsed}"); 68 | if (r.ProxyStatus == ProxyStatus.Proxied) { 69 | logger.LogDebug($" New Url: {r.ProxiedUri.AbsoluteUri} Status: {r.HttpStatusCode}"); 70 | } 71 | }, 72 | FollowRedirects = false 73 | }; 74 | 75 | app.UseProxy(proxyOptions); 76 | 77 | app.Run(async (context) => { 78 | await context.Response.WriteAsync("Hello World!"); 79 | }); 80 | } 81 | 82 | private static void ConfigureAuthentication(IApplicationBuilder app) { 83 | var secretKey = "foofoofoofoofoobar"; 84 | var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey)); 85 | 86 | var tokenValidationParameters = new TokenValidationParameters { 87 | ValidateIssuerSigningKey = true, 88 | IssuerSigningKey = signingKey, 89 | ValidateIssuer = true, 90 | ValidIssuer = "someIssuer", 91 | ValidateAudience = true, 92 | ValidAudience = "Browser", 93 | ValidateLifetime = true, 94 | ClockSkew = TimeSpan.Zero 95 | }; 96 | 97 | app.UseJwtBearerAuthentication(new JwtBearerOptions { 98 | AutomaticAuthenticate = true, 99 | AutomaticChallenge = true, 100 | TokenValidationParameters = tokenValidationParameters 101 | }); 102 | 103 | app.UseCookieAuthentication(new CookieAuthenticationOptions { 104 | AutomaticAuthenticate = true, 105 | AutomaticChallenge = true, 106 | AuthenticationScheme = "Cookie", 107 | CookieName = "access_token", 108 | TicketDataFormat = new CustomJwtDataFormat( 109 | SecurityAlgorithms.HmacSha256, 110 | tokenValidationParameters) 111 | }); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | -------------------------------------------------------------------------------- /src/SharpReverseProxy/ProxyMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace SharpReverseProxy { 9 | public class ProxyMiddleware { 10 | private readonly RequestDelegate _next; 11 | private readonly HttpClient _httpClient; 12 | private readonly ProxyOptions _options; 13 | 14 | public ProxyMiddleware(RequestDelegate next, IOptions options) { 15 | _next = next; 16 | _options = options.Value; 17 | _httpClient = new HttpClient(_options.BackChannelMessageHandler ?? new HttpClientHandler { 18 | AllowAutoRedirect = _options.FollowRedirects 19 | }); 20 | } 21 | 22 | public async Task Invoke(HttpContext context) { 23 | var uri = GeRequestUri(context); 24 | var resultBuilder = new ProxyResultBuilder(uri); 25 | 26 | var matchedRule = _options.ProxyRules.FirstOrDefault(r => r.Matcher.Invoke(uri)); 27 | if (matchedRule == null) { 28 | await _next(context); 29 | _options.Reporter.Invoke(resultBuilder.NotProxied(context.Response.StatusCode)); 30 | return; 31 | } 32 | 33 | if (matchedRule.RequiresAuthentication && !UserIsAuthenticated(context)) { 34 | context.Response.StatusCode = StatusCodes.Status401Unauthorized; 35 | _options.Reporter.Invoke(resultBuilder.NotAuthenticated()); 36 | return; 37 | } 38 | 39 | var proxyRequest = new HttpRequestMessage(new HttpMethod(context.Request.Method), uri); 40 | SetProxyRequestBody(proxyRequest, context); 41 | SetProxyRequestHeaders(proxyRequest, context); 42 | 43 | matchedRule.Modifier.Invoke(proxyRequest, context.User); 44 | 45 | proxyRequest.Headers.Host = !proxyRequest.RequestUri.IsDefaultPort 46 | ? $"{proxyRequest.RequestUri.Host}:{proxyRequest.RequestUri.Port}" 47 | : proxyRequest.RequestUri.Host; 48 | 49 | try { 50 | await ProxyTheRequest(context, proxyRequest, matchedRule); 51 | } 52 | catch (HttpRequestException) { 53 | context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; 54 | } 55 | _options.Reporter.Invoke(resultBuilder.Proxied(proxyRequest.RequestUri, context.Response.StatusCode)); 56 | } 57 | 58 | private async Task ProxyTheRequest(HttpContext context, HttpRequestMessage proxyRequest, ProxyRule proxyRule) { 59 | using (var responseMessage = await _httpClient.SendAsync(proxyRequest, 60 | HttpCompletionOption.ResponseHeadersRead, 61 | context.RequestAborted)) { 62 | 63 | if(proxyRule.PreProcessResponse || proxyRule.ResponseModifier == null) { 64 | context.Response.StatusCode = (int)responseMessage.StatusCode; 65 | context.Response.ContentType = responseMessage.Content?.Headers.ContentType?.MediaType; 66 | foreach (var header in responseMessage.Headers) { 67 | context.Response.Headers[header.Key] = header.Value.ToArray(); 68 | } 69 | // SendAsync removes chunking from the response. 70 | // This removes the header so it doesn't expect a chunked response. 71 | context.Response.Headers.Remove("transfer-encoding"); 72 | 73 | if (responseMessage.Content != null) { 74 | foreach (var contentHeader in responseMessage.Content.Headers) { 75 | context.Response.Headers[contentHeader.Key] = contentHeader.Value.ToArray(); 76 | } 77 | await responseMessage.Content.CopyToAsync(context.Response.Body); 78 | } 79 | } 80 | 81 | if (proxyRule.ResponseModifier != null) { 82 | await proxyRule.ResponseModifier.Invoke(responseMessage, context); 83 | } 84 | } 85 | } 86 | 87 | private static Uri GeRequestUri(HttpContext context) { 88 | var request = context.Request; 89 | var uriString = $"{request.Scheme}://{request.Host}{request.PathBase}{request.Path}{request.QueryString}"; 90 | return new Uri(uriString); 91 | } 92 | 93 | private static void SetProxyRequestBody(HttpRequestMessage requestMessage, HttpContext context) { 94 | var requestMethod = context.Request.Method; 95 | if (HttpMethods.IsGet(requestMethod) || 96 | HttpMethods.IsHead(requestMethod) || 97 | HttpMethods.IsDelete(requestMethod) || 98 | HttpMethods.IsTrace(requestMethod)) { 99 | return; 100 | } 101 | requestMessage.Content = new StreamContent(context.Request.Body); 102 | } 103 | 104 | private void SetProxyRequestHeaders(HttpRequestMessage requestMessage, HttpContext context) { 105 | foreach (var header in context.Request.Headers) { 106 | if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray())) { 107 | requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); 108 | } 109 | } 110 | 111 | if (_options.AddForwardedHeader) { 112 | requestMessage.Headers.TryAddWithoutValidation("Forwarded", $"for={context.Connection.RemoteIpAddress}"); 113 | requestMessage.Headers.TryAddWithoutValidation("Forwarded", $"host={requestMessage.Headers.Host}"); 114 | requestMessage.Headers.TryAddWithoutValidation("Forwarded", string.Format("proto={0}", context.Request.IsHttps ? "https" : "http")); 115 | } 116 | } 117 | 118 | private bool UserIsAuthenticated(HttpContext context) { 119 | return context.User.Identities.FirstOrDefault()?.IsAuthenticated ?? false; 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /test/SharpReverseProxy.Tests/ProxyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Options; 5 | using NUnit.Framework; 6 | using SharpReverseProxy.Tests.HttpContextFakes; 7 | using System.IO; 8 | using System.Net.Http; 9 | using System.Text; 10 | using System.Net.Http.Headers; 11 | 12 | namespace SharpReverseProxy.Tests { 13 | public class ProxyTests { 14 | private HttpContextFake _context; 15 | private FakeHttpMessageHandler _fakeHttpMessageHandler; 16 | private ProxyMiddleware _proxy; 17 | private ProxyOptions _proxyOptions; 18 | private HttpRequestFake _request; 19 | private HttpResponseFake _response; 20 | private List _rules; 21 | 22 | [SetUp] 23 | public void SetUp() { 24 | _rules = new List(); 25 | _fakeHttpMessageHandler = new FakeHttpMessageHandler(); 26 | _request = new HttpRequestFake(new Uri("http://myserver.com/api/user")); 27 | _response = new HttpResponseFake(); 28 | _context = new HttpContextFake(_request, _response); 29 | _proxyOptions = new ProxyOptions(_rules); 30 | _proxyOptions.BackChannelMessageHandler = _fakeHttpMessageHandler; 31 | 32 | var options = Options.Create(_proxyOptions); 33 | _proxy = new ProxyMiddleware(next => Task.FromResult(_request), options); 34 | } 35 | 36 | [Test] 37 | public async Task Should_match_simple_rule() { 38 | var matched = false; 39 | _rules.Add(new ProxyRule { 40 | Matcher = uri => uri.AbsolutePath.Contains("api"), 41 | Modifier = (msg, user) => { matched = true; } 42 | }); 43 | await _proxy.Invoke(_context); 44 | Assert.IsTrue(matched); 45 | } 46 | 47 | [Test] 48 | public async Task Should_not_match_any_rule() { 49 | var matched = false; 50 | ProxyResult result = null; 51 | _proxyOptions.Reporter = r => result = r; 52 | _rules.Add(new ProxyRule { 53 | Matcher = uri => false, 54 | Modifier = (msg, user) => { matched = true; } 55 | }); 56 | await _proxy.Invoke(_context); 57 | Assert.IsFalse(matched); 58 | Assert.IsNotNull(result); 59 | Assert.AreEqual(ProxyStatus.NotProxied, result.ProxyStatus); 60 | Assert.AreEqual(_request.Uri, result.OriginalUri); 61 | } 62 | 63 | [Test] 64 | public async Task Should_call_reporter_when_request_is_proxied() { 65 | var targetUri = new Uri("http://myotherserver.com/api/user"); 66 | ProxyResult result = null; 67 | _rules.Add(new ProxyRule { 68 | Matcher = uri => uri.AbsolutePath.Contains("api"), 69 | Modifier = (msg, user) => { msg.RequestUri = targetUri; } 70 | }); 71 | _proxyOptions.Reporter = r => result = r; 72 | await _proxy.Invoke(_context); 73 | 74 | Assert.IsNotNull(result); 75 | Assert.AreEqual(ProxyStatus.Proxied, result.ProxyStatus); 76 | Assert.AreEqual(_request.Uri, result.OriginalUri); 77 | Assert.AreEqual(targetUri, result.ProxiedUri); 78 | } 79 | 80 | [Test] 81 | public async Task Should_pass_all_headers() { 82 | _rules.Add(new ProxyRule { 83 | Matcher = uri => uri.AbsolutePath.Contains("api"), 84 | Modifier = (msg, user) => { } 85 | }); 86 | await _proxy.Invoke(_context); 87 | 88 | _fakeHttpMessageHandler.ResponseMessageToReturn.Headers.Add("SomeHeader", "SomeValue"); 89 | 90 | foreach (var header in _request.Headers) { 91 | var respHeader = _response.Headers[header.Key]; 92 | Assert.AreEqual(header.Value.ToString(), respHeader.ToString()); 93 | } 94 | } 95 | 96 | [Test] 97 | public async Task Should_pass_contentType() { 98 | _rules.Add(new ProxyRule { 99 | Matcher = uri => uri.AbsolutePath.Contains("api") 100 | }); 101 | _fakeHttpMessageHandler.ResponseMessageToReturn.Content = 102 | new MultipartFormDataContent { 103 | Headers = { 104 | ContentType = MediaTypeHeaderValue.Parse("application/json") 105 | } 106 | }; 107 | await _proxy.Invoke(_context); 108 | Assert.AreEqual("application/json", _context.Response.ContentType); 109 | } 110 | 111 | [Test] 112 | public async Task Should_call_responseModifier_if_set() { 113 | _rules.Add(new ProxyRule { 114 | Matcher = uri => uri.AbsolutePath.Contains("api"), 115 | Modifier = (msg, user) => { }, 116 | ResponseModifier = async (res, ctx) => { 117 | var bytes = Encoding.UTF8.GetBytes("Hello, world!"); 118 | await ctx.Response.Body.WriteAsync(bytes, 0, bytes.Length); 119 | } 120 | }); 121 | await _proxy.Invoke(_context); 122 | 123 | var body = (MemoryStream)_response.Body; 124 | var bodyText = Encoding.UTF8.GetString(body.ToArray()); 125 | Assert.AreEqual("Hello, world!", bodyText); 126 | } 127 | 128 | [Test] 129 | public async Task Should_provide_clean_response_if_preProcessResponse_is_false() { 130 | _fakeHttpMessageHandler.ResponseMessageToReturn 131 | .Headers.Add("foo", "bar"); 132 | 133 | _rules.Add(new ProxyRule { 134 | Matcher = uri => uri.AbsolutePath.Contains("api"), 135 | PreProcessResponse = false, 136 | ResponseModifier = async (res, ctx) => { 137 | var bytes = Encoding.UTF8.GetBytes("Hello, world!"); 138 | await ctx.Response.Body.WriteAsync(bytes, 0, bytes.Length); 139 | } 140 | }); 141 | await _proxy.Invoke(_context); 142 | 143 | Assert.AreEqual(0, _context.Response.Headers.Count); 144 | } 145 | 146 | [Test] 147 | public async Task Should_pass_full_response_if_preProcessResponse_is_false_and_no_responseModifier_is_provided() { 148 | _fakeHttpMessageHandler.ResponseMessageToReturn 149 | .Headers.Add("foo", "bar"); 150 | 151 | _rules.Add(new ProxyRule { 152 | Matcher = uri => uri.AbsolutePath.Contains("api"), 153 | PreProcessResponse = false 154 | }); 155 | await _proxy.Invoke(_context); 156 | 157 | Assert.AreEqual(1, _context.Response.Headers.Count); 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SharpReverseProxy 2 | ================= 3 | 4 | Powerful Reverse Proxy written as OWIN Middleware. Perfect for ASP .NET, Web API, microservices, and more. 5 | 6 | Looking for a way to build an API Gateway based on rules, I found the [Asp.Net Proxy repository](https://github.com/aspnet/Proxy). 7 | 8 | The problem is that it proxies all request and I would like to have granular control of the proxy rules. So I wrote SharpReverseProxy. 😃 9 | 10 | 11 | [![Build status](https://ci.appveyor.com/api/projects/status/b8y5k1vxwybsdj1s?svg=true)](https://ci.appveyor.com/project/Andre/sharpreverseproxy) 12 | 13 | 14 | ## How to Use 15 | 16 | Install the [SharpReverseProxy package](https://www.nuget.org/packages/SharpReverseProxy/) via Nuget: 17 | 18 | ```powershell 19 | Install-Package SharpReverseProxy 20 | ``` 21 | 22 | Open your *Startup.cs* and configure your reverse proxy: 23 | 24 | ```csharp 25 | public void Configure(IApplicationBuilder app, 26 | IHostingEnvironment env, 27 | ILoggerFactory loggerFactory) { 28 | 29 | app.UseProxy(new List { 30 | new ProxyRule { 31 | Matcher = uri => uri.AbsoluteUri.Contains("/api/"), 32 | Modifier = (req, user) => { 33 | var match = Regex.Match(req.RequestUri.AbsolutePath, "/api/(.+)service"); 34 | req.RequestUri = new Uri(string.Format("http://{0}.{1}/{2}", 35 | match.Groups[1].Value, 36 | req.RequestUri.Host, 37 | req.RequestUri.AbsolutePath.Replace(match.Value, "/api/") 38 | )); 39 | }, 40 | RequiresAuthentication = true 41 | } 42 | }, 43 | r => { 44 | _logger.LogDebug($"Proxy: {r.ProxyStatus} Url: {r.OriginalUri} Time: {r.Elapsed}"); 45 | if (r.ProxyStatus == ProxyStatus.Proxied) { 46 | _logger.LogDebug($" New Url: {r.ProxiedUri.AbsoluteUri} Status: {r.HttpStatusCode}"); 47 | } 48 | }); 49 | } 50 | ``` 51 | 52 | ### Explanation: 53 | 54 | Add a proxy rule. You can create as many as you want and the proxy will use the first matched rule to divert the request. 55 | For every rule, define the matcher, the modifier and optinally a response modifier. 56 | 57 | #### Matcher 58 | 59 | `Func Matcher`: responsible for selecting which request will be handled by this rule. Simply analyse the Uri and return true/false. 60 | 61 | #### Modifier 62 | 63 | `Action Modifier`: responsible for modifying the proxied request. 64 | 65 | In the code below, we are adding the following rule: 66 | 67 | 1. Find urls with `/api1`. Ex: `http://www.noplace.com/api/[servicename]service/` 68 | 2. Proxy the request to: `http://[servicename].noplace.com/api/` 69 | 70 | ```csharp 71 | new ProxyRule { 72 | Matcher = uri => uri.AbsoluteUri.Contains("/api/"), 73 | Modifier = uri => { 74 | var match = Regex.Match(uri.Path, "/api/(.+)service"); 75 | uri.Host = match.Groups[1].Value + "." + uri.Host; 76 | uri.Path = uri.Path.Replace(match.Value, "/api/"); 77 | }, 78 | RequiresAuthentication = true 79 | } 80 | ``` 81 | 82 | ##### Authentication 83 | 84 | If you set `RequiresAuthentication = true`, the proxy will only act if the user is authenticated; otherwise, a 401 status code will be sent back and the request ends there. Just make sure to add your authentication middleware before adding the proxy one in the pipeline. 85 | 86 | You have total control over proxying requests: have fun! 😃 87 | 88 | 89 | #### Response Modifier 90 | 91 | [Version 1.3](https://www.nuget.org/packages/SharpReverseProxy/1.3.0) adds the ability to modify the response providing a **ResponseModifier**. Thank you so much for [@vsimonia](https://github.com/vsimonian) for this PR :) 92 | 93 | This is extremely useful when: 94 | 95 | - Proxying a service that has its URL hardcoded, which needs to be replaced with the proxy URL so that links and references function properly. 96 | - Modifying the behaviour of an existing application or service in situations where there is no alternative. 97 | 98 | Here's an example of usage: 99 | 100 | 101 | ```csharp 102 | new ProxyRule { 103 | // ... 104 | Modifier = (req, user) => { 105 | req.RequestUri = new Uri( 106 | $"https://www.example.com{req.RequestUri.PathAndQuery}" 107 | ); 108 | }, 109 | ResponseModifier = async (msg, ctx) => 110 | { 111 | ctx.Response.Headers.Remove("Strict-Transport-Security"); 112 | ctx.Response.Headers.Remove("Content-Security-Policy"); 113 | if (msg.StatusCode == System.Net.HttpStatusCode.OK) 114 | { 115 | switch (msg.Content.Headers.ContentType.MediaType) 116 | { 117 | case "text/html": 118 | case "application/xhtml+xml": 119 | case "application/javascript": 120 | case "text/css": 121 | var body = Regex.Replace( 122 | await msg.Content.ReadAsStringAsync(), 123 | @"(http(s)?:)?//(?:www\.)?example.com", 124 | string.Format( 125 | "{0}://{1}", 126 | ctx.Request.Scheme, 127 | ctx.Request.Host 128 | ), 129 | RegexOptions.IgnoreCase 130 | ); 131 | ctx.Response.ContentType = msg.Content.Headers.ContentType?.MediaType; 132 | byte[] data = Encoding.UTF8.GetBytes(body); 133 | ctx.Response.ContentLength = data.Length; 134 | await ctx.Response.Body.WriteAsync(data, 0, data.Length); 135 | break; 136 | default: 137 | await msg.Content.CopyToAsync(ctx.Response.Body); 138 | break; 139 | } 140 | } 141 | else 142 | { 143 | await msg.Content.CopyToAsync(ctx.Response.Body); 144 | } 145 | } 146 | } 147 | ``` 148 | 149 | ##### Skipping the default operations 150 | 151 | This version also adds the `PreProcessResponse` boolean, with a default value of `true`. If set to `false`, and a delegate is specified for `ResponseModifier`, default operations that modify the response sent to the user agent (such as copying headers from the originating server) are skipped and you have to do all the work yourself. 152 | 153 | 154 | #### Reporter 155 | 156 | After every request, a `ProxyResult` is returned so you can log/take actions about what happened. 157 | 158 | `Action Reporter`: returns request information. 159 | 160 | In the code below, we show the request URL, if it was proxied, and the time it took. When proxied, we also log the new URL and the status code. 161 | 162 | ```csharp 163 | proxyOptions.Reporter = r => { 164 | logger.LogDebug($"Proxy: {r.ProxyStatus} Url: {r.OriginalUri} Time: {r.Elapsed}"); 165 | if (r.ProxyStatus == ProxyStatus.Proxied) { 166 | logger.LogDebug($" New Url: {r.ProxiedUri.AbsoluteUri} Status: {r.HttpStatusCode}"); 167 | } 168 | }; 169 | ``` 170 | 171 | 172 | And that's it! 173 | 174 | Heavily inspired by https://github.com/aspnet/Proxy 175 | 176 | ## Licence 177 | 178 | MIT 179 | --------------------------------------------------------------------------------