C #에서 간단한 프록시를 만드는 방법은 무엇입니까?


143

몇 주 전에 Privoxy를 다운로드했으며 재미를 위해 간단한 버전의 버전을 수행하는 방법을 알고 싶었습니다.

프록시에 요청을 보내려면 브라우저 (클라이언트)를 구성해야한다는 것을 알고 있습니다. 프록시는 요청을 웹에 보냅니다 (http 프록시라고 말하십시오). 프록시는 응답을 수신하지만 프록시는 어떻게 요청을 브라우저 (클라이언트)에게 다시 보낼 수 있습니까?

웹에서 C # 및 http 프록시를 검색했지만 장면 뒤에서 올바르게 작동하는 방식을 이해할 수있는 것을 찾지 못했습니다. (역 프록시를 원하지는 않지만 확실하지 않습니다).

이 소규모 프로젝트를 계속 진행할 수있는 설명이나 정보가 있습니까?

최신 정보

이것이 내가 이해하는 것입니다 (아래 그림 참조).

1 단계 프록시가 수신하는 포트에서 모든 요청을 127.0.0.1로 보내도록 클라이언트 (브라우저)를 구성합니다. 이런 식으로 요청은 인터넷으로 직접 전송되지 않지만 프록시에 의해 처리됩니다.

2 단계 프록시가 새 연결을보고 HTTP 헤더를 읽고 실행해야하는 요청을 확인합니다. 그는 요청을 실행합니다.

3 단계 프록시는 요청으로부터 응답을받습니다. 이제 그는 웹에서 클라이언트로 답변을 보내야하지만 어떻게 ???

대체 텍스트

유용한 링크

Mentalis Proxy :이 프로젝트가 프록시라는 것을 알았습니다. 소스를 확인할 수도 있지만 더 많은 개념을 이해하기 위해 기본적인 것을 원했습니다.

ASP 프록시 : 여기에서도 정보를 얻을 수 있습니다.

리플렉터 요청 : 간단한 예입니다.

다음은 간단한 Http 프록시 가있는 Git Hub 리포지토리입니다 .


2015 년에는 2008 년 스크린 샷이 없습니다. 죄송합니다.
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

93

HttpListener 또는 이와 유사한 것을 사용하지 않으므로 많은 문제가 발생합니다.

가장 중요한 것은 지원하는 데 큰 고통이 될 것입니다.

  • 프록시 연결 유지
  • SSL이 작동하지 않습니다 (올바른 방식으로 팝업이 표시됨)
  • .NET 라이브러리는 RFC를 엄격하게 준수하므로 일부 요청은 실패합니다 (IE, FF 및 기타 다른 브라우저에서도 작동).

당신이해야 할 일은 :

  • TCP 포트 청취
  • 브라우저 요청 파싱
  • TCP 레벨에서 해당 호스트에 호스트 연결 추출
  • 사용자 정의 헤더 등을 추가하지 않으려면 모든 것을 앞뒤로 전달하십시오.

다른 요구 사항으로 .NET에 2 개의 다른 HTTP 프록시를 작성했으며 이것이 최선의 방법이라고 말할 수 있습니다.

Mentalis는이 작업을 수행하지만 코드는 "대리 스파게티"이며 GoTo보다 나쁩니다.)


1
TCP 연결에 어떤 클래스를 사용 했습니까?
Cameron

8
@cameron TCPListener 및 SslStream.
박사. 악

2
HTTPS가 작동하지 않는 이유에 대해 경험을 공유해 주시겠습니까?
Restuta

10
SSL이 작동하려면 @Restuta가 실제로 TCP 수준에서 SSL을 건드리지 않고 연결을 전달해야하며 HttpListener는 그렇게 할 수 없습니다. SSL의 작동 방식을 읽을 수 있으며 대상 서버에 인증해야 함을 알 수 있습니다. 따라서 클라이언트는 google.com 에 연결하려고 시도 하지만 실제로는 google.com 이 아닌 Httplistener를 연결 하고 인증서 불일치 오류가 발생하며 청취자가 서명 된 인증서를 사용하지 않으므로 잘못된 인증서 등이 표시됩니다. 클라이언트가 사용할 컴퓨터에 CA를 설치하면됩니다. 꽤 더러운 해결책입니다.
박사.

1
@ dr.evil : +++ 1 놀라운 팁에 감사하지만 클라이언트 (브라우저)에 데이터를 다시 보내는 방법이 궁금합니다 .TcpClient에 어떻게 응답을 클라이언트에 다시 보내야합니까?
세이버

26

최근에 TcpListenerTcpClient 사용하여 c # .net에 간단한 프록시를 작성했습니다 .

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

클라이언트 시스템은 프록시가 사용하는 루트 인증서를 신뢰해야합니다. WebSockets 릴레이도 지원합니다. 파이프 라이닝을 제외한 HTTP 1.1의 모든 기능이 지원됩니다. 어쨌든 대부분의 최신 브라우저에서는 파이프 라이닝을 사용하지 않습니다. Windows 인증 (일반, 다이제스트)도 지원합니다.

프로젝트를 참조하여 응용 프로그램을 연결 한 다음 모든 트래픽을보고 수정할 수 있습니다. (요청 및 답변).

성능까지는 내 컴퓨터에서 테스트했으며 눈에 띄게 지연없이 작동합니다.


공유해 주셔서 감사합니다 :)
Mark Adamson

19

프록시는 다음과 같은 방식으로 작동 할 수 있습니다.

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

트래픽을 가로 채기를 원하는 경우 피들러 코어를 사용하여 프록시를 만들 수 있습니다.

http://fiddler.wikidot.com/fiddlercore

UI에서 fiddler를 먼저 실행하여 기능을 확인하십시오. http / https 트래픽을 디버깅 할 수있는 프록시입니다. c #으로 작성되었으며 자신의 응용 프로그램에 빌드 할 수있는 핵심이 있습니다.

FiddlerCore는 상업용 응용 프로그램에는 무료가 아닙니다.



5

HTTPListener를 사용하는 경우 악의에 동의하면 많은 문제가 발생하고 요청을 구문 분석해야하며 헤더에 참여하게됩니다 ...

  1. tcp listener를 사용하여 브라우저 요청을 청취
  2. 요청의 첫 번째 줄만 구문 분석하고 호스트 도메인과 포트를 연결하십시오.
  3. 브라우저 요청의 첫 번째 줄에서 찾은 호스트에게 정확한 원시 요청을 보냅니다.
  4. 대상 사이트에서 데이터 수신 (이 섹션에 문제가 있음)
  5. 호스트로부터받은 정확한 데이터를 브라우저로 전송

브라우저 요청에 무엇이 있는지 알 필요가 없으며 구문 분석하고 첫 번째 줄에서만 대상 사이트 주소를 가져옵니다. 첫 번째 줄은 일반적으로 다음 GET http://google.com HTTP1.1 또는 CONNECT facebook.com을 좋아합니다 . 443 (ssl 요청 용)


4

Socks4는 구현하기 매우 간단한 프로토콜입니다. 초기 연결을 수신하고 클라이언트가 요청한 호스트 / 포트에 연결 한 후 성공 코드를 클라이언트로 전송 한 다음 소켓으로 나가고 들어오는 스트림을 전달합니다.

HTTP를 사용하는 경우 약간의 작업을 수행하기 위해 일부 HTTP 헤더를 읽고 설정 / 제거해야합니다.

올바르게 기억하면 SSL은 HTTP 및 Socks 프록시에서 작동합니다. HTTP 프록시의 경우 위에서 설명한 것처럼 socks4와 매우 유사한 CONNECT 동사를 구현하면 클라이언트는 프록시 된 tcp 스트림을 통해 SSL 연결을 엽니 다.


2

브라우저는 프록시에 연결되므로 프록시가 웹 서버에서 가져 오는 데이터는 브라우저가 프록시에 시작한 것과 동일한 연결을 통해 전송됩니다.


1

가치있는 것을 위해 여기에 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.