C#で単純なプロキシを作成する方法


143

私は数週間前にPrivoxyをダウンロードしましたが、その楽しみのために、単純なバージョンのPrivoxyがどのように実行できるか知りたいと思っていました。

リクエストをプロキシに送信するようにブラウザ(クライアント)を構成する必要があることを理解しています。プロキシはリクエストをウェブに送信します(httpプロキシであるとしましょう)。プロキシは答えを受け取ります...しかし、プロキシはどのようにしてブラウザ(クライアント)にリクエストを送り返すことができますか?

WebでC#とhttpプロキシを検索しましたが、舞台裏でどのように機能するかを理解するための何かが見つかりませんでした。(私はリバースプロキシが不要だと思いますが、わかりません)。

この小さなプロジェクトを続行するための説明や情報はありますか?

更新

これは私が理解していることです(下の図を参照)。

ステップ1すべての要求がプロキシがリッスンするポートで127.0.0.1に送信されるようにクライアント(ブラウザ)を構成します。このように、リクエストは直接インターネットに送信されず、プロキシによって処理されます。

ステップ2プロキシは新しい接続を確認し、HTTPヘッダーを読み取り、実行する必要のあるリクエストを確認します。彼は要求を実行します。

ステップ3プロキシはリクエストから回答を受け取ります。今、彼はウェブからクライアントに答えを送る必要がありますが、どうやって???

代替テキスト

お役立ちリンク

Mentalis Proxy:このプロジェクトはプロキシです(ただし、もっと欲しい)。私はソースをチェックするかもしれませんが、コンセプトをより理解するために基本的なものが本当に欲しかったです。

ASPプロキシ:ここでもいくつかの情報を取得できる場合があります。

要求リフレクター:これは簡単な例です。

以下は、シンプルなHttpプロキシを備えGit Hubリポジトリです。


2015年の2008年のスクリーンショットはありません。申し訳ありません。
Patrick Desjardins

実際、archive.orgにはあることがわかりました。お邪魔して申し訳ありません。
Ilmari Karonen、2015

回答:


35

HttpListener着信要求をリッスンするHttpWebRequestクラスと、要求を中継するクラスを使用して1つを構築できます。


どこで中継しますか?情報を送り返す場所を知るにはどうすればよいですか?ブラウザの送信先は、99.99のクライアント127.0.0.1:9999に要求を取得させ、それをWebに送信させます。答えを得る...クライアントが行うことよりも?どのアドレスに送信しますか?
Patrick Desjardins、

2
HttpListenerを使用している場合は、応答をHttpListener.GetContext()。Response.OutputStreamに書き込むだけです。アドレスを気にする必要はありません。
OregonGhost 2008年

興味深いことに、私はこの方法で確認します。
Patrick Desjardins、

8
これにはHttpListenerを使用しません。代わりに、ASP.NETアプリをビルドしてIIS内でホストします。HttpListenerを使用すると、IISが提供するプロセスモデルをあきらめることになります。つまり、プロセス管理(起動、障害検出、リサイクル)、スレッドプール管理などが失われます。
Mauricio Scheffer、

2
つまり、多くのクライアントコンピューターで使用する場合...おもちゃのプロキシではHttpListenerは問題ありません...
Mauricio Scheffer

94

私はHttpListenerなどを使用しません。そのようにすると、非常に多くの問題が発生します。

最も重要なのは、サポートするのが非常に困難になることです。

  • プロキシキープアライブ
  • SSLが機能しない(正しい方法では、ポップアップが表示されます)
  • .NETライブラリは厳密にRFCに準拠しているため、一部のリクエストが失敗します(IE、FF、および世界中の他のブラウザは動作しますが)。

あなたがする必要があるのは:

  • TCPポートをリッスンする
  • ブラウザのリクエストを解析する
  • ホストを抽出し、TCPレベルでそのホストに接続する
  • カスタムヘッダーなどを追加する場合を除き、すべてを前後に転送します。

.NETで2つの異なるHTTPプロキシをさまざまな要件で作成しましたが、これが最適な方法であることがわかります。

メンタリスはこれを行っていますが、コードは「デリゲートスパゲッティ」であり、GoToよりも悪いです:)


1
TCP接続にどのクラスを使用しましたか?
Cameron

8
@cameron TCPListenerおよびSslStream。
dr。邪悪な

2
HTTPSが機能しない理由を教えてください。
Restuta 2011

10
SSLが機能するための@Restutaは、TCPレベルで実際に接続せずに接続を転送する必要があり、HttpListenerはそれを実行できません。SSLがどのように機能するかを読むと、ターゲットサーバーへの認証にSSLが必要であることがわかります。接続しようとするクライアントはそうgoogle.comが、実際にされていない、あなたのHttpListenerをを接続するgoogle.comと証明書不一致エラーを取得し、あなたのリスナーが署名した証明書を使用していないので、証明書などの間違ったでしょうあなたが修正することができますただし、クライアントが使用するコンピュータにCAをインストールします。それはかなり汚いソリューションです。
dr。邪悪な

1
@ dr.evil:+++ 1すばらしいヒントをありがとう、しかしクライアント(ブラウザ)にデータを返信する方法に興味があります。TcpClientでクライアントに応答を返信するにはどうすればよいですか?
2012

26

私は最近、c#.netでTcpListenerTcpClientを使用して軽量プロキシを作成しました

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

安全なHTTPを正しくサポートし、クライアントマシンはプロキシで使用されるルート証明書を信頼する必要があります。WebSocketsリレーもサポートします。パイプライン処理を除き、HTTP 1.1のすべての機能がサポートされています。とにかく、ほとんどの最新のブラウザーではパイプライン処理は使用されていません。Windows認証(プレーン、ダイジェスト)もサポートします。

プロジェクトを参照してアプリケーションを接続し、すべてのトラフィックを確認および変更できます。(リクエストとレスポンス)。

パフォーマンスに関しては、自分のマシンでテストしましたが、目立った遅延なしで動作します。


共有してくれてありがとう:)
マークアダムソン

20

プロキシは次のように機能します。

ステップ1、proxyHost:proxyPortを使用するようにクライアントを構成します。

プロキシは、proxyHost:proxyPortでリッスンしているTCPサーバーです。ブラウザはプロキシとの接続を開き、Httpリクエストを送信します。プロキシはこのリクエストを解析し、「ホスト」ヘッダーの検出を試みます。このヘッダーは、接続を開く場所をプロキシに通知します。

ステップ2:プロキシは、「ホスト」ヘッダーで指定されたアドレスへの接続を開きます。次に、そのリモートサーバーに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

14

トラフィックを傍受するだけの場合は、fiddlerコアを使用してプロキシを作成できます...

http://fiddler.wikidot.com/fiddlercore

最初にUIを使用してフィドラーを実行し、その動作を確認します。これは、http / httpsトラフィックをデバッグできるプロキシです。それはc#で書かれており、独自のアプリケーションに組み込むことができるコアを持っています。

FiddlerCoreは商用アプリケーションでは無料ではないことに注意してください。


6

HTTPListenerを使用している場合は悪事に同意し、多くの問題が発生し、リクエストを解析する必要があり、ヘッダーに関与します...

  1. tcpリスナーを使用してブラウザー要求をリッスンする
  2. リクエストの最初の行のみを解析して、接続するホストドメインとポートを取得する
  3. ブラウザリクエストの最初の行で、見つかったホストに正確なrawリクエストを送信します
  4. ターゲットサイトからデータを受信します(このセクションに問題があります)
  5. ホストから受信した正確なデータをブラウザに送信する

あなたはあなたも、ブラウザの要求であるか知っているし、それを解析する必要がいけない参照のみ通常好きな最初の行の最初の行からターゲットサイトのアドレスを取得し、このGET http://google.com HTTP1.1またはCONNECT facebook.com: 443(これはsslリクエスト用です)



5

Socks4は、実装が非常に簡単なプロトコルです。初期接続をリッスンし、クライアントから要求されたホスト/ポートに接続し、成功コードをクライアントに送信して、ソケットを介して送信ストリームと受信ストリームを転送します。

HTTPを使用する場合は、いくつかのHTTPヘッダーを読み取り、場合によっては設定/削除する必要があるため、少し作業が増えます。

私の記憶が正しければ、SSLはHTTPプロキシとSocksプロキシで機能します。HTTPプロキシの場合、上記のsocks4のように機能するCONNECT動詞を実装すると、クライアントはプロキシされたtcpストリームを介してSSL接続を開きます。


2

ブラウザーはプロキシーに接続されているため、プロキシーがWebサーバーから取得するデータは、ブラウザーがプロキシーに開始したのと同じ接続を介して送信されるだけです。


2

価値があるのは、HttpListenerHttpClientに基づくC#のサンプル非同期実装です(AndroidデバイスのChromeをIIS Expressに接続できるようにするために使用しています。これが唯一の方法です...)。

HTTPSサポートが必要な場合は、コードを追加する必要はありません。証明書の構成だけが必要です。HTTPSサポート付きのHttplistener

// 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;
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.