如何在 C# 中创建一个简单的代理?
Posted
技术标签:
【中文标题】如何在 C# 中创建一个简单的代理?【英文标题】:How to create a simple proxy in C#? 【发布时间】:2010-09-18 14:43:26 【问题描述】:几周前我下载了 Privoxy,为了好玩,我很想知道它的简单版本是如何完成的。
我了解我需要配置浏览器(客户端)以向代理发送请求。代理将请求发送到网络(假设它是一个 http 代理)。代理会收到答案...但是代理如何将请求发回给浏览器(客户端)?
我在网上搜索了 C# 和 http 代理,但没有找到让我了解它在幕后如何正确工作的东西。 (我相信我不想要反向代理,但我不确定)。
你们有没有一些解释或信息可以让我继续这个小项目?
更新
这是我的理解(见下图)。
第 1 步 我将客户端(浏览器)配置为将所有请求发送到代理侦听端口的 127.0.0.1。这样,请求就不会直接发送到互联网,而是由代理处理。
Step2 代理看到一个新连接,读取 HTTP 标头并查看他必须执行的请求。他执行请求。
Step3 代理收到来自请求的答复。现在他必须将答案从网络发送给客户端,但如何???
有用的链接
Mentalis Proxy:我发现这个项目是一个代理(但我想要更多)。我可能会检查源代码,但我真的想要一些基本的东西来更多地理解这个概念。
ASP Proxy : 我也可以在这里得到一些信息。
Request reflector:这是一个简单的例子。
这是Git Hub Repository with a Simple Http Proxy。
【问题讨论】:
其实是archive.org does have it。很抱歉打扰您。 【参考方案1】:我不会使用 HttpListener 或类似的东西,那样你会遇到很多问题。
最重要的是,支持将是一个巨大的痛苦:
代理保活 SSL 不起作用(以正确的方式,您会看到弹出窗口) .NET 库严格遵循 RFC,这会导致某些请求失败(即使 IE、FF 和世界上任何其他浏览器都可以使用。)你需要做的是:
监听 TCP 端口 解析浏览器请求 提取主机连接到 TCP 级别的主机 来回转发所有内容,除非您想添加自定义标题等。我在 .NET 中编写了 2 个具有不同要求的不同 HTTP 代理,我可以告诉你这是最好的方法。
Mentalis 这样做,但他们的代码是“代理意大利面条”,比 GoTo 更糟糕:)
【讨论】:
您使用什么类进行 TCP 连接? @cameron TCPListener 和 SslStream。 能否分享一下为什么 HTTPS 不起作用的经验? @Restuta 为使 SSL 工作,您应该转发连接,而无需在 TCP 级别实际接触它,而 HttpListener 无法做到这一点。您可以阅读 SSL 的工作原理,您会看到它需要对目标服务器进行身份验证。所以客户端会尝试连接到google.com,但实际上会连接你的Httplistener,它不是google.com,并且会出现证书不匹配错误,因为你的监听器不会使用签名证书,会得到不正确的证书等。你可以修复通过将 CA 安装到客户端将使用的计算机来实现。这是一个非常肮脏的解决方案。 @dr.evil : +++1 感谢您的精彩提示,但我很好奇如何将数据发送回客户端(浏览器),假设我有 TcpClient 我应该如何发送响应给客户?【参考方案2】:您可以使用HttpListener
类来监听传入的请求,并使用HttpWebRequest
类来中继请求。
【讨论】:
我在哪里中继?我怎么知道在哪里发回信息?浏览器发送到 127.0.0.1:9999 让位于 9999 的客户端获取请求并将其发送到网络。得到答案... 比客户做什么?寄到什么地址? 如果您使用 HttpListener,您只需将响应写入 HttpListener.GetContext().Response.OutputStream。无需关心地址。 有意思,我就这样查。 我不会为此使用 HttpListener。相反,构建一个 ASP.NET 应用程序并将其托管在 IIS 中。使用 HttpListener 时,您将放弃 IIS 提供的进程模型。这意味着您会丢失进程管理(启动、故障检测、回收)、线程池管理等内容。 也就是说,如果您打算将它用于许多客户端计算机...对于玩具代理 HttpListener 是可以的...【参考方案3】:我最近使用 TcpListener 和 TcpClient 在 c# .net 中编写了一个轻量级代理。
https://github.com/titanium007/Titanium-Web-Proxy
它以正确的方式支持安全HTTP,客户端机器需要信任代理使用的根证书。还支持 WebSockets 中继。除流水线外,支持 HTTP 1.1 的所有功能。无论如何,大多数现代浏览器都不使用流水线。还支持 Windows 身份验证(普通、摘要)。
您可以通过引用项目来连接您的应用程序,然后查看和修改所有流量。 (请求和响应)。
就性能而言,我已经在我的机器上对其进行了测试,并且没有任何明显的延迟。
【讨论】:
2020年还在维护,感谢分享:)【参考方案4】:代理可以通过以下方式工作。
Step1,配置客户端使用proxyHost:proxyPort。
Proxy 是侦听 proxyHost:proxyPort 的 TCP 服务器。 浏览器打开与 Proxy 的连接并发送 Http 请求。 代理解析此请求并尝试检测“主机”标头。此标头将告诉代理在哪里打开连接。
第 2 步:代理打开到“主机”标头中指定的地址的连接。然后它将 HTTP 请求发送到该远程服务器。读取响应。
步骤 3:从远程 HTTP 服务器读取响应后,Proxy 通过较早打开的与浏览器的 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
【讨论】:
【参考方案5】:如果你只是想拦截流量,你可以使用提琴手核心来创建一个代理......
http://fiddler.wikidot.com/fiddlercore
首先使用 UI 运行 fiddler 以查看它的作用,它是一个代理,允许您调试 http/https 流量。它是用 c# 编写的,有一个核心,您可以将其构建到您自己的应用程序中。
请记住,FiddlerCore 对商业应用程序不是免费的。
【讨论】:
【参考方案6】:同意邪恶博士 如果你使用 HTTPListener 你会遇到很多问题,你必须解析请求,并且会参与到 headers 和 ...
-
使用 tcp 监听器监听浏览器请求
仅解析请求的第一行并获取要连接的主机域和端口
在浏览器请求的第一行向找到的主机发送准确的原始请求
从目标站点接收数据(我在这部分有问题)
将从主机接收到的准确数据发送到浏览器
您看到您甚至不需要知道浏览器请求中的内容并对其进行解析,只需从第一行获取目标站点地址 第一行通常喜欢这个 获取http://google.comHTTP1.1 要么 CONNECT facebook.com:443(这是用于 ssl 请求)
【讨论】:
【参考方案7】:使用 OWIN 和 WebAPI 让事情变得非常简单。在我搜索 C# 代理服务器时,我还看到了这篇文章 http://blog.kloud.com.au/2013/11/24/do-it-yourself-web-api-proxy/ 。这就是我要走的路。
【讨论】:
【参考方案8】:Socks4 是一个实现起来非常简单的协议。您侦听初始连接,连接到客户端请求的主机/端口,将成功代码发送到客户端,然后通过套接字转发传出和传入流。
如果您使用 HTTP,则必须阅读并可能设置/删除一些 HTTP 标头,这样就需要做更多的工作。
如果我没记错的话,SSL 可以跨 HTTP 和 Socks 代理工作。对于 HTTP 代理,您实现 CONNECT 动词,其工作方式与上述 socks4 非常相似,然后客户端通过代理的 tcp 流打开 SSL 连接。
【讨论】:
【参考方案9】:对于它的价值,这是一个基于 HttpListener 和 HttpClient 的 C# 示例异步实现(我使用它能够将 android 设备中的 Chrome 连接到 IIS Express,这是我发现的唯一方法... )。
如果您需要 HTTPS 支持,则不需要更多代码,只需配置证书:Httplistener with HTTPS support
// 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;
【讨论】:
非常简单!伟人!【参考方案10】:浏览器已连接到代理,因此代理从 Web 服务器获取的数据只是通过浏览器向代理发起的同一连接发送。
【讨论】:
以上是关于如何在 C# 中创建一个简单的代理?的主要内容,如果未能解决你的问题,请参考以下文章