1081 lines
36 KiB
C#
1081 lines
36 KiB
C#
using System;
|
|
using System.Buffers;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Runtime.InteropServices;
|
|
using System.Runtime.InteropServices.ComTypes;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
class Program
|
|
{
|
|
static string USER = "user";
|
|
static string PASS = "pass";
|
|
static readonly string UPSTREAM_USER = ResolveUpstreamUser();
|
|
static readonly string UPSTREAM_PASS = Environment.GetEnvironmentVariable("UPSTREAM_PASS") ?? string.Empty;
|
|
static readonly string UPSTREAM_PROXY_AUTH_HEADER = BuildUpstreamProxyAuthHeader();
|
|
static long _connectionSeq;
|
|
|
|
static async Task Main()
|
|
{
|
|
int port = 8888;
|
|
RegisterGlobalExceptionHandlers();
|
|
|
|
Log.Info("=== LOCAL PROXY ===");
|
|
Log.Info($"http://127.0.0.1:{port}");
|
|
Log.Info($"login: {USER}");
|
|
Log.Info($"pass : {PASS}");
|
|
Log.Info($"upstream auth user: {UPSTREAM_USER}");
|
|
Log.Info("upstream auth pass: <empty>");
|
|
|
|
var listener = new TcpListener(IPAddress.Any, port);
|
|
listener.Start();
|
|
Log.Info($"Listening on {listener.LocalEndpoint}");
|
|
|
|
while (true)
|
|
{
|
|
try
|
|
{
|
|
var client = await listener.AcceptTcpClientAsync();
|
|
var connectionId = Interlocked.Increment(ref _connectionSeq);
|
|
Log.Info($"[conn:{connectionId}] accepted from {client.Client.RemoteEndPoint}");
|
|
_ = Task.Run(() => HandleClient(client, connectionId));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error("AcceptTcpClientAsync failed", ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void RegisterGlobalExceptionHandlers()
|
|
{
|
|
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
|
|
Log.Error("Unhandled exception", args.ExceptionObject as Exception);
|
|
|
|
TaskScheduler.UnobservedTaskException += (_, args) =>
|
|
{
|
|
Log.Error("Unobserved task exception", args.Exception);
|
|
args.SetObserved();
|
|
};
|
|
}
|
|
|
|
static async Task HandleClient(TcpClient client, long connectionId)
|
|
{
|
|
using (client)
|
|
{
|
|
try
|
|
{
|
|
var stream = client.GetStream();
|
|
var request = await ReadHeaders(stream, $"conn:{connectionId} client-request");
|
|
|
|
if (request == null)
|
|
{
|
|
Log.Warn($"[conn:{connectionId}] empty or invalid request");
|
|
return;
|
|
}
|
|
|
|
Log.Info($"[conn:{connectionId}] {request.Method} {request.Target}");
|
|
|
|
if (!CheckAuth(request.Raw))
|
|
{
|
|
Log.Warn($"[conn:{connectionId}] proxy auth failed");
|
|
await Write407(stream);
|
|
return;
|
|
}
|
|
|
|
if (request.Method == "CONNECT")
|
|
await HandleConnect(stream, request.Target, connectionId);
|
|
else
|
|
await HandleHttp(stream, request, connectionId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error($"[conn:{connectionId}] client handling failed", ex);
|
|
}
|
|
finally
|
|
{
|
|
Log.Info($"[conn:{connectionId}] closed");
|
|
}
|
|
}
|
|
}
|
|
|
|
// ================= HTTP =================
|
|
|
|
static async Task HandleHttp(NetworkStream clientStream, HttpRequest req, long connectionId)
|
|
{
|
|
var proxyUri = WinHttpHelper.GetProxyForUrl(new Uri(req.Url));
|
|
if (proxyUri == null)
|
|
{
|
|
Log.Error($"[conn:{connectionId}] no upstream proxy for {req.Url}");
|
|
await Write502(clientStream);
|
|
return;
|
|
}
|
|
|
|
Log.Info($"[conn:{connectionId}] HTTP upstream {proxyUri.Host}:{proxyUri.Port} for {req.Url}");
|
|
LogUpstreamUser(connectionId, proxyUri.Host);
|
|
|
|
using var upstream = new TcpClient();
|
|
await upstream.ConnectAsync(proxyUri.Host, proxyUri.Port);
|
|
|
|
var upstreamStream = upstream.GetStream();
|
|
|
|
var outbound = BuildUpstreamHttpRequest(req);
|
|
await upstreamStream.WriteAsync(outbound);
|
|
|
|
await Pump(upstreamStream, clientStream, $"conn:{connectionId} HTTP upstream->client");
|
|
}
|
|
|
|
// ================= CONNECT =================
|
|
|
|
static async Task HandleConnect(NetworkStream clientStream, string target, long connectionId)
|
|
{
|
|
var parts = target.Split(':');
|
|
if (parts.Length != 2 || !int.TryParse(parts[1], out int port))
|
|
{
|
|
Log.Error($"[conn:{connectionId}] invalid CONNECT target: {target}");
|
|
await clientStream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 400 Bad Request\r\n\r\n"));
|
|
return;
|
|
}
|
|
|
|
string host = parts[0];
|
|
|
|
var proxyUri = WinHttpHelper.GetProxyForUrl(new Uri($"https://{host}:{port}"));
|
|
if (proxyUri == null)
|
|
{
|
|
Log.Error($"[conn:{connectionId}] no upstream proxy for CONNECT {host}:{port}");
|
|
await Write502(clientStream);
|
|
return;
|
|
}
|
|
|
|
Log.Info($"[conn:{connectionId}] CONNECT {host}:{port} via {proxyUri.Host}:{proxyUri.Port}");
|
|
LogUpstreamUser(connectionId, proxyUri.Host);
|
|
|
|
using var upstream = new TcpClient();
|
|
await upstream.ConnectAsync(proxyUri.Host, proxyUri.Port);
|
|
|
|
var upstreamStream = upstream.GetStream();
|
|
var connectResp = await PerformUpstreamConnectHandshake(
|
|
upstreamStream,
|
|
host,
|
|
port,
|
|
proxyUri.Host,
|
|
connectionId);
|
|
|
|
int statusCode = ParseStatusCode(connectResp?.Raw);
|
|
if (connectResp == null || statusCode != 200)
|
|
{
|
|
var status = connectResp?.Raw?.Split("\r\n", StringSplitOptions.None).FirstOrDefault() ?? "<no response>";
|
|
Log.Error($"[conn:{connectionId}] upstream CONNECT rejected ({statusCode}): {status}");
|
|
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, $"conn:{connectionId} tunnel upstream->client");
|
|
var t2 = Pump(clientStream, upstreamStream, $"conn:{connectionId} tunnel client->upstream");
|
|
|
|
await Task.WhenAny(t1, t2);
|
|
}
|
|
|
|
// ================= AUTH =================
|
|
|
|
static bool CheckAuth(string raw)
|
|
{
|
|
string expected = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{USER}:{PASS}"));
|
|
return raw.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));
|
|
}
|
|
|
|
static async Task Write502(NetworkStream stream)
|
|
{
|
|
const string resp = "HTTP/1.1 502 Bad Gateway\r\n\r\n";
|
|
await stream.WriteAsync(Encoding.ASCII.GetBytes(resp));
|
|
}
|
|
|
|
// ================= PARSER =================
|
|
|
|
class HttpRequest
|
|
{
|
|
public string Method;
|
|
public string Target;
|
|
public string Url;
|
|
public string Raw;
|
|
public byte[] RawBytes;
|
|
public int HeaderLength;
|
|
}
|
|
|
|
static async Task<HttpRequest> ReadHeaders(Stream stream, string context)
|
|
{
|
|
try
|
|
{
|
|
var rawBytes = await ReadHeaderBytes(stream);
|
|
if (rawBytes == null || rawBytes.Length == 0)
|
|
return null;
|
|
string raw = Encoding.ASCII.GetString(rawBytes);
|
|
|
|
var lines = raw.Split("\r\n");
|
|
var first = lines[0].Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
if (first.Length < 2)
|
|
{
|
|
Log.Error($"[{context}] invalid request line: {lines[0]}");
|
|
return null;
|
|
}
|
|
|
|
var req = new HttpRequest
|
|
{
|
|
Method = first[0],
|
|
Target = first[1],
|
|
Raw = raw,
|
|
RawBytes = rawBytes,
|
|
HeaderLength = GetHeaderLength(raw)
|
|
};
|
|
|
|
if (req.Method != "CONNECT")
|
|
{
|
|
string host = lines
|
|
.FirstOrDefault(l => l.StartsWith("Host:", StringComparison.OrdinalIgnoreCase))
|
|
?.Split(':', 2)[1]
|
|
.Trim();
|
|
req.Url = req.Target.StartsWith("http", StringComparison.OrdinalIgnoreCase)
|
|
? req.Target
|
|
: $"http://{host}{req.Target}";
|
|
}
|
|
|
|
return req;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error($"[{context}] failed to read/parse headers", ex);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static async Task<byte[]> ReadHeaderBytes(Stream stream)
|
|
{
|
|
const int maxHeaderBytes = 64 * 1024;
|
|
var data = new List<byte>(1024);
|
|
var one = new byte[1];
|
|
|
|
while (data.Count < maxHeaderBytes)
|
|
{
|
|
int read = await stream.ReadAsync(one, 0, 1);
|
|
if (read <= 0)
|
|
break;
|
|
|
|
data.Add(one[0]);
|
|
int n = data.Count;
|
|
if (n >= 4
|
|
&& data[n - 4] == (byte)'\r'
|
|
&& data[n - 3] == (byte)'\n'
|
|
&& data[n - 2] == (byte)'\r'
|
|
&& data[n - 1] == (byte)'\n')
|
|
break;
|
|
}
|
|
|
|
return data.Count == 0 ? null : data.ToArray();
|
|
}
|
|
|
|
// ================= STREAM =================
|
|
|
|
static async Task Pump(Stream from, Stream to, string context)
|
|
{
|
|
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 (Exception ex)
|
|
{
|
|
if (IsExpectedDisconnect(ex))
|
|
Log.Info($"[{context}] stream closed: {OneLine(ex.Message)}");
|
|
else
|
|
Log.Error($"[{context}] stream pump failed", ex);
|
|
}
|
|
finally
|
|
{
|
|
ArrayPool<byte>.Shared.Return(buffer);
|
|
}
|
|
}
|
|
|
|
static bool IsExpectedDisconnect(Exception ex)
|
|
{
|
|
if (ex is ObjectDisposedException)
|
|
return true;
|
|
if (ex is OperationCanceledException)
|
|
return true;
|
|
|
|
if (ex is IOException io && io.InnerException is SocketException se)
|
|
{
|
|
return se.SocketErrorCode == SocketError.ConnectionAborted
|
|
|| se.SocketErrorCode == SocketError.ConnectionReset
|
|
|| se.SocketErrorCode == SocketError.Shutdown
|
|
|| se.SocketErrorCode == SocketError.OperationAborted
|
|
|| se.SocketErrorCode == SocketError.TimedOut;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static string OneLine(string message)
|
|
{
|
|
if (string.IsNullOrEmpty(message))
|
|
return string.Empty;
|
|
return message.Replace("\r", " ").Replace("\n", " ").Trim();
|
|
}
|
|
|
|
static int ParseStatusCode(string rawHeaders)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(rawHeaders))
|
|
return 0;
|
|
|
|
var line = rawHeaders.Split("\r\n", StringSplitOptions.None).FirstOrDefault();
|
|
if (string.IsNullOrWhiteSpace(line))
|
|
return 0;
|
|
|
|
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length < 2)
|
|
return 0;
|
|
|
|
return int.TryParse(parts[1], out int code) ? code : 0;
|
|
}
|
|
|
|
static int GetHeaderLength(string raw)
|
|
{
|
|
int idx = raw.IndexOf("\r\n\r\n", StringComparison.Ordinal);
|
|
return idx >= 0 ? idx + 4 : raw.Length;
|
|
}
|
|
|
|
static byte[] BuildUpstreamHttpRequest(HttpRequest req)
|
|
{
|
|
var lines = req.Raw.Split("\r\n", StringSplitOptions.None);
|
|
var sb = new StringBuilder();
|
|
sb.Append(BuildUpstreamRequestLine(req, lines[0])).Append("\r\n");
|
|
|
|
for (int i = 1; i < lines.Length; i++)
|
|
{
|
|
var line = lines[i];
|
|
if (line.Length == 0)
|
|
break;
|
|
|
|
if (line.StartsWith("Proxy-Authorization:", StringComparison.OrdinalIgnoreCase))
|
|
continue; // local proxy auth, must not go upstream
|
|
if (line.StartsWith("Proxy-Connection:", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
sb.Append(line).Append("\r\n");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(UPSTREAM_PROXY_AUTH_HEADER))
|
|
sb.Append(UPSTREAM_PROXY_AUTH_HEADER);
|
|
|
|
sb.Append("\r\n");
|
|
var headerBytes = Encoding.ASCII.GetBytes(sb.ToString());
|
|
|
|
if (req.HeaderLength >= req.RawBytes.Length)
|
|
return headerBytes;
|
|
|
|
int bodyLen = req.RawBytes.Length - req.HeaderLength;
|
|
var result = new byte[headerBytes.Length + bodyLen];
|
|
Buffer.BlockCopy(headerBytes, 0, result, 0, headerBytes.Length);
|
|
Buffer.BlockCopy(req.RawBytes, req.HeaderLength, result, headerBytes.Length, bodyLen);
|
|
return result;
|
|
}
|
|
|
|
static string BuildUpstreamRequestLine(HttpRequest req, string originalLine)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(originalLine))
|
|
return originalLine ?? string.Empty;
|
|
|
|
var parts = originalLine.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length < 3)
|
|
return originalLine;
|
|
|
|
string method = parts[0];
|
|
string version = parts[2];
|
|
string url = req.Url;
|
|
|
|
if (string.IsNullOrWhiteSpace(url))
|
|
return originalLine;
|
|
|
|
// Upstream forward proxy expects absolute-form: METHOD http://host/path HTTP/1.1
|
|
return $"{method} {url} {version}";
|
|
}
|
|
|
|
static string BuildUpstreamProxyAuthHeader()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(UPSTREAM_USER))
|
|
{
|
|
Log.Warn("UPSTREAM_USER is empty; upstream Proxy-Authorization is disabled");
|
|
return string.Empty;
|
|
}
|
|
|
|
string pass = UPSTREAM_PASS ?? string.Empty;
|
|
string encoded = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{UPSTREAM_USER}:{pass}"));
|
|
return $"Proxy-Authorization: Basic {encoded}\r\n";
|
|
}
|
|
|
|
static string ResolveUpstreamUser()
|
|
{
|
|
var fromEnv = Environment.GetEnvironmentVariable("UPSTREAM_USER");
|
|
if (!string.IsNullOrWhiteSpace(fromEnv))
|
|
return fromEnv;
|
|
|
|
var domain = Environment.UserDomainName;
|
|
var user = Environment.UserName;
|
|
if (!string.IsNullOrWhiteSpace(domain) && !string.IsNullOrWhiteSpace(user))
|
|
return $"{domain}\\{user}";
|
|
return user ?? string.Empty;
|
|
}
|
|
|
|
static void LogUpstreamUser(long connectionId, string proxyHost)
|
|
{
|
|
if (string.Equals(proxyHost, "proxy.syktsu.ru", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Log.Info($"[conn:{connectionId}] upstream auth login for {proxyHost}: '{UPSTREAM_USER}' (password: empty)");
|
|
}
|
|
}
|
|
|
|
static async Task<HttpRequest> PerformUpstreamConnectHandshake(
|
|
NetworkStream upstreamStream,
|
|
string targetHost,
|
|
int targetPort,
|
|
string proxyHost,
|
|
long connectionId)
|
|
{
|
|
// 1) First CONNECT without auth (browser-like flow).
|
|
await SendConnectRequest(upstreamStream, targetHost, targetPort, null);
|
|
var resp = await ReadHeaders(upstreamStream, $"conn:{connectionId} upstream-connect-response-1");
|
|
int code = ParseStatusCode(resp?.Raw);
|
|
if (code == 200 || resp == null)
|
|
return resp;
|
|
|
|
var challenges = ParseProxyAuthenticateValues(resp.Raw);
|
|
if (code != 407 || challenges.Count == 0)
|
|
return resp;
|
|
|
|
// 2) Basic fallback.
|
|
if (!string.IsNullOrEmpty(UPSTREAM_PROXY_AUTH_HEADER))
|
|
{
|
|
await SendConnectRequest(upstreamStream, targetHost, targetPort, UPSTREAM_PROXY_AUTH_HEADER.TrimEnd('\r', '\n'));
|
|
var basicResp = await ReadHeaders(upstreamStream, $"conn:{connectionId} upstream-connect-response-basic");
|
|
if (ParseStatusCode(basicResp?.Raw) == 200)
|
|
return basicResp;
|
|
resp = basicResp ?? resp;
|
|
challenges = ParseProxyAuthenticateValues(resp.Raw);
|
|
}
|
|
|
|
// 3) Negotiate/NTLM over SSPI (as browsers do for AD proxies).
|
|
bool hasNegotiate = challenges.Any(c => c.StartsWith("Negotiate", StringComparison.OrdinalIgnoreCase));
|
|
bool hasNtlm = challenges.Any(c => c.StartsWith("NTLM", StringComparison.OrdinalIgnoreCase));
|
|
if (!hasNegotiate && !hasNtlm)
|
|
return resp;
|
|
|
|
string scheme = hasNegotiate ? "Negotiate" : "NTLM";
|
|
using var sspi = new SspiProxyAuthenticator(hasNegotiate ? "Negotiate" : "NTLM", proxyHost);
|
|
string inToken = ExtractSchemeToken(challenges, scheme);
|
|
|
|
for (int step = 0; step < 4; step++)
|
|
{
|
|
string outToken = sspi.NextToken(inToken);
|
|
if (string.IsNullOrEmpty(outToken))
|
|
break;
|
|
|
|
await SendConnectRequest(
|
|
upstreamStream,
|
|
targetHost,
|
|
targetPort,
|
|
$"Proxy-Authorization: {scheme} {outToken}");
|
|
|
|
var authResp = await ReadHeaders(upstreamStream, $"conn:{connectionId} upstream-connect-response-{scheme}-{step + 1}");
|
|
int authCode = ParseStatusCode(authResp?.Raw);
|
|
if (authCode == 200)
|
|
return authResp;
|
|
if (authCode != 407 || authResp == null)
|
|
return authResp ?? resp;
|
|
|
|
challenges = ParseProxyAuthenticateValues(authResp.Raw);
|
|
inToken = ExtractSchemeToken(challenges, scheme);
|
|
resp = authResp;
|
|
}
|
|
|
|
return resp;
|
|
}
|
|
|
|
static async Task SendConnectRequest(NetworkStream stream, string host, int port, string proxyAuthHeader)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.Append($"CONNECT {host}:{port} HTTP/1.1\r\n");
|
|
sb.Append($"Host: {host}:{port}\r\n");
|
|
sb.Append("Proxy-Connection: Keep-Alive\r\n");
|
|
if (!string.IsNullOrWhiteSpace(proxyAuthHeader))
|
|
sb.Append(proxyAuthHeader).Append("\r\n");
|
|
sb.Append("\r\n");
|
|
await stream.WriteAsync(Encoding.ASCII.GetBytes(sb.ToString()));
|
|
}
|
|
|
|
static List<string> ParseProxyAuthenticateValues(string rawHeaders)
|
|
{
|
|
var result = new List<string>();
|
|
if (string.IsNullOrWhiteSpace(rawHeaders))
|
|
return result;
|
|
|
|
foreach (var line in rawHeaders.Split("\r\n", StringSplitOptions.None))
|
|
{
|
|
if (line.StartsWith("Proxy-Authenticate:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var value = line.Substring("Proxy-Authenticate:".Length).Trim();
|
|
if (value.Length > 0)
|
|
result.Add(value);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static string ExtractSchemeToken(List<string> values, string scheme)
|
|
{
|
|
foreach (var v in values)
|
|
{
|
|
if (!v.StartsWith(scheme, StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
var parts = v.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length == 2)
|
|
return parts[1].Trim();
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
sealed class SspiProxyAuthenticator : IDisposable
|
|
{
|
|
const int SECPKG_CRED_OUTBOUND = 2;
|
|
const int SECURITY_NATIVE_DREP = 0x00000010;
|
|
const int ISC_REQ_CONNECTION = 0x00000800;
|
|
const int SECBUFFER_VERSION = 0;
|
|
const int SECBUFFER_TOKEN = 2;
|
|
const uint SEC_I_CONTINUE_NEEDED = 0x00090312;
|
|
const uint SEC_E_OK = 0x00000000;
|
|
|
|
SecHandle _cred;
|
|
SecHandle _ctx;
|
|
bool _credAcquired;
|
|
bool _ctxInitialized;
|
|
readonly string _package;
|
|
readonly string _targetSpn;
|
|
|
|
public SspiProxyAuthenticator(string package, string proxyHost)
|
|
{
|
|
_package = package;
|
|
_targetSpn = $"HTTP/{proxyHost}";
|
|
AcquireCredentials();
|
|
}
|
|
|
|
public string NextToken(string incomingBase64Token)
|
|
{
|
|
byte[] inBytes = null;
|
|
if (!string.IsNullOrWhiteSpace(incomingBase64Token))
|
|
{
|
|
try { inBytes = Convert.FromBase64String(incomingBase64Token); }
|
|
catch { inBytes = null; }
|
|
}
|
|
|
|
var outBuffer = new SecBuffer
|
|
{
|
|
cbBuffer = 0,
|
|
BufferType = SECBUFFER_TOKEN,
|
|
pvBuffer = IntPtr.Zero
|
|
};
|
|
var outDesc = new SecBufferDesc();
|
|
IntPtr outBufPtr = Marshal.AllocHGlobal(Marshal.SizeOf<SecBuffer>());
|
|
try
|
|
{
|
|
Marshal.StructureToPtr(outBuffer, outBufPtr, false);
|
|
outDesc.ulVersion = SECBUFFER_VERSION;
|
|
outDesc.cBuffers = 1;
|
|
outDesc.pBuffers = outBufPtr;
|
|
|
|
uint attrs;
|
|
FILETIME expiry;
|
|
uint status;
|
|
|
|
IntPtr inDescPtr = IntPtr.Zero;
|
|
IntPtr inBufPtr = IntPtr.Zero;
|
|
IntPtr inTokenPtr = IntPtr.Zero;
|
|
try
|
|
{
|
|
if (inBytes != null && inBytes.Length > 0)
|
|
{
|
|
inTokenPtr = Marshal.AllocHGlobal(inBytes.Length);
|
|
Marshal.Copy(inBytes, 0, inTokenPtr, inBytes.Length);
|
|
var inBuf = new SecBuffer
|
|
{
|
|
cbBuffer = inBytes.Length,
|
|
BufferType = SECBUFFER_TOKEN,
|
|
pvBuffer = inTokenPtr
|
|
};
|
|
inBufPtr = Marshal.AllocHGlobal(Marshal.SizeOf<SecBuffer>());
|
|
Marshal.StructureToPtr(inBuf, inBufPtr, false);
|
|
|
|
var inDesc = new SecBufferDesc
|
|
{
|
|
ulVersion = SECBUFFER_VERSION,
|
|
cBuffers = 1,
|
|
pBuffers = inBufPtr
|
|
};
|
|
inDescPtr = Marshal.AllocHGlobal(Marshal.SizeOf<SecBufferDesc>());
|
|
Marshal.StructureToPtr(inDesc, inDescPtr, false);
|
|
}
|
|
|
|
if (_ctxInitialized)
|
|
{
|
|
status = InitializeSecurityContext(
|
|
ref _cred,
|
|
ref _ctx,
|
|
_targetSpn,
|
|
ISC_REQ_CONNECTION,
|
|
0,
|
|
SECURITY_NATIVE_DREP,
|
|
inDescPtr,
|
|
0,
|
|
out _ctx,
|
|
out outDesc,
|
|
out attrs,
|
|
out expiry);
|
|
}
|
|
else
|
|
{
|
|
status = InitializeSecurityContext(
|
|
ref _cred,
|
|
IntPtr.Zero,
|
|
_targetSpn,
|
|
ISC_REQ_CONNECTION,
|
|
0,
|
|
SECURITY_NATIVE_DREP,
|
|
inDescPtr,
|
|
0,
|
|
out _ctx,
|
|
out outDesc,
|
|
out attrs,
|
|
out expiry);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (inTokenPtr != IntPtr.Zero) Marshal.FreeHGlobal(inTokenPtr);
|
|
if (inBufPtr != IntPtr.Zero) Marshal.FreeHGlobal(inBufPtr);
|
|
if (inDescPtr != IntPtr.Zero) Marshal.FreeHGlobal(inDescPtr);
|
|
}
|
|
|
|
_ctxInitialized = true;
|
|
if (status != SEC_E_OK && status != SEC_I_CONTINUE_NEEDED)
|
|
return null;
|
|
|
|
var finalOut = Marshal.PtrToStructure<SecBuffer>(outBufPtr);
|
|
if (finalOut.cbBuffer <= 0 || finalOut.pvBuffer == IntPtr.Zero)
|
|
return null;
|
|
|
|
var token = new byte[finalOut.cbBuffer];
|
|
Marshal.Copy(finalOut.pvBuffer, token, 0, token.Length);
|
|
FreeContextBuffer(finalOut.pvBuffer);
|
|
return Convert.ToBase64String(token);
|
|
}
|
|
finally
|
|
{
|
|
Marshal.FreeHGlobal(outBufPtr);
|
|
}
|
|
}
|
|
|
|
void AcquireCredentials()
|
|
{
|
|
FILETIME expiry;
|
|
uint status = AcquireCredentialsHandle(
|
|
null,
|
|
_package,
|
|
SECPKG_CRED_OUTBOUND,
|
|
IntPtr.Zero,
|
|
IntPtr.Zero,
|
|
IntPtr.Zero,
|
|
IntPtr.Zero,
|
|
out _cred,
|
|
out expiry);
|
|
_credAcquired = status == SEC_E_OK;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_ctxInitialized)
|
|
DeleteSecurityContext(ref _ctx);
|
|
if (_credAcquired)
|
|
FreeCredentialsHandle(ref _cred);
|
|
}
|
|
|
|
[DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
|
static extern uint AcquireCredentialsHandle(
|
|
string pszPrincipal,
|
|
string pszPackage,
|
|
int fCredentialUse,
|
|
IntPtr pvLogonId,
|
|
IntPtr pAuthData,
|
|
IntPtr pGetKeyFn,
|
|
IntPtr pvGetKeyArgument,
|
|
out SecHandle phCredential,
|
|
out FILETIME ptsExpiry);
|
|
|
|
[DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
|
static extern uint InitializeSecurityContext(
|
|
ref SecHandle phCredential,
|
|
IntPtr phContext,
|
|
string pszTargetName,
|
|
int fContextReq,
|
|
int Reserved1,
|
|
int TargetDataRep,
|
|
IntPtr pInput,
|
|
int Reserved2,
|
|
out SecHandle phNewContext,
|
|
out SecBufferDesc pOutput,
|
|
out uint pfContextAttr,
|
|
out FILETIME ptsExpiry);
|
|
|
|
[DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
|
static extern uint InitializeSecurityContext(
|
|
ref SecHandle phCredential,
|
|
ref SecHandle phContext,
|
|
string pszTargetName,
|
|
int fContextReq,
|
|
int Reserved1,
|
|
int TargetDataRep,
|
|
IntPtr pInput,
|
|
int Reserved2,
|
|
out SecHandle phNewContext,
|
|
out SecBufferDesc pOutput,
|
|
out uint pfContextAttr,
|
|
out FILETIME ptsExpiry);
|
|
|
|
[DllImport("secur32.dll", SetLastError = false)]
|
|
static extern uint DeleteSecurityContext(ref SecHandle phContext);
|
|
|
|
[DllImport("secur32.dll", SetLastError = false)]
|
|
static extern uint FreeCredentialsHandle(ref SecHandle phCredential);
|
|
|
|
[DllImport("secur32.dll", SetLastError = false)]
|
|
static extern uint FreeContextBuffer(IntPtr pvContextBuffer);
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
struct SecHandle
|
|
{
|
|
public IntPtr dwLower;
|
|
public IntPtr dwUpper;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
struct SecBuffer
|
|
{
|
|
public int cbBuffer;
|
|
public int BufferType;
|
|
public IntPtr pvBuffer;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
struct SecBufferDesc
|
|
{
|
|
public int ulVersion;
|
|
public int cBuffers;
|
|
public IntPtr pBuffers;
|
|
}
|
|
}
|
|
|
|
// ================= WINHTTP =================
|
|
|
|
static class WinHttpHelper
|
|
{
|
|
const int WINHTTP_ACCESS_TYPE_NO_PROXY = 1;
|
|
const int WINHTTP_AUTOPROXY_AUTO_DETECT = 0x00000001;
|
|
const int WINHTTP_AUTOPROXY_CONFIG_URL = 0x00000002;
|
|
const int WINHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001;
|
|
const int WINHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002;
|
|
|
|
[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);
|
|
|
|
[DllImport("winhttp.dll", SetLastError = true)]
|
|
static extern bool WinHttpGetDefaultProxyConfiguration(out WINHTTP_PROXY_INFO proxyInfo);
|
|
|
|
[DllImport("winhttp.dll", SetLastError = true)]
|
|
static extern bool WinHttpGetIEProxyConfigForCurrentUser(out WINHTTP_CURRENT_USER_IE_PROXY_CONFIG proxyConfig);
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
static extern IntPtr GlobalFree(IntPtr hMem);
|
|
|
|
static readonly Uri ForcedUpstreamProxy = ReadForcedUpstreamProxy();
|
|
|
|
public static Uri GetProxyForUrl(Uri url)
|
|
{
|
|
if (ForcedUpstreamProxy != null)
|
|
{
|
|
Log.Info($"Using forced upstream proxy {ForcedUpstreamProxy.Host}:{ForcedUpstreamProxy.Port} for {url}");
|
|
return ForcedUpstreamProxy;
|
|
}
|
|
|
|
IntPtr session = WinHttpOpen("proxy", WINHTTP_ACCESS_TYPE_NO_PROXY, null, null, 0);
|
|
if (session == IntPtr.Zero)
|
|
{
|
|
Log.Error($"WinHttpOpen failed (Win32={Marshal.GetLastWin32Error()})");
|
|
return null;
|
|
}
|
|
|
|
WINHTTP_PROXY_INFO autoInfo = default;
|
|
WINHTTP_PROXY_INFO defaultInfo = default;
|
|
WINHTTP_CURRENT_USER_IE_PROXY_CONFIG ieConfig = default;
|
|
|
|
try
|
|
{
|
|
if (WinHttpGetIEProxyConfigForCurrentUser(out ieConfig))
|
|
{
|
|
var autoConfigUrl = PtrToString(ieConfig.lpszAutoConfigUrl);
|
|
bool hasSupportedAutoConfigUrl = !string.IsNullOrWhiteSpace(autoConfigUrl)
|
|
&& (autoConfigUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
|
|| autoConfigUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase));
|
|
if (!string.IsNullOrWhiteSpace(autoConfigUrl) && !hasSupportedAutoConfigUrl)
|
|
{
|
|
Log.Warn($"IE PAC URL has unsupported scheme for WinHTTP: {autoConfigUrl}");
|
|
}
|
|
|
|
var options = new WINHTTP_AUTOPROXY_OPTIONS
|
|
{
|
|
dwFlags = 0,
|
|
dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A,
|
|
lpszAutoConfigUrl = hasSupportedAutoConfigUrl ? ieConfig.lpszAutoConfigUrl : IntPtr.Zero,
|
|
fAutoLogonIfChallenged = true
|
|
};
|
|
|
|
if (hasSupportedAutoConfigUrl)
|
|
options.dwFlags |= WINHTTP_AUTOPROXY_CONFIG_URL;
|
|
if (ieConfig.fAutoDetect)
|
|
options.dwFlags |= WINHTTP_AUTOPROXY_AUTO_DETECT;
|
|
|
|
if (options.dwFlags != 0)
|
|
{
|
|
if (WinHttpGetProxyForUrl(session, url.ToString(), ref options, out autoInfo))
|
|
{
|
|
var proxyUri = ParseProxyInfo(autoInfo, url.Scheme);
|
|
if (proxyUri != null)
|
|
{
|
|
return proxyUri;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int err = Marshal.GetLastWin32Error();
|
|
Log.Warn($"WinHttpGetProxyForUrl failed for {url} (Win32={err})");
|
|
}
|
|
}
|
|
|
|
var ieStaticProxyRaw = PtrToString(ieConfig.lpszProxy);
|
|
var ieStaticProxy = ParseProxyString(ieStaticProxyRaw, url.Scheme);
|
|
if (ieStaticProxy != null)
|
|
{
|
|
Log.Info($"Using IE static proxy {ieStaticProxy.Host}:{ieStaticProxy.Port} for {url}");
|
|
return ieStaticProxy;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log.Warn($"WinHttpGetIEProxyConfigForCurrentUser failed (Win32={Marshal.GetLastWin32Error()})");
|
|
}
|
|
|
|
if (WinHttpGetDefaultProxyConfiguration(out defaultInfo))
|
|
{
|
|
var fallbackProxy = ParseProxyInfo(defaultInfo, url.Scheme);
|
|
if (fallbackProxy != null)
|
|
{
|
|
Log.Info($"Using default WinHTTP proxy {fallbackProxy.Host}:{fallbackProxy.Port} for {url}");
|
|
return fallbackProxy;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log.Warn($"WinHttpGetDefaultProxyConfiguration failed (Win32={Marshal.GetLastWin32Error()})");
|
|
}
|
|
|
|
Log.Error($"No system upstream proxy resolved for {url}");
|
|
return null;
|
|
}
|
|
finally
|
|
{
|
|
FreeWinHttpProxyInfo(autoInfo);
|
|
FreeWinHttpProxyInfo(defaultInfo);
|
|
FreeIeConfig(ieConfig);
|
|
WinHttpCloseHandle(session);
|
|
}
|
|
}
|
|
|
|
static string PtrToString(IntPtr ptr)
|
|
{
|
|
if (ptr == IntPtr.Zero) return null;
|
|
return Marshal.PtrToStringUni(ptr);
|
|
}
|
|
|
|
static Uri ParseProxyInfo(WINHTTP_PROXY_INFO info, string scheme)
|
|
{
|
|
var raw = PtrToString(info.lpszProxy);
|
|
return ParseProxyString(raw, scheme);
|
|
}
|
|
|
|
static Uri ParseProxyString(string raw, string scheme)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(raw))
|
|
return null;
|
|
|
|
var parts = raw.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(p => p.Trim())
|
|
.ToArray();
|
|
|
|
string schemeToken = parts.FirstOrDefault(p =>
|
|
p.StartsWith($"{scheme}=", StringComparison.OrdinalIgnoreCase));
|
|
string genericToken = parts.FirstOrDefault(p => p.StartsWith("http=", StringComparison.OrdinalIgnoreCase))
|
|
?? parts.FirstOrDefault(p => p.StartsWith("https=", StringComparison.OrdinalIgnoreCase));
|
|
string first = parts.FirstOrDefault();
|
|
string token = schemeToken ?? genericToken ?? first;
|
|
if (string.IsNullOrWhiteSpace(token))
|
|
return null;
|
|
|
|
var value = token.Contains('=')
|
|
? token.Split('=', 2)[1].Trim()
|
|
: token;
|
|
|
|
if (value.StartsWith("PROXY ", StringComparison.OrdinalIgnoreCase))
|
|
value = value.Substring(6).Trim();
|
|
if (value.Equals("DIRECT", StringComparison.OrdinalIgnoreCase))
|
|
return null;
|
|
|
|
var normalized = value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
|
|| value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
|
|
? value
|
|
: "http://" + value;
|
|
|
|
if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri) || string.IsNullOrWhiteSpace(uri.Host))
|
|
{
|
|
Log.Warn($"Cannot parse proxy value '{raw}'");
|
|
return null;
|
|
}
|
|
|
|
return uri;
|
|
}
|
|
|
|
static Uri ReadForcedUpstreamProxy()
|
|
{
|
|
var value = Environment.GetEnvironmentVariable("UPSTREAM_PROXY");
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
return null;
|
|
|
|
var normalized = value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
|
|| value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
|
|
? value
|
|
: "http://" + value;
|
|
|
|
if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri) || string.IsNullOrWhiteSpace(uri.Host))
|
|
{
|
|
Log.Error($"Invalid UPSTREAM_PROXY value: '{value}'");
|
|
return null;
|
|
}
|
|
|
|
return uri;
|
|
}
|
|
|
|
static void FreeWinHttpProxyInfo(WINHTTP_PROXY_INFO info)
|
|
{
|
|
if (info.lpszProxy != IntPtr.Zero)
|
|
GlobalFree(info.lpszProxy);
|
|
if (info.lpszProxyBypass != IntPtr.Zero)
|
|
GlobalFree(info.lpszProxyBypass);
|
|
}
|
|
|
|
static void FreeIeConfig(WINHTTP_CURRENT_USER_IE_PROXY_CONFIG config)
|
|
{
|
|
if (config.lpszAutoConfigUrl != IntPtr.Zero)
|
|
GlobalFree(config.lpszAutoConfigUrl);
|
|
if (config.lpszProxy != IntPtr.Zero)
|
|
GlobalFree(config.lpszProxy);
|
|
if (config.lpszProxyBypass != IntPtr.Zero)
|
|
GlobalFree(config.lpszProxyBypass);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
struct WINHTTP_CURRENT_USER_IE_PROXY_CONFIG
|
|
{
|
|
[MarshalAs(UnmanagedType.Bool)]
|
|
public bool fAutoDetect;
|
|
public IntPtr lpszAutoConfigUrl;
|
|
public IntPtr lpszProxy;
|
|
public IntPtr lpszProxyBypass;
|
|
}
|
|
}
|
|
|
|
static class Log
|
|
{
|
|
static readonly object _sync = new();
|
|
|
|
public static void Info(string message) => Write("INF", message);
|
|
public static void Warn(string message) => Write("WRN", message);
|
|
public static void Error(string message, Exception ex = null)
|
|
{
|
|
Write("ERR", ex == null ? message : $"{message}{Environment.NewLine}{ex}");
|
|
}
|
|
|
|
static void Write(string level, string message)
|
|
{
|
|
lock (_sync)
|
|
{
|
|
Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{level}] {message}");
|
|
}
|
|
}
|
|
}
|