使用Asio实现Http转Https代理
· 阅读需 4 分钟
最近我们游戏需要接谷歌SDK, 比较麻烦的是国内无法直接访问谷歌接口,并且需要用https访问。由于moon只支持http client,所以想到用nginx来正向代理并实现http转https,然后使用本机的https代理请求谷歌。但实践后发现nginx代理后的请求并没有使用本机的https代理。首先想到一种解决方案是,手动配置nginx把请求发送到本机的https代理。http、https代理的机制如下:
代理请求结构
通常情况下我发送的http请求头里面的结构是:
GET /path?params HTTP 1.1\r\n
如过使用http代理,需要发送的结构是:
GET http://host:port/path?params HTTP/1.1\r\n
可以发现比正常请求多了http://host:port
但通过尝试,nginx好像无法做到转发代理请求。所以进一步了解了下HTTP代理处理流程,准备自己尝试开发。
HTTPS代理处理流程
- 从
source
socket 按\r\n
读取一行获取 host,port - 根据 host,port connect
destination
socket destination
openssl handshake- 把第一步读取到数据中
http://host:port
去掉,发送给destination
- 直接转发
source
->destination
,destination
->source
的数据
HTTPS代理再次转发代理处理流程
- 从
source
socket 按\r\n
读取一行获取 host,port - 根据 host,port connect
destination
socket - 向
destination
发送CONNECT host:port HTTP/1.1\r\nHost: host:port\r\n\r\n
- 按
\r\n\r\n
读取destination
,获取结果200OK
destination
openssl handshake- 直接转发
source
->destination
,destination
->source
的数据
使用asio封装的cpp20协程很容易实现上面流程:
awaitable<bool> handshake(std::string_view proxy_host, uint16_t proxy_port)
{
asio::streambuf streambuf;
size_t n = co_await asio::async_read_until(source, streambuf, "\r\n", use_awaitable);
if (0 == n)
co_return false;
auto line = std::string_view{ (const char*)streambuf.data().data(), n };
size_t method_end;
if ((method_end = line.find(' ')) == std::string_view::npos)
{
co_return false;
}
auto method = line.substr(0, method_end);
size_t query_start = std::string_view::npos;
size_t path_and_query_string_end = std::string_view::npos;
for (size_t i = method_end + 1; i < line.size(); ++i)
{
if (line[i] == '?' && (i + 1) < line.size())
{
query_start = i + 1;
}
else if (line[i] == ' ')
{
path_and_query_string_end = i;
break;
}
}
std::string_view scheme;
std::string_view host;
uint16_t port;
std::string_view path;
parse_url(line.substr(method_end + 1, path_and_query_string_end - method_end - 1), scheme, host, port, path);
asio::ip::tcp::resolver resolver(parent->io_context());
if (!proxy_host.empty())
{
auto endpoints = co_await resolver.async_resolve(proxy_host, std::to_string(proxy_port), use_awaitable);
co_await asio::async_connect(destination.lowest_layer(), endpoints, use_awaitable);
std::string host_port_string;
host_port_string.append(host);
host_port_string.append(":");
host_port_string.append(std::to_string(port));
std::string data;
data.append("CONNECT ");
data.append(host_port_string);
data.append(" HTTP/1.1\r\n");
data.append("Host: ");
data.append(host_port_string);
data.append("\r\n\r\n");
co_await asio::async_write(destination.next_layer(), asio::buffer(data.data(), data.size()), use_awaitable);
asio::streambuf readbuf;
co_await asio::async_read_until(destination.next_layer(), readbuf, "\r\n\r\n", use_awaitable);
if (0 == n)
co_return false;
LOG_INFO("recv proxy response: %s", std::string{ (const char*)readbuf.data().data(), readbuf.size() }.data());
}
else
{
auto endpoints = co_await resolver.async_resolve(host, std::to_string(port), use_awaitable);
co_await asio::async_connect(destination.lowest_layer(), endpoints, use_awaitable);
}
SSL_set_tlsext_host_name(destination.native_handle(), std::string{host}.data());
co_await destination.async_handshake(asio::ssl::stream_base::client, use_awaitable);
if (proxy_host.empty())
{
std::string str;
str.append(method);
str.append(1, ' ');
str.append(path);
str.append(1, ' ');
str.append(line.substr(path_and_query_string_end + 1));
size_t num_additional_bytes = streambuf.size() - n;
streambuf.consume(n);
str.append((const char*)streambuf.data().data(), num_additional_bytes);
co_await asio::async_write(destination, asio::buffer(str.data(), str.size()), use_awaitable);
}
else
{
co_await asio::async_write(destination, streambuf, use_awaitable);
}
co_return true;
}
通过测试,满足了我们的需求