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

152
我几周前下载了Privoxy,出于好奇想知道如何制作一个简单版本。我知道需要配置浏览器(客户端)以将请求发送到代理。代理将请求发送到Web(假设它是http代理)。代理将接收答案...但代理如何将请求发送回浏览器(客户端)呢?
我在网上搜索了C#和http代理,但没有找到让我正确理解其背后工作原理的东西。(我相信我不想要反向代理,但我不确定。)
你们有什么解释或信息可以让我继续这个小项目吗?
更新
这就是我的理解(见下图)。
步骤1:我将客户端(浏览器)配置为将所有请求发送到代理侦听的端口的127.0.0.1。这样,请求将不会直接发送到互联网,而是由代理处理。
步骤2:代理看到一个新连接,读取HTTP头并查看他必须执行的请求。他执行请求。
步骤3:代理从请求中接收一个答案。现在他必须将答案从Web发送回客户端,但如何实现?

alt text

有用的链接

Mentalis Proxy:我发现了这个项目,它是一个代理(但更多),我可能会查看源代码,但我真的希望有一些基础的东西来更好地理解概念。

ASP Proxy:我也可能在这里获得一些信息。

Request reflector:这是一个简单的例子。

这里有一个Git Hub Repository with a Simple Http Proxy


实际上,事实证明archive.org有它。很抱歉打扰你了。 - Ilmari Karonen
10个回答

96

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

最重要的是,这会带来巨大的支持痛苦:

  • 代理保持活动(Proxy Keep-Alives)
  • SSL不会正常工作(你会得到弹窗)
  • .NET库严格遵循RFC,导致一些请求失败(即使IE、FF和全世界任何其他浏览器都能正常工作。)

你需要做的是:

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

我用.NET写了2个不同的HTTP代理,我可以告诉您这是最好的方法。

Mentalis正在做这件事,但他们的代码是"委托面条",比GoTo还糟糕:)


1
你用了哪个类来处理TCP连接? - Cameron
8
TCPListener和SslStream - dr. evil
2
请分享一下您的经验,为什么HTTPS无法工作? - Restuta
11
为了使SSL正常工作,您应该在TCP层面上转发连接而不实际触及它,而HttpListener无法做到这一点。 您可以了解SSL的工作原理,您会看到需要对目标服务器进行身份验证。 因此,客户端将尝试连接到https://google.com,但实际上会连接到您的Httplistener,而不是https://google.com,并且会收到证书不匹配错误,由于您的监听器将不使用已签名的证书,因此还会出现不正确的证书等问题。 但是,您可以通过为客户端使用的计算机安装CA来解决此问题。 这是一个相当麻烦的解决方案。 - dr. evil
1
@dr.evil:+++1 感谢您提供的精彩技巧,但我很好奇如何将数据发送回客户端(浏览器),比方说我有一个TcpClient,我应该如何将响应发送回客户端? - Saber Amani
显示剩余2条评论

37

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

https://github.com/titanium007/Titanium-Web-Proxy

它支持安全的HTTP方式,客户端机器需要信任代理使用的根证书。还支持WebSockets中继。除了流水线处理之外,支持HTTP 1.1的所有功能。不过,现代浏览器大多不使用流水线处理。还支持Windows身份验证(明文,摘要)。您可以通过引用该项目来连接您的应用程序,然后查看和修改所有流量(请求和响应)。就性能而言,我已在我的计算机上进行了测试,没有任何明显的延迟。

2
仍然在2020年得以维护,感谢分享 :) - Mark Adamson
@MarkAdamson 跟进一下,它在2022年仍在维护中。名副其实的钛金属,做得很好! - fmoliveira
链接的 GitHub 仓库已于2023年7月8日归档。 - undefined
它已经死了...... - undefined

36

我应该中继到哪里?我怎么知道要把信息发送回哪里?浏览器发送到127.0.0.1:9999,客户端在9999处接收请求并将其发送到Web。得到答案后...那么客户端该怎么做?发送到哪个地址? - Patrick Desjardins
3
如果您正在使用HttpListener,只需将响应写入HttpListener.GetContext().Response.OutputStream中,无需关心地址。 - OregonGhost
有趣,我会用这种方式检查。 - Patrick Desjardins
8
我不会使用HttpListener来完成这项任务。相反地,您应该建立一个ASP.NET应用程序并在IIS中进行托管。当使用HttpListener时,您放弃了IIS提供的进程模型。这意味着您会失去像进程管理(启动、故障检测、回收)和线程池管理等功能。 - Mauricio Scheffer
2
那么,如果您打算将其用于许多客户端计算机... 对于玩具代理HttpListener可以,但不建议。 - Mauricio Scheffer

21

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

第一步,配置客户端使用代理主机(proxyHost)和代理端口(proxyPort)。

代理是一个TCP服务器,监听在proxyHost:proxyPort上。浏览器与代理建立连接,并发送HTTP请求。代理解析此请求并尝试检测“Host”头信息。该头信息将告诉代理在哪里打开连接。

第二步:代理打开到“Host”头信息指定地址的连接。然后它向远程服务器发送HTTP请求,并读取响应。

第三步:从远程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

14

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

http://fiddler.wikidot.com/fiddlercore

先运行Fiddler并使用UI查看其功能,它是一款代理工具,可以帮助您调试HTTP/HTTPS流量。它使用C#编写,并拥有一个核心库,可供您构建自己的应用程序。
请注意,商业应用程序使用FiddlerCore需要付费。

6

如果您使用HTTPListener,将会遇到很多问题,需要解析请求并处理头部等问题。建议使用TCP Listener来监听浏览器请求。

  1. 使用TCP Listener来监听浏览器请求
  2. 只解析请求的第一行,并获取连接的目标站点域名和端口号
  3. 将准确的原始请求发送到找到的目标站点
  4. 从目标站点接收数据(在这个步骤中可能会有问题)
  5. 将从目标站点接收到的准确数据发送回浏览器

您可以看到,您甚至不需要知道浏览器请求中的内容并对其进行解析,只需从第一行获取目标站点地址即可。第一行通常如下所示:

GET http://google.com HTTP1.1 或者 CONNECT facebook.com:443(用于SSL请求)


6

5
Socks4是一种非常简单的协议实现。您需要监听初始连接,连接到客户端请求的主机/端口,将成功代码发送给客户端,然后通过套接字转发出站和入站流。如果选择HTTP,则需要读取并可能设置/删除一些HTTP标头,因此工作量会更大。如果我没记错的话,SSL可以在HTTP和Socks代理之间工作。对于HTTP代理,您需要实现CONNECT动词,它的工作方式与上面描述的Socks4类似,然后客户端通过代理的TCP流打开SSL连接。

4

以下是一份基于HttpListenerHttpClient的C#样例异步实现(我使用它将Chrome在Android设备上连接到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;
    }
}

2
浏览器与代理服务器相连,因此代理从Web服务器获取的数据只是通过浏览器与代理建立的同一连接发送。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接