336 lines
9.1 KiB
C#
336 lines
9.1 KiB
C#
using System;
|
||
using System.Net;
|
||
using System.Net.Http;
|
||
using System.Net.Sockets;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.IO;
|
||
using System.Runtime.InteropServices;
|
||
|
||
class Program
|
||
{
|
||
static string USER = "user";
|
||
static string PASS = "pass";
|
||
|
||
static async Task Main()
|
||
{
|
||
int port = 8888;
|
||
|
||
Console.WriteLine($"Local proxy:");
|
||
Console.WriteLine($" http://127.0.0.1:{port}");
|
||
Console.WriteLine($" login: {USER}");
|
||
Console.WriteLine($" pass : {PASS}");
|
||
|
||
var listener = new TcpListener(IPAddress.Any, port);
|
||
listener.Start();
|
||
|
||
while (true)
|
||
{
|
||
var client = await listener.AcceptTcpClientAsync();
|
||
_ = Task.Run(() => HandleClient(client));
|
||
}
|
||
}
|
||
|
||
static async Task HandleClient(TcpClient client)
|
||
{
|
||
using (client)
|
||
{
|
||
var stream = client.GetStream();
|
||
|
||
var request = await ReadHeaders(stream);
|
||
if (request == null) return;
|
||
|
||
if (!CheckAuth(request.HeadersRaw))
|
||
{
|
||
await Write407(stream);
|
||
return;
|
||
}
|
||
|
||
if (request.Method == "CONNECT")
|
||
{
|
||
await HandleConnect(stream, request.Target);
|
||
}
|
||
else
|
||
{
|
||
await HandleHttp(stream, request);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ================= HTTP =================
|
||
|
||
static async Task HandleHttp(NetworkStream clientStream, HttpRequestData req)
|
||
{
|
||
DefaultProxyCredentials = CredentialCache.DefaultCredentials
|
||
var handler = new SocketsHttpHandler
|
||
{
|
||
UseProxy = true,
|
||
Proxy = new WinHttpProxy(),
|
||
DefaultProxyCredentials = CredentialCache.DefaultCredentials,
|
||
AllowAutoRedirect = false
|
||
};
|
||
|
||
using var http = new HttpClient(handler);
|
||
|
||
var message = new HttpRequestMessage(new HttpMethod(req.Method), req.Url);
|
||
|
||
foreach (var h in req.Headers)
|
||
{
|
||
if (!h.Key.Equals("Proxy-Authorization", StringComparison.OrdinalIgnoreCase))
|
||
message.Headers.TryAddWithoutValidation(h.Key, h.Value);
|
||
}
|
||
|
||
if (req.Body != null)
|
||
message.Content = new StreamContent(req.Body);
|
||
|
||
var resp = await http.SendAsync(message, HttpCompletionOption.ResponseHeadersRead);
|
||
|
||
// статус
|
||
var statusLine = $"HTTP/1.1 {(int)resp.StatusCode} {resp.ReasonPhrase}\r\n";
|
||
await clientStream.WriteAsync(Encoding.ASCII.GetBytes(statusLine));
|
||
|
||
// заголовки
|
||
foreach (var h in resp.Headers)
|
||
{
|
||
foreach (var v in h.Value)
|
||
await WriteHeader(clientStream, h.Key, v);
|
||
}
|
||
|
||
foreach (var h in resp.Content.Headers)
|
||
{
|
||
foreach (var v in h.Value)
|
||
await WriteHeader(clientStream, h.Key, v);
|
||
}
|
||
|
||
await clientStream.WriteAsync(Encoding.ASCII.GetBytes("\r\n"));
|
||
|
||
// тело (стриминг)
|
||
await resp.Content.CopyToAsync(clientStream);
|
||
}
|
||
|
||
// ================= CONNECT =================
|
||
|
||
static async Task HandleConnect(NetworkStream clientStream, string target)
|
||
{
|
||
var parts = target.Split(':');
|
||
string host = parts[0];
|
||
int port = int.Parse(parts[1]);
|
||
|
||
var proxy = new WinHttpProxy();
|
||
var proxyUri = proxy.GetProxy(new Uri($"https://{host}:{port}"));
|
||
|
||
using var upstream = new TcpClient();
|
||
await upstream.ConnectAsync(proxyUri.Host, proxyUri.Port);
|
||
|
||
var upstreamStream = upstream.GetStream();
|
||
|
||
// CONNECT к upstream proxy
|
||
string connectReq =
|
||
$"CONNECT {host}:{port} HTTP/1.1\r\n" +
|
||
$"Host: {host}:{port}\r\n" +
|
||
$"Proxy-Connection: Keep-Alive\r\n\r\n";
|
||
|
||
await upstreamStream.WriteAsync(Encoding.ASCII.GetBytes(connectReq));
|
||
|
||
// читаем ответ upstream полностью до \r\n\r\n
|
||
var resp = await ReadHeaders(upstreamStream);
|
||
|
||
if (!resp.HeadersRaw.Contains("200"))
|
||
{
|
||
await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 502 Bad Gateway\r\n\r\n"));
|
||
return;
|
||
}
|
||
|
||
// говорим клиенту ОК
|
||
await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 200 Connection Established\r\n\r\n"));
|
||
|
||
// туннель
|
||
var t1 = Pump(upstreamStream, clientStream);
|
||
var t2 = Pump(clientStream, upstreamStream);
|
||
|
||
await Task.WhenAny(t1, t2);
|
||
}
|
||
|
||
// ================= AUTH =================
|
||
|
||
static bool CheckAuth(string headers)
|
||
{
|
||
string expected = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{USER}:{PASS}"));
|
||
return headers.Contains($"Proxy-Authorization: Basic {expected}");
|
||
}
|
||
|
||
static async Task Write407(NetworkStream stream)
|
||
{
|
||
string resp =
|
||
"HTTP/1.1 407 Proxy Authentication Required\r\n" +
|
||
"Proxy-Authenticate: Basic realm=\"proxy\"\r\n\r\n";
|
||
|
||
await stream.WriteAsync(Encoding.ASCII.GetBytes(resp));
|
||
}
|
||
|
||
// ================= REQUEST PARSER =================
|
||
|
||
class HttpRequestData
|
||
{
|
||
public string Method;
|
||
public string Url;
|
||
public string Target;
|
||
public string HeadersRaw;
|
||
public Dictionary<string, string> Headers = new();
|
||
public Stream Body;
|
||
}
|
||
|
||
static async Task<HttpRequestData> ReadHeaders(NetworkStream stream)
|
||
{
|
||
var buffer = new byte[8192];
|
||
int read = await stream.ReadAsync(buffer);
|
||
|
||
if (read <= 0) return null;
|
||
|
||
string text = Encoding.ASCII.GetString(buffer, 0, read);
|
||
|
||
var lines = text.Split("\r\n");
|
||
var first = lines[0].Split(' ');
|
||
|
||
var data = new HttpRequestData
|
||
{
|
||
Method = first[0],
|
||
Target = first[1],
|
||
HeadersRaw = text
|
||
};
|
||
|
||
foreach (var line in lines.Skip(1))
|
||
{
|
||
if (string.IsNullOrWhiteSpace(line)) break;
|
||
|
||
int idx = line.IndexOf(':');
|
||
if (idx > 0)
|
||
data.Headers[line[..idx]] = line[(idx + 1)..].Trim();
|
||
}
|
||
|
||
if (data.Method != "CONNECT")
|
||
{
|
||
data.Url = data.Target.StartsWith("http")
|
||
? data.Target
|
||
: $"http://{data.Headers["Host"]}{data.Target}";
|
||
}
|
||
|
||
return data;
|
||
}
|
||
|
||
static async Task WriteHeader(NetworkStream stream, string key, string value)
|
||
{
|
||
string line = $"{key}: {value}\r\n";
|
||
await stream.WriteAsync(Encoding.ASCII.GetBytes(line));
|
||
}
|
||
}
|
||
|
||
// ================= WINHTTP PROXY =================
|
||
|
||
class WinHttpProxy : IWebProxy
|
||
{
|
||
public ICredentials Credentials { get; set; }
|
||
|
||
public Uri GetProxy(Uri destination)
|
||
{
|
||
return WinHttpHelper.GetProxyForUrl(destination);
|
||
}
|
||
|
||
public bool IsBypassed(Uri host) => false;
|
||
}
|
||
|
||
using System.Runtime.InteropServices;
|
||
|
||
static class WinHttpHelper
|
||
{
|
||
[DllImport("winhttp.dll", SetLastError = true)]
|
||
static extern IntPtr WinHttpOpen(string agent, int accessType, string proxy, string bypass, int flags);
|
||
|
||
[DllImport("winhttp.dll", SetLastError = true)]
|
||
static extern bool WinHttpGetProxyForUrl(
|
||
IntPtr hSession,
|
||
string url,
|
||
ref WINHTTP_AUTOPROXY_OPTIONS options,
|
||
out WINHTTP_PROXY_INFO proxyInfo);
|
||
|
||
[DllImport("winhttp.dll")]
|
||
static extern bool WinHttpCloseHandle(IntPtr handle);
|
||
|
||
public static Uri GetProxyForUrl(Uri url)
|
||
{
|
||
IntPtr session = WinHttpOpen("proxy", 0, null, null, 0);
|
||
|
||
var options = new WINHTTP_AUTOPROXY_OPTIONS
|
||
{
|
||
dwFlags = 0x00000001 | 0x00000002, // AUTO_DETECT + CONFIG_URL
|
||
dwAutoDetectFlags = 0x00000001 | 0x00000002 // DHCP + DNS_A
|
||
};
|
||
|
||
if (WinHttpGetProxyForUrl(session, url.ToString(), ref options, out var info))
|
||
{
|
||
string proxy = PtrToString(info.lpszProxy);
|
||
|
||
if (!string.IsNullOrEmpty(proxy))
|
||
{
|
||
var first = proxy.Split(';')[0];
|
||
return new Uri(Normalize(first));
|
||
}
|
||
}
|
||
|
||
WinHttpCloseHandle(session);
|
||
return url; // direct fallback
|
||
}
|
||
|
||
static string PtrToString(IntPtr ptr)
|
||
{
|
||
if (ptr == IntPtr.Zero) return null;
|
||
return Marshal.PtrToStringUni(ptr);
|
||
}
|
||
|
||
static string Normalize(string proxy)
|
||
{
|
||
if (!proxy.StartsWith("http"))
|
||
return "http://" + proxy;
|
||
return proxy;
|
||
}
|
||
|
||
struct WINHTTP_AUTOPROXY_OPTIONS
|
||
{
|
||
public int dwFlags;
|
||
public int dwAutoDetectFlags;
|
||
public IntPtr lpszAutoConfigUrl;
|
||
public IntPtr lpvReserved;
|
||
public int dwReserved;
|
||
public bool fAutoLogonIfChallenged;
|
||
}
|
||
|
||
struct WINHTTP_PROXY_INFO
|
||
{
|
||
public int dwAccessType;
|
||
public IntPtr lpszProxy;
|
||
public IntPtr lpszProxyBypass;
|
||
}
|
||
}
|
||
|
||
static async Task Pump(Stream from, Stream to)
|
||
{
|
||
var buffer = ArrayPool<byte>.Shared.Rent(8192);
|
||
|
||
try
|
||
{
|
||
while (true)
|
||
{
|
||
int read = await from.ReadAsync(buffer);
|
||
if (read <= 0) break;
|
||
|
||
await to.WriteAsync(buffer.AsMemory(0, read));
|
||
}
|
||
}
|
||
catch { }
|
||
finally
|
||
{
|
||
ArrayPool<byte>.Shared.Return(buffer);
|
||
}
|
||
} |