Как создать простой прокси в C#?
Я скачал Privoxy несколько недель назад, и мне было любопытно узнать, как сделать простую версию.
Я понимаю, что мне нужно настроить браузер (клиент) для отправки запроса на прокси. Прокси-сервер отправляет запрос в Интернет (скажем, это http-прокси). Прокси-сервер получит ответ... но как прокси-сервер может отправить запрос обратно в браузер (клиент)?
Я ищу в Интернете прокси C# и http, но не нашел чего-то, что позволило бы мне понять, как это работает за кулисами. (Я считаю, что я не хочу обратного прокси, но я не уверен).
Есть ли у кого-нибудь из вас какие-либо объяснения или информация, которая позволит мне продолжить этот небольшой проект?
Обновить
Это то, что я понимаю (см. Рисунок ниже).
Шаг 1 Я настраиваю клиент (браузер) для отправки всех запросов на 127.0.0.1 через порт, который прослушивает Прокси. Таким образом, запрос не будет отправлен напрямую в Интернет, а будет обработан прокси.
Шаг 2 Прокси видит новое соединение, читает заголовок HTTP и видит запрос, который он должен выполнить. Он выполняет запрос.
Шаг 3 Прокси-сервер получает ответ на запрос. Теперь он должен отправить ответ из Интернета клиенту, но как???
Полезная ссылка
Mentalis Proxy: Я нашел этот проект, который является прокси (но больше, что я хотел бы). Я мог бы проверить источник, но я действительно хотел чего-то простого, чтобы лучше понять концепцию.
ASP Proxy: Я мог бы также получить некоторую информацию здесь.
Отражатель запроса: это простой пример.
9 ответов
Вы можете построить один с HttpListener
класс для прослушивания входящих запросов и HttpWebRequest
класс для передачи запросов.
Я бы не использовал HttpListener или что-то в этом роде, так что вы столкнетесь с таким количеством проблем.
Самое главное, это будет огромная боль для поддержки:
- Proxy Keep-Alives
- SSL не будет работать (правильно, вы получите всплывающие окна)
- Библиотеки.NET строго следуют RFC, что приводит к сбою некоторых запросов (даже если IE, FF и любой другой браузер в мире будут работать.)
Что вам нужно сделать, это:
- Слушайте порт TCP
- Разобрать запрос браузера
- Извлечь узел подключиться к этому узлу на уровне TCP
- Пересылать все назад и вперед, если вы не хотите добавлять собственные заголовки и т. Д.
Я написал 2 разных HTTP прокси в.NET с разными требованиями и могу вам сказать, что это лучший способ сделать это.
Mentalis делают это, но их код "делегат спагетти", хуже, чем GoTo:)
Недавно я написал облегченный прокси в C# .net, используя TcpListener и TcpClient.
https://github.com/titanium007/Titanium-Web-Proxy
Он поддерживает безопасный HTTP правильно, клиентский компьютер должен доверять корневому сертификату, используемому прокси. Также поддерживает ретрансляцию WebSockets. Поддерживаются все функции HTTP 1.1, кроме конвейерной. В любом случае конвейерная обработка не используется большинством современных браузеров. Также поддерживает проверку подлинности Windows (обычный, дайджест).
Вы можете подключить свое приложение, ссылаясь на проект, а затем посмотреть и изменить весь трафик. (Запрос и ответ).
Что касается производительности, я проверил ее на своей машине и работает без каких-либо заметных задержек.
Прокси может работать следующим образом.
Шаг 1, настройте клиент для использования proxyHost:proxyPort.
Прокси-сервер - это TCP-сервер, который прослушивает proxyHost:proxyPort. Браузер открывает соединение с прокси и отправляет Http-запрос. Прокси анализирует этот запрос и пытается обнаружить заголовок "Host". Этот заголовок скажет Прокси, где открыть соединение.
Шаг 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
Если вы просто хотите перехватить трафик, вы можете использовать ядро Fiddler для создания прокси...
http://fiddler.wikidot.com/fiddlercore
Сначала запустите fiddler с пользовательским интерфейсом, чтобы увидеть, что он делает, это прокси, который позволяет отлаживать трафик http/https. Он написан на C# и имеет ядро, которое вы можете встроить в свои собственные приложения.
Имейте в виду, FiddlerCore не является бесплатным для коммерческих приложений.
Согласитесь с dr evil, если вы будете использовать HTTPListener, у вас будет много проблем, вам придется анализировать запросы и вы будете заняты заголовками и...
- Используйте tcp listener для прослушивания запросов браузера
- анализируем только первую строку запроса и получаем домен хоста и порт для подключения
- отправьте точный необработанный запрос найденному хосту в первой строке запроса браузера
- получить данные с целевого сайта (у меня есть проблемы в этом разделе)
- отправить точные данные, полученные от хоста в браузер
вы видите, что вам даже не нужно знать, что находится в запросе браузера, и анализировать его, только получить адрес целевого сайта из первой строки, первой строке обычно нравится этот GET http://google.com/ HTTP1.1 или CONNECT facebook.com:443 (это для запросов ssl)
С OWIN и WebAPI все стало очень просто. В моем поиске прокси-сервера C# я также наткнулся на этот пост http://blog.kloud.com.au/2013/11/24/do-it-yourself-web-api-proxy/. Это будет дорога, по которой я иду.
Socks4 - очень простой в реализации протокол. Вы прослушиваете начальное соединение, подключаетесь к хосту / порту, запрошенному клиентом, отправляете клиенту код успеха, а затем пересылаете исходящий и входящий потоки через сокеты.
Если вы используете HTTP, вам придется читать и, возможно, устанавливать / удалять некоторые заголовки HTTP, так что это немного больше работы.
Если я правильно помню, SSL будет работать через HTTP и Socks прокси. Для HTTP-прокси вы реализуете глагол CONNECT, который работает так же, как socks4, как описано выше, затем клиент открывает SSL-соединение через прокси-поток tcp.
Вот пример асинхронной реализации 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;
}
}
Браузер подключен к прокси, поэтому данные, которые прокси получает с веб-сервера, просто отправляются через то же соединение, которое браузер инициировал для прокси.
Я также написал простой, но мощный обратный прокси-сервер для asp.net / web api.
Вы можете найти его здесь: https://github.com/SharpTools/SharpReverseProxy
Просто добавьте в свой проект с помощью Nuget, и все готово. Вы даже можете на лету изменить запрос, ответ или отклонить переадресацию из-за ошибки аутентификации.
Взгляните на исходный код, его действительно легко реализовать:)