diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3729ff0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/CloudFlareReverseProxy.sln b/CloudFlareReverseProxy.sln new file mode 100644 index 0000000..9c3b5a6 --- /dev/null +++ b/CloudFlareReverseProxy.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31911.196 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CloudFlareReverseProxy", "CloudFlareReverseProxy\CloudFlareReverseProxy.csproj", "{EA43FF3F-964F-4982-8C79-B4DB77AB7FEF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EA43FF3F-964F-4982-8C79-B4DB77AB7FEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA43FF3F-964F-4982-8C79-B4DB77AB7FEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA43FF3F-964F-4982-8C79-B4DB77AB7FEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA43FF3F-964F-4982-8C79-B4DB77AB7FEF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1983598E-E53B-4CDD-A7FA-E15EB8F3DB9A} + EndGlobalSection +EndGlobal diff --git a/CloudFlareReverseProxy/CloudFlareReverseProxy.csproj b/CloudFlareReverseProxy/CloudFlareReverseProxy.csproj new file mode 100644 index 0000000..3e61aba --- /dev/null +++ b/CloudFlareReverseProxy/CloudFlareReverseProxy.csproj @@ -0,0 +1,14 @@ + + + + net5.0 + fe0043e8-ea3b-4937-b2a1-fb2af7fe92d6 + Linux + + + + + + + + diff --git a/CloudFlareReverseProxy/Dockerfile b/CloudFlareReverseProxy/Dockerfile new file mode 100644 index 0000000..74eae50 --- /dev/null +++ b/CloudFlareReverseProxy/Dockerfile @@ -0,0 +1,22 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build +WORKDIR /src +COPY ["CloudFlareReverseProxy/CloudFlareReverseProxy.csproj", "CloudFlareReverseProxy/"] +RUN dotnet restore "CloudFlareReverseProxy/CloudFlareReverseProxy.csproj" +COPY . . +WORKDIR "/src/CloudFlareReverseProxy" +RUN dotnet build "CloudFlareReverseProxy.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "CloudFlareReverseProxy.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "CloudFlareReverseProxy.dll"] \ No newline at end of file diff --git a/CloudFlareReverseProxy/Program.cs b/CloudFlareReverseProxy/Program.cs new file mode 100644 index 0000000..53220b1 --- /dev/null +++ b/CloudFlareReverseProxy/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CloudFlareReverseProxy +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/CloudFlareReverseProxy/Properties/launchSettings.json b/CloudFlareReverseProxy/Properties/launchSettings.json new file mode 100644 index 0000000..56f42da --- /dev/null +++ b/CloudFlareReverseProxy/Properties/launchSettings.json @@ -0,0 +1,35 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:40939", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "CloudFlareReverseProxy": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": "true", + "applicationUrl": "https://localhost:5001;http://localhost:5000" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "publishAllPorts": true, + "useSSL": true + } + } +} \ No newline at end of file diff --git a/CloudFlareReverseProxy/ReverseProxyMiddleware.cs b/CloudFlareReverseProxy/ReverseProxyMiddleware.cs new file mode 100644 index 0000000..dc1e197 --- /dev/null +++ b/CloudFlareReverseProxy/ReverseProxyMiddleware.cs @@ -0,0 +1,167 @@ +using FlareSolverrSharp; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace CloudFlareReverseProxy +{ + public class ReverseProxyMiddleware + { + private static HttpClient _httpClient; + private readonly RequestDelegate _nextMiddleware; + + public ReverseProxyMiddleware(RequestDelegate nextMiddleware, IConfiguration configuration) + { + _nextMiddleware = nextMiddleware; + var handler = new ClearanceHandler(configuration["FlareSolverrApiUrl"]) + { + MaxTimeout = 60000 + }; + _httpClient = new HttpClient(handler); + } + + public async Task Invoke(HttpContext context) + { + var targetUri = BuildTargetUri(context.Request); + + if (targetUri != null) + { + var targetRequestMessage = CreateTargetMessage(context, targetUri); + + using (var responseMessage = await _httpClient.SendAsync(targetRequestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted)) + { + context.Response.StatusCode = (int)responseMessage.StatusCode; + + CopyFromTargetResponseHeaders(context, responseMessage); + + await ProcessResponseContent(context, responseMessage); + } + + return; + } + + await _nextMiddleware(context); + } + + private async Task ProcessResponseContent(HttpContext context, HttpResponseMessage responseMessage) + { + var content = await responseMessage.Content.ReadAsByteArrayAsync(); + + if (IsContentOfType(responseMessage, "text/html") || IsContentOfType(responseMessage, "text/javascript")) + { + var stringContent = Encoding.UTF8.GetString(content); + var newContent = stringContent.Replace("https://www.google.com", "/google") + .Replace("https://www.gstatic.com", "/googlestatic") + .Replace("https://docs.google.com/forms", "/googleforms"); + await context.Response.WriteAsync(newContent, Encoding.UTF8); + } + else + { + await context.Response.Body.WriteAsync(content); + } + } + + private bool IsContentOfType(HttpResponseMessage responseMessage, string type) + { + var result = false; + + if (responseMessage.Content?.Headers?.ContentType != null) + { + result = responseMessage.Content.Headers.ContentType.MediaType == type; + } + + return result; + } + + private HttpRequestMessage CreateTargetMessage(HttpContext context, Uri targetUri) + { + var requestMessage = new HttpRequestMessage(); + CopyFromOriginalRequestContentAndHeaders(context, requestMessage); + + //targetUri = new Uri(QueryHelpers.AddQueryString(targetUri.OriginalString, new Dictionary() { { "entry.1884265043", "John Doe" } })); + + requestMessage.RequestUri = targetUri; + requestMessage.Headers.Host = targetUri.Host; + requestMessage.Method = GetMethod(context.Request.Method); + + return requestMessage; + } + + private void CopyFromOriginalRequestContentAndHeaders(HttpContext context, HttpRequestMessage requestMessage) + { + var requestMethod = context.Request.Method; + + if (!HttpMethods.IsGet(requestMethod) && + !HttpMethods.IsHead(requestMethod) && + !HttpMethods.IsDelete(requestMethod) && + !HttpMethods.IsTrace(requestMethod)) + { + var streamContent = new StreamContent(context.Request.Body); + requestMessage.Content = streamContent; + } + + foreach (var header in context.Request.Headers) + { + requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); + } + } + + private void CopyFromTargetResponseHeaders(HttpContext context, HttpResponseMessage responseMessage) + { + foreach (var header in responseMessage.Headers) + { + context.Response.Headers[header.Key] = header.Value.ToArray(); + } + + foreach (var header in responseMessage.Content.Headers) + { + context.Response.Headers[header.Key] = header.Value.ToArray(); + } + context.Response.Headers.Remove("transfer-encoding"); + } + private static HttpMethod GetMethod(string method) + { + if (HttpMethods.IsDelete(method)) return HttpMethod.Delete; + if (HttpMethods.IsGet(method)) return HttpMethod.Get; + if (HttpMethods.IsHead(method)) return HttpMethod.Head; + if (HttpMethods.IsOptions(method)) return HttpMethod.Options; + if (HttpMethods.IsPost(method)) return HttpMethod.Post; + if (HttpMethods.IsPut(method)) return HttpMethod.Put; + if (HttpMethods.IsTrace(method)) return HttpMethod.Trace; + return new HttpMethod(method); + } + + private Uri BuildTargetUri(HttpRequest request) + { + Uri targetUri = null; + //PathString remainingPath; + + /* + if (request.Path.StartsWithSegments("/googleforms", out remainingPath)) + { + targetUri = new Uri("https://docs.google.com/forms" + remainingPath); + } + + if (request.Path.StartsWithSegments("/google", out remainingPath)) + { + targetUri = new Uri("https://www.google.com" + remainingPath); + } + + if (request.Path.StartsWithSegments("/googlestatic", out remainingPath)) + { + targetUri = new Uri(" https://www.gstatic.com" + remainingPath); + } + */ + if (!request.Query["url"].Any()) + return targetUri; + else + return new Uri(Uri.UnescapeDataString(request.Query["url"].ToString())); + } + } +} diff --git a/CloudFlareReverseProxy/Startup.cs b/CloudFlareReverseProxy/Startup.cs new file mode 100644 index 0000000..7ac5349 --- /dev/null +++ b/CloudFlareReverseProxy/Startup.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CloudFlareReverseProxy +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + //app.UseRouting(); + app.UseMiddleware(); + + /* + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/", async context => + { + await context.Response.WriteAsync("Hello World!"); + }); + }); + */ + } + } +} diff --git a/CloudFlareReverseProxy/appsettings.Development.json b/CloudFlareReverseProxy/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/CloudFlareReverseProxy/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/CloudFlareReverseProxy/appsettings.json b/CloudFlareReverseProxy/appsettings.json new file mode 100644 index 0000000..4652911 --- /dev/null +++ b/CloudFlareReverseProxy/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "FlareSolverrApiUrl": "http://basilisk.chrispr.lan:8191" +}