如何在 C # 中创建一个简单的代理?

我已经下载了 Privoxy 几个星期前,为了好玩,我想知道如何一个简单的版本可以做到这一点。

我知道我需要配置浏览器(客户端)来向代理发送请求。代理将请求发送到 Web (假设它是一个 http 代理)。代理将收到答案... 但是代理如何将请求发送回浏览器(客户端) ?

我已经在网上搜索了 C # 和 http 代理,但还没有找到一些东西,让我了解它是如何工作背后的场景正确。(我相信我不想要反向代理,但我不确定)。

你们有什么解释或者信息可以让我继续这个小项目吗?

更新

这就是我所理解的(见下图)。

步骤1 我将客户端(浏览器)配置为在代理监听端口将所有请求发送到127.0.0.1。这样,请求将不会直接发送到 Internet,而是由代理处理。

Step2 代理查看一个新连接,读取 HTTP 头并查看必须执行的请求。他执行请求。

Step3 代理从请求中收到一个答案。现在他必须把答案从网络发送到客户端,但是怎么做呢? ? ?

alt text

有用的链接

Mentalis Proxy : 我已经发现这个项目是一个代理(但更多的是我想要的)。我可能会检查的来源,但我真的希望一些基本的了解更多的概念。

ASP 代理 : 我也许可以在这里得到一些信息。

请求反射器 : 这是一个简单的示例。

这是 使用简单 Http 代理的 Git Hub 存储库

184930 次浏览

您可以构建一个 HttpListener类来侦听传入请求,而 HttpWebRequest类来中继请求。

浏览器连接到代理,因此代理从 Web 服务器获得的数据通过与浏览器启动到代理相同的连接发送。

代理可以按以下方式工作。

Step1,配置客户端使用 proxyHost: proxyPort。

Proxy 是侦听 proxyHost: proxyPort 的 TCP 服务器。 浏览器打开与代理的连接并发送 Http 请求。 代理解析这个请求并尝试检测“主机”头。这个头将告诉代理在哪里打开连接。

步骤2: 代理打开到“ Host”头中指定的地址的连接。然后它向该远程服务器发送 HTTP 请求。读取回应。

步骤3: 从远程 HTTP 服务器读取响应后,代理通过早期打开的与浏览器的 TCP 连接发送响应。

示意图看起来是这样的:

Browser                            Proxy                     HTTP server
Open TCP connection
Send HTTP request  ----------->
Read HTTP header
detect Host header
Send request to HTTP ----------->
Server
<-----------
Read response and send
<-----------  it back to the browser
Render content

我不会使用 HttpListener 或类似的东西,这样你会遇到很多问题。

最重要的是,这将是一个巨大的痛苦来支持:

  • 代理保住性命
  • SSL 不起作用(以正确的方式,您将看到弹出窗口)
  • .NET 库严格遵循 RFC,这会导致一些请求失败(即使 IE、 FF 和世界上任何其他浏览器都可以工作)

你需要做的是:

  • 监听 TCP 端口
  • 解析浏览器请求
  • 在 TCP 级别提取到该主机的主机连接
  • 来回转发所有内容,除非你想添加自定义标题等。

中编写了两个不同的 HTTP 代理。NET 有不同的要求,我可以告诉你,这是最好的方式做到这一点。

Mentalis 正在这样做,但是他们的代码是“委托意大利面条”,比 GoTo 还糟糕:)

同意邪恶博士 如果您使用 HTTPListener,您将会遇到许多问题,您必须解析请求,并且将与头和..。

  1. 使用 tcp 侦听器监听浏览器请求
  2. 仅解析请求的第一行,并获取主机域和端口以进行连接
  3. 在浏览器请求的第一行发送确切的原始请求到找到的主机
  4. 从目标站点接收数据(本节中有问题)
  5. 将从主机接收到的确切数据发送到浏览器

你看,你甚至不需要知道什么是在浏览器请求和解析它,只有从第一行的目标网站地址 第一排通常喜欢这样 获取 http://google.com HTTP1.1 或者 CONNECT facebook.com: 443(这是用于 ssl 请求的)

Socks4是一个实现起来非常简单的协议。侦听初始连接,连接到客户机请求的主机/端口,向客户机发送成功代码,然后跨套接字转发传出和传入流。

如果使用 HTTP,则必须读取并可能设置/删除一些 HTTP 头,因此需要多做一些工作。

如果我没记错的话,SSL 将跨 HTTP 和 Socks 代理工作。对于 HTTP 代理,您实现 CONNECT 谓词,它的工作方式与上面描述的 socks4非常相似,然后客户机通过经过代理的 tcp 流打开 SSL 连接。

如果你只是想拦截流量,你可以使用 Fiddler 核心来创建一个代理..。

Http://fiddler.wikidot.com/fiddlercore

首先用 UI 运行 fiddler 来查看它的功能,它是一个代理,允许您调试 http/https 流量。它是用 c # 编写的,并且有一个核心,您可以将其构建到自己的应用程序中。

请记住 FiddlerCore 对于商业应用程序是不免费的。

我最近在 c # . net 中使用 TcpListenerTcpClient编写了一个轻量级代理。

Https://github.com/titanium007/titanium-web-proxy

它以正确的方式支持安全的 HTTP,客户端机器需要信任代理使用的根证书。也支持 WebSockets 中继。除了流水线以外,HTTP 1.1的所有特性都得到了支持。反正大多数现代浏览器都不使用流水线。还支持 Windows 身份验证(纯文本、摘要)。

您可以通过引用项目来连接应用程序,然后查看和修改所有流量(请求和响应)。

至于性能,我已经在我的机器上测试过了,并且没有任何明显的延迟。

使用 OWIN 和 WebAPI,事情变得非常简单。在我搜索 C # 代理服务器时,我也遇到了这篇文章 http://blog.kloud.com.au/2013/11/24/do-it-yourself-web-api-proxy/。我要走这条路。

值得一提的是,这里有一个基于 HttpListenerHttpClient的 C # 示例异步实现(我用它来连接 Android 设备中的 Chrome 和 IIS Express,这是我找到的唯一方法... ...)。

如果您需要 HTTPS 支持,它不应该需要更多的代码,只需要证书配置: 支持 HTTPS 的 Httplisten

// define http://localhost:5000 and http://127.0.0.1:5000/ to be proxies for http://localhost:53068
using (var server = new ProxyServer("http://localhost:53068", "http://localhost:5000/", "http://127.0.0.1:5000/"))
{
server.Start();
Console.WriteLine("Press ESC to stop server.");
while (true)
{
var key = Console.ReadKey(true);
if (key.Key == ConsoleKey.Escape)
break;
}
server.Stop();
}


....


public class ProxyServer : IDisposable
{
private readonly HttpListener _listener;
private readonly int _targetPort;
private readonly string _targetHost;
private static readonly HttpClient _client = new HttpClient();


public ProxyServer(string targetUrl, params string[] prefixes)
: this(new Uri(targetUrl), prefixes)
{
}


public ProxyServer(Uri targetUrl, params string[] prefixes)
{
if (targetUrl == null)
throw new ArgumentNullException(nameof(targetUrl));


if (prefixes == null)
throw new ArgumentNullException(nameof(prefixes));


if (prefixes.Length == 0)
throw new ArgumentException(null, nameof(prefixes));


RewriteTargetInText = true;
RewriteHost = true;
RewriteReferer = true;
TargetUrl = targetUrl;
_targetHost = targetUrl.Host;
_targetPort = targetUrl.Port;
Prefixes = prefixes;


_listener = new HttpListener();
foreach (var prefix in prefixes)
{
_listener.Prefixes.Add(prefix);
}
}


public Uri TargetUrl { get; }
public string[] Prefixes { get; }
public bool RewriteTargetInText { get; set; }
public bool RewriteHost { get; set; }
public bool RewriteReferer { get; set; } // this can have performance impact...


public void Start()
{
_listener.Start();
_listener.BeginGetContext(ProcessRequest, null);
}


private async void ProcessRequest(IAsyncResult result)
{
if (!_listener.IsListening)
return;


var ctx = _listener.EndGetContext(result);
_listener.BeginGetContext(ProcessRequest, null);
await ProcessRequest(ctx).ConfigureAwait(false);
}


protected virtual async Task ProcessRequest(HttpListenerContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));


var url = TargetUrl.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
using (var msg = new HttpRequestMessage(new HttpMethod(context.Request.HttpMethod), url + context.Request.RawUrl))
{
msg.Version = context.Request.ProtocolVersion;


if (context.Request.HasEntityBody)
{
msg.Content = new StreamContent(context.Request.InputStream); // disposed with msg
}


string host = null;
foreach (string headerName in context.Request.Headers)
{
var headerValue = context.Request.Headers[headerName];
if (headerName == "Content-Length" && headerValue == "0") // useless plus don't send if we have no entity body
continue;


bool contentHeader = false;
switch (headerName)
{
// some headers go to content...
case "Allow":
case "Content-Disposition":
case "Content-Encoding":
case "Content-Language":
case "Content-Length":
case "Content-Location":
case "Content-MD5":
case "Content-Range":
case "Content-Type":
case "Expires":
case "Last-Modified":
contentHeader = true;
break;


case "Referer":
if (RewriteReferer && Uri.TryCreate(headerValue, UriKind.Absolute, out var referer)) // if relative, don't handle
{
var builder = new UriBuilder(referer);
builder.Host = TargetUrl.Host;
builder.Port = TargetUrl.Port;
headerValue = builder.ToString();
}
break;


case "Host":
host = headerValue;
if (RewriteHost)
{
headerValue = TargetUrl.Host + ":" + TargetUrl.Port;
}
break;
}


if (contentHeader)
{
msg.Content.Headers.Add(headerName, headerValue);
}
else
{
msg.Headers.Add(headerName, headerValue);
}
}


using (var response = await _client.SendAsync(msg).ConfigureAwait(false))
{
using (var os = context.Response.OutputStream)
{
context.Response.ProtocolVersion = response.Version;
context.Response.StatusCode = (int)response.StatusCode;
context.Response.StatusDescription = response.ReasonPhrase;


foreach (var header in response.Headers)
{
context.Response.Headers.Add(header.Key, string.Join(", ", header.Value));
}


foreach (var header in response.Content.Headers)
{
if (header.Key == "Content-Length") // this will be set automatically at dispose time
continue;


context.Response.Headers.Add(header.Key, string.Join(", ", header.Value));
}


var ct = context.Response.ContentType;
if (RewriteTargetInText && host != null && ct != null &&
(ct.IndexOf("text/html", StringComparison.OrdinalIgnoreCase) >= 0 ||
ct.IndexOf("application/json", StringComparison.OrdinalIgnoreCase) >= 0))
{
using (var ms = new MemoryStream())
{
using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
await stream.CopyToAsync(ms).ConfigureAwait(false);
var enc = context.Response.ContentEncoding ?? Encoding.UTF8;
var html = enc.GetString(ms.ToArray());
if (TryReplace(html, "//" + _targetHost + ":" + _targetPort + "/", "//" + host + "/", out var replaced))
{
var bytes = enc.GetBytes(replaced);
using (var ms2 = new MemoryStream(bytes))
{
ms2.Position = 0;
await ms2.CopyToAsync(context.Response.OutputStream).ConfigureAwait(false);
}
}
else
{
ms.Position = 0;
await ms.CopyToAsync(context.Response.OutputStream).ConfigureAwait(false);
}
}
}
}
else
{
using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
await stream.CopyToAsync(context.Response.OutputStream).ConfigureAwait(false);
}
}
}
}
}
}


public void Stop() => _listener.Stop();
public override string ToString() => string.Join(", ", Prefixes) + " => " + TargetUrl;
public void Dispose() => ((IDisposable)_listener)?.Dispose();


// out-of-the-box replace doesn't tell if something *was* replaced or not
private static bool TryReplace(string input, string oldValue, string newValue, out string result)
{
if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(oldValue))
{
result = input;
return false;
}


var oldLen = oldValue.Length;
var sb = new StringBuilder(input.Length);
bool changed = false;
var offset = 0;
for (int i = 0; i < input.Length; i++)
{
var c = input[i];


if (offset > 0)
{
if (c == oldValue[offset])
{
offset++;
if (oldLen == offset)
{
changed = true;
sb.Append(newValue);
offset = 0;
}
continue;
}


for (int j = 0; j < offset; j++)
{
sb.Append(input[i - offset + j]);
}


sb.Append(c);
offset = 0;
}
else
{
if (c == oldValue[0])
{
if (oldLen == 1)
{
changed = true;
sb.Append(newValue);
}
else
{
offset = 1;
}
continue;
}


sb.Append(c);
}
}


if (changed)
{
result = sb.ToString();
return true;
}


result = input;
return false;
}
}