จะสร้าง proxy อย่างง่ายใน C # ได้อย่างไร


143

ฉันได้ดาวน์โหลด Privoxy เมื่อไม่กี่สัปดาห์ที่ผ่านมาและเพื่อความสนุกฉันอยากรู้ว่ามันง่ายเพียงใด

ฉันเข้าใจว่าฉันต้องกำหนดค่าเบราว์เซอร์ (ไคลเอนต์) เพื่อส่งคำขอไปยังพร็อกซี พร็อกซีส่งคำขอไปยังเว็บ (สมมติว่าเป็นพร็อกซี http) พร็อกซีจะได้รับคำตอบ ... แต่พร็อกซีจะส่งคำขอกลับไปยังเบราว์เซอร์ (ไคลเอ็นต์) ได้อย่างไร

ฉันค้นหา C # และ http proxy บนเว็บ แต่ไม่พบสิ่งใดที่ให้ฉันเข้าใจว่ามันทำงานอย่างไรได้อย่างถูกต้องเบื้องหลัง (ฉันเชื่อว่าฉันไม่ต้องการพร็อกซีย้อนกลับ แต่ฉันไม่แน่ใจ)

มีใครบ้างที่มีคำอธิบายหรือข้อมูลบางอย่างที่จะให้ฉันดำเนินโครงการเล็ก ๆ นี้ต่อไป?

ปรับปรุง

นี่คือสิ่งที่ฉันเข้าใจ (ดูกราฟด้านล่าง)

ขั้นตอนที่ 1ฉันกำหนดค่าไคลเอนต์ (เบราว์เซอร์) สำหรับคำขอทั้งหมดที่จะส่งไปยัง 127.0.0.1 ที่พอร์ตที่ฟัง Proxy วิธีนี้คำขอจะไม่ถูกส่งไปยังอินเทอร์เน็ตโดยตรง แต่จะดำเนินการโดยพร็อกซี

ขั้นที่ 2พร็อกซีเห็นการเชื่อมต่อใหม่อ่านส่วนหัว HTTP และดูคำขอที่เขาต้องดำเนินการ เขาดำเนินการตามคำขอ

ขั้นตอนที่ 3พร็อกซีได้รับคำตอบจากคำขอ ตอนนี้เขาต้องส่งคำตอบจากเว็บไปยังลูกค้า แต่อย่างไร

ข้อความแสดงแทน

ลิงค์ที่มีประโยชน์

Mentalis Proxy : ฉันพบโครงการนี้ซึ่งเป็นพร็อกซี (แต่มากกว่าที่ฉันต้องการ) ฉันอาจตรวจสอบแหล่งที่มา แต่ฉันต้องการบางสิ่งบางอย่างพื้นฐานเพื่อทำความเข้าใจแนวคิดเพิ่มเติม

ASP Proxy : ฉันอาจได้รับข้อมูลบางอย่างจากที่นี่ด้วย

ขอตัวสะท้อนสัญญาณ : นี่เป็นตัวอย่างง่ายๆ

นี่คือGit Hub พื้นที่เก็บข้อมูลที่มีความเรียบง่าย Http พร็อกซี่


ฉันไม่มีภาพหน้าจอของปี 2008 ในปี 2015 ขออภัย
Patrick Desjardins

จริงๆแล้วมันกลับกลายเป็นว่าarchive.org มีอยู่ ขอโทษที่รบกวนคุณ.
Ilmari Karonen

คำตอบ:


35

คุณสามารถสร้างHttpListenerคลาสขึ้นมาหนึ่งคลาสเพื่อรับฟังคำร้องขอที่เข้ามาและHttpWebRequestชั้นเรียนเพื่อถ่ายทอดคำขอ


ฉันจะถ่ายทอดได้ที่ไหน ฉันจะทราบได้อย่างไรว่าจะส่งข้อมูลกลับไปที่ใด? เบราว์เซอร์ส่งไปให้ 127.0.0.1:9999 กล่าวว่าไคลเอนต์ที่ 9999 ได้รับคำขอและส่งไปยังเว็บ รับคำตอบ ... กว่าสิ่งที่ลูกค้าทำ ส่งไปยังที่อยู่ใด
Patrick Desjardins

2
หากคุณใช้ HttpListener คุณเพียงแค่เขียนคำตอบไปที่ HttpListener.GetContext (). Response.OutputStream ไม่จำเป็นต้องดูแลที่อยู่
OregonGhost

น่าสนใจฉันจะตรวจสอบด้วยวิธีนี้
Patrick Desjardins

8
ฉันจะไม่ใช้ HttpListener สำหรับสิ่งนี้ สร้างแอป ASP.NET แทนและโฮสต์ภายใน IIS แทน เมื่อใช้ HttpListener คุณกำลังละทิ้งโมเดลกระบวนการโดย IIS ซึ่งหมายความว่าคุณสูญเสียสิ่งต่างๆเช่นการจัดการกระบวนการ (การเริ่มต้นการตรวจสอบความล้มเหลวการรีไซเคิล) การจัดการกลุ่มเธรด
Mauricio Scheffer

2
นั่นคือถ้าคุณตั้งใจจะใช้มันสำหรับคอมพิวเตอร์ไคลเอนต์จำนวนมาก ... สำหรับพร็อกซีของเล่น HttpListener ก็โอเค ...
Mauricio Scheffer

94

ฉันจะไม่ใช้ HttpListener หรืออะไรทำนองนั้นในแบบที่คุณจะเจอกับปัญหามากมาย

สิ่งสำคัญที่สุดคือมันจะเป็นความเจ็บปวดอย่างมากที่จะสนับสนุน:

  • Proxy Keep-Alives
  • SSL จะไม่ทำงาน (ในวิธีที่ถูกต้องคุณจะได้รับป๊อปอัป)
  • . NET ไลบรารีจะติดตาม RFC อย่างเคร่งครัดซึ่งทำให้บางคำขอล้มเหลว (แม้ว่า IE, FF และเบราว์เซอร์อื่น ๆ ในโลกจะทำงานได้)

สิ่งที่คุณต้องทำคือ:

  • ฟังพอร์ต TCP
  • แยกคำขอเบราว์เซอร์
  • แตกโฮสต์เชื่อมต่อกับโฮสต์นั้นในระดับ TCP
  • ส่งต่อทุกอย่างไปมาเว้นแต่คุณต้องการเพิ่มส่วนหัวที่กำหนดเอง ฯลฯ

ฉันเขียนพร็อกซี HTTP ที่แตกต่างกัน 2 รายการใน. NET ด้วยข้อกำหนดที่แตกต่างกันและฉันสามารถบอกคุณได้ว่านี่เป็นวิธีที่ดีที่สุดในการทำ

Mentalis ทำสิ่งนี้ แต่รหัสของพวกเขาคือ "ตัวแทนปาเก็ตตี้" แย่กว่า GoTo :)


1
คุณใช้คลาสใดในการเชื่อมต่อ TCP
คาเมรอน

8
@cameron TCPListener และ SslStream
ดร. ชั่วร้าย

2
คุณช่วยแบ่งปันประสบการณ์ที่ทำให้ HTTPS ไม่ทำงานได้ไหม
Restuta

10
@Restuta เพื่อให้ SSL ทำงานได้คุณควรส่งต่อการเชื่อมต่อโดยไม่ต้องแตะระดับ TCP และ HttpListener ไม่สามารถทำได้ คุณสามารถอ่านวิธีการทำงานของ SSL และคุณจำเป็นต้องตรวจสอบความถูกต้องกับเซิร์ฟเวอร์เป้าหมาย ดังนั้นลูกค้าจะพยายามเชื่อมต่อกับgoogle.comแต่จริง ๆ แล้วจะเชื่อมต่อ Httplistener ของคุณซึ่งไม่ใช่google.comและจะได้รับข้อผิดพลาดใบรับรองที่ไม่ตรงกันและเนื่องจากผู้ฟังของคุณจะไม่ใช้ใบรับรองที่ลงนามแล้วจะได้รับใบรับรองที่ไม่ถูกต้องเป็นต้น โดยการติดตั้ง CA ลงในคอมพิวเตอร์ที่ไคลเอ็นต์จะใช้ มันเป็นทางออกที่สกปรกมาก
ดร. ชั่วร้าย

1
@ dr.evil: +++ 1 ขอบคุณสำหรับเคล็ดลับที่น่าทึ่ง แต่ฉันอยากรู้วิธีการส่งข้อมูลกลับไปยังลูกค้า (เบราว์เซอร์) ให้บอกว่าฉัน TcpClient ฉันจะส่งการตอบกลับไปยังลูกค้าได้อย่างไร?
กระบี่

26

ฉันได้เขียนเมื่อเร็ว ๆ นี้พร็อกซีน้ำหนักเบาใน c # .net ใช้TcpListenerและTcpClient

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

สนับสนุน HTTP ที่ปลอดภัยในวิธีที่ถูกต้องเครื่องไคลเอนต์ต้องเชื่อถือใบรับรองหลักที่ใช้โดยพร็อกซี นอกจากนี้ยังรองรับการถ่ายทอด WebSockets รองรับฟีเจอร์ทั้งหมดของ HTTP 1.1 ยกเว้น pipelining Pipelining ไม่ได้ถูกใช้โดยเบราว์เซอร์ที่ทันสมัยที่สุดอย่างไรก็ตาม นอกจากนี้ยังรองรับการตรวจสอบ windows (ธรรมดาย่อย)

คุณสามารถขอใบสมัครของคุณโดยอ้างอิงโครงการจากนั้นดูและแก้ไขปริมาณการใช้งานทั้งหมด (คำขอและการตอบสนอง)

เท่าที่ประสิทธิภาพฉันได้ทดสอบบนเครื่องของฉันและทำงานได้โดยไม่ล่าช้า


และยังคงอยู่ในปี 2020 ขอบคุณสำหรับการแบ่งปัน :)
Mark Adamson

20

พร็อกซีสามารถทำงานในวิธีต่อไปนี้

ขั้นที่ 1 กำหนดค่าไคลเอนต์ให้ใช้ proxyHost: proxyPort

พร็อกซีเป็นเซิร์ฟเวอร์ TCP ที่กำลังฟังบน proxyHost: proxyPort เบราว์เซอร์เปิดการเชื่อมต่อกับพร็อกซีและส่งคำขอ 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

หากคุณเพียงแค่ต้องการสกัดกั้นทราฟฟิกคุณสามารถใช้หลักพู้ทำเล่นเพื่อสร้างพร็อกซี ...

http://fiddler.wikidot.com/fiddlercore

เรียกใช้พู้ทำเล่นคนแรกกับ UI เพื่อดูว่ามันทำอะไรมันเป็นพร็อกซี่ที่ช่วยให้คุณสามารถแก้ปัญหาปริมาณการใช้งาน http / https มันเขียนใน c # และมีแกนกลางที่คุณสามารถสร้างเป็นแอปพลิเคชันของคุณเอง

โปรดทราบว่า FiddlerCore ไม่ได้ฟรีสำหรับการใช้งานเชิงพาณิชย์


6

เห็นด้วยกับความชั่วร้ายถ้าคุณใช้ HTTPListener คุณจะมีปัญหามากมายคุณต้องแยกวิเคราะห์คำขอและจะหมั้นกับส่วนหัวและ ...

  1. ใช้ฟัง tcp เพื่อฟังคำขอของเบราว์เซอร์
  2. แยกเฉพาะบรรทัดแรกของคำขอและรับโดเมนโฮสต์และพอร์ตเพื่อเชื่อมต่อ
  3. ส่งคำขอดิบที่แน่นอนไปยังโฮสต์ที่พบในบรรทัดแรกของคำขอเบราว์เซอร์
  4. รับข้อมูลจากเว็บไซต์เป้าหมาย (ฉันมีปัญหาในส่วนนี้)
  5. ส่งข้อมูลที่แน่นอนที่ได้รับจากโฮสต์ไปยังเบราว์เซอร์

คุณเห็นว่าคุณไม่จำเป็นต้องรู้ว่าสิ่งที่อยู่ในคำขอของเบราว์เซอร์และแยกมันเพียงรับที่อยู่เว็บไซต์เป้าหมายจากบรรทัดแรกบรรทัดแรกมักจะชอบ GET นี้http://google.com HTTP1.1 หรือเชื่อมต่อ facebook.com: 443 (ใช้สำหรับการร้องขอ SSL)


6

ทุกอย่างกลายเป็นเรื่องง่ายด้วย OWIN และ WebAPI ในการค้นหาของฉันสำหรับเซิร์ฟเวอร์พร็อกซี C # ฉันยังมาในโพสต์นี้http://blog.kloud.com.au/2013/11/24/do-it-yourself-web-api-proxy/ นี่จะเป็นถนนที่ฉันกำลังไป


5

Socks4 เป็นโปรโตคอลที่ใช้งานง่ายมาก คุณฟังการเชื่อมต่อเริ่มต้นเชื่อมต่อกับโฮสต์ / พอร์ตที่ลูกค้าร้องขอส่งรหัสความสำเร็จไปยังไคลเอนต์จากนั้นส่งต่อกระแสขาออกและขาเข้าขาออกข้ามซ็อกเก็ต

ถ้าคุณใช้ HTTP คุณจะต้องอ่านและอาจจะตั้งค่า / ลบส่วนหัว HTTP บางส่วนเพื่อให้ทำงานได้มากขึ้นอีกเล็กน้อย

ถ้าฉันจำได้อย่างถูกต้อง SSL จะทำงานกับ HTTP และถุงเท้าพร็อกซี่ สำหรับพร็อกซี HTTP คุณใช้ CONNECT verb ซึ่งทำงานคล้ายกับ socks4 ตามที่อธิบายไว้ข้างต้นจากนั้นไคลเอ็นต์จะเปิดการเชื่อมต่อ SSL ข้ามสตรีมพร็อกซี tcp


2

เบราว์เซอร์เชื่อมต่อกับพร็อกซีดังนั้นข้อมูลที่พร็อกซีได้รับจากเว็บเซิร์ฟเวอร์จะถูกส่งผ่านการเชื่อมต่อเดียวกับที่เบราว์เซอร์เริ่มต้นไปยังพร็อกซี


2

นี่คือการใช้งาน async ตัวอย่าง C # ตามHttpListenerและHttpClient (ฉันใช้เพื่อเชื่อมต่อ Chrome ในอุปกรณ์ Android กับ IIS Express นั่นเป็นวิธีเดียวที่ฉันพบ ... )

และหากคุณต้องการการสนับสนุน HTTPS ก็ไม่จำเป็นต้องใช้รหัสเพิ่มเติมเพียงแค่กำหนดค่าใบรับรอง: Httplistener พร้อมการสนับสนุน HTTPS

// 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.