HttpProxyService: Forward https, modify http
This commit is contained in:
@@ -1,47 +1,78 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace UmlautAdaptarr.Services
|
namespace UmlautAdaptarr.Services
|
||||||
{
|
{
|
||||||
public class HttpProxyService : IHostedService
|
public class HttpProxyService : IHostedService
|
||||||
{
|
{
|
||||||
private HttpListener _listener;
|
private TcpListener _listener;
|
||||||
private readonly IHttpClientFactory _clientFactory;
|
|
||||||
private readonly ILogger<HttpProxyService> _logger;
|
private readonly ILogger<HttpProxyService> _logger;
|
||||||
private const int PROXY_PORT = 5006; // TODO move to appsettings.json
|
private readonly int _proxyPort = 5006; // TODO move to appsettings.json
|
||||||
|
private readonly IHttpClientFactory _clientFactory;
|
||||||
|
|
||||||
public HttpProxyService(IHttpClientFactory clientFactory, ILogger<HttpProxyService> logger)
|
public HttpProxyService(ILogger<HttpProxyService> logger, IHttpClientFactory clientFactory)
|
||||||
{
|
{
|
||||||
_clientFactory = clientFactory;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_clientFactory = clientFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleRequests()
|
private async Task HandleRequests(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
while (_listener.IsListening)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
var clientSocket = await _listener.AcceptSocketAsync();
|
||||||
{
|
_ = Task.Run(() => ProcessRequest(clientSocket), stoppingToken);
|
||||||
var context = await _listener.GetContextAsync();
|
|
||||||
await ProcessRequest(context);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError($"Error handling request: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessRequest(HttpListenerContext context)
|
private async Task ProcessRequest(Socket clientSocket)
|
||||||
{
|
{
|
||||||
var request = context.Request;
|
using var clientStream = new NetworkStream(clientSocket, ownsSocket: true);
|
||||||
var response = context.Response;
|
var buffer = new byte[8192];
|
||||||
|
var bytesRead = await clientStream.ReadAsync(buffer, 0, buffer.Length);
|
||||||
|
var requestString = Encoding.ASCII.GetString(buffer, 0, bytesRead);
|
||||||
|
|
||||||
|
if (requestString.StartsWith("CONNECT"))
|
||||||
|
{
|
||||||
|
// Handle HTTPS CONNECT request
|
||||||
|
await HandleHttpsConnect(requestString, clientStream, clientSocket);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Handle HTTP request
|
||||||
|
await HandleHttp(requestString, clientStream, clientSocket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleHttpsConnect(string requestString, NetworkStream clientStream, Socket clientSocket)
|
||||||
|
{
|
||||||
|
var targetInfo = ParseTargetInfo(requestString);
|
||||||
|
if (targetInfo.host != "prowlarr.servarr.com")
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"Indexer {targetInfo.host} needs to be set to http:// instead of https://");
|
||||||
|
}
|
||||||
|
using var targetSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var originalUri = new Uri(request.RawUrl);
|
await targetSocket.ConnectAsync(targetInfo.host, targetInfo.port);
|
||||||
var modifiedUri = "http://localhost:5005/_/" + originalUri.Host + originalUri.PathAndQuery; // TODO read port from appsettings?
|
await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 200 Connection Established\r\n\r\n"));
|
||||||
|
using var targetStream = new NetworkStream(targetSocket, ownsSocket: true);
|
||||||
|
await RelayTraffic(clientStream, targetStream);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError($"Failed to connect to target: {ex.Message}");
|
||||||
|
clientSocket.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Act as a proxy and forward the modified request to internal endpoints
|
private async Task HandleHttp(string requestString, NetworkStream clientStream, Socket clientSocket)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var uri = new Uri(requestString.Split(' ')[1]);
|
||||||
|
var modifiedUri = $"http://localhost:5005/_/{uri.Host}{uri.PathAndQuery}"; // TODO read port from appsettings?
|
||||||
using var client = _clientFactory.CreateClient();
|
using var client = _clientFactory.CreateClient();
|
||||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, modifiedUri);
|
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, modifiedUri);
|
||||||
var result = await client.SendAsync(httpRequestMessage);
|
var result = await client.SendAsync(httpRequestMessage);
|
||||||
@@ -49,36 +80,60 @@ namespace UmlautAdaptarr.Services
|
|||||||
if (result.IsSuccessStatusCode)
|
if (result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var responseData = await result.Content.ReadAsByteArrayAsync();
|
var responseData = await result.Content.ReadAsByteArrayAsync();
|
||||||
response.ContentLength64 = responseData.Length;
|
await clientStream.WriteAsync(Encoding.ASCII.GetBytes($"HTTP/1.1 200 OK\r\nContent-Length: {responseData.Length}\r\n\r\n"));
|
||||||
await response.OutputStream.WriteAsync(responseData);
|
await clientStream.WriteAsync(responseData);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
response.StatusCode = (int)result.StatusCode;
|
await clientStream.WriteAsync(Encoding.ASCII.GetBytes($"HTTP/1.1 {result.StatusCode}\r\n\r\n"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError($"HTTP Proxy error: {ex.Message}");
|
_logger.LogError($"HTTP Proxy error: {ex.Message}");
|
||||||
response.StatusCode = 500;
|
await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 500 Internal Server Error\r\n\r\n"));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
response.OutputStream.Close();
|
clientSocket.Close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private (string host, int port) ParseTargetInfo(string requestLine)
|
||||||
|
{
|
||||||
|
var parts = requestLine.Split(' ')[1].Split(':');
|
||||||
|
return (parts[0], int.Parse(parts[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RelayTraffic(NetworkStream clientStream, NetworkStream targetStream)
|
||||||
|
{
|
||||||
|
var clientToTargetTask = RelayStream(clientStream, targetStream);
|
||||||
|
var targetToClientTask = RelayStream(targetStream, clientStream);
|
||||||
|
await Task.WhenAll(clientToTargetTask, targetToClientTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RelayStream(NetworkStream input, NetworkStream output)
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = await input.ReadAsync(buffer.AsMemory(0, buffer.Length))) > 0)
|
||||||
|
{
|
||||||
|
await output.WriteAsync(buffer.AsMemory(0, bytesRead));
|
||||||
|
await output.FlushAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_listener = new HttpListener();
|
_listener = new TcpListener(IPAddress.Any, _proxyPort);
|
||||||
_listener.Prefixes.Add($"http://*:{PROXY_PORT}/");
|
|
||||||
_listener.Start();
|
_listener.Start();
|
||||||
Task.Run(HandleRequests, cancellationToken);
|
Task.Run(() => HandleRequests(cancellationToken), cancellationToken);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_listener.Stop();
|
_listener.Stop();
|
||||||
_listener.Close();
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user