Skip to content

Commit

Permalink
[feature] add socks5 proto
Browse files Browse the repository at this point in the history
  • Loading branch information
BusyStudent committed Sep 18, 2024
1 parent 65e79bb commit 8bc43a4
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/xmake-test-on-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Install dependencies
run: sudo apt install -y lcov
- name: Configure
run: xmake f -m coverage --use_fmt==n -y
run: xmake f -m coverage --use_fmt=y -y
- name: Build
run: xmake -y
- name: Run Test and collect coverage
Expand Down
7 changes: 7 additions & 0 deletions include/ilias/http/http1.1.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ class Http1Stream final : public HttpStream {
}
}
mHeaderSent = true;
mMethodHead = (method == "HEAD");
ILIAS_TRACE("Http1.1", "Send Request Successfully");
co_return {};
}
Expand Down Expand Up @@ -285,6 +286,11 @@ class Http1Stream final : public HttpStream {
co_return returnError(Error::HttpBadReply);
}

// Check if the method is HEAD
if (mMethodHead) {
mContentEnd = true;
}

// Done
mHeaderReceived = true;
co_return {};
Expand All @@ -294,6 +300,7 @@ class Http1Stream final : public HttpStream {
auto sprintf(std::string &buf, const char *fmt, ...) -> void;

Http1Connection *mCon;
bool mMethodHead = false; //< If the method is HEAD, we should not recv the body
bool mHeaderSent = false;
bool mHeaderReceived = false;
bool mContentEnd = false;
Expand Down
68 changes: 51 additions & 17 deletions include/ilias/http/session.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <ilias/http/cookie.hpp>
#include <ilias/http/reply.hpp>
#include <ilias/net/addrinfo.hpp>
#include <ilias/net/socks5.hpp>
#include <ilias/net/tcp.hpp>
#include <ilias/io/context.hpp>
#include <ilias/ssl.hpp>
Expand Down Expand Up @@ -92,6 +93,12 @@ class HttpSession {
*/
auto setCookieJar(HttpCookieJar *jar) -> void { mCookieJar = jar; }

/**
* @brief Set the Proxy object
*
* @param proxy
*/
auto setProxy(const Url &proxy) -> void { mProxy = proxy; }
private:
/**
* @brief The sendRequest implementation, only do the connection handling
Expand Down Expand Up @@ -185,7 +192,7 @@ inline auto HttpSession::sendRequest(std::string_view method, const HttpRequest
if (location.empty()) {
co_return Unexpected(Error::HttpBadReply);
}
ILIAS_TRACE("Http", "Redirecting to {} ({} of maximum {})", location, idx + 1, maximumRedirects);
ILIAS_INFO("Http", "Redirecting to {} ({} of maximum {})", location, idx + 1, maximumRedirects);
// Do redirect
url = location;
headers = request.headers();
Expand Down Expand Up @@ -313,27 +320,54 @@ inline auto HttpSession::connect(const Url &url, bool &fromPool) -> Task<std::un
}

// No connection found, create a new one
auto addrinfo = co_await AddressInfo::fromHostnameAsync(host.c_str());
if (!addrinfo) {
co_return Unexpected(addrinfo.error());
}
auto endpoints = addrinfo->addresses();
ILIAS_ASSERT(!endpoints.empty());

// Try connect to all addresses
IStreamClient cur;
for (size_t idx = 0; idx < endpoints.size(); ++idx) {
auto &addr = endpoints[idx];
TcpClient client(mCtxt, addr.family());
ILIAS_TRACE("Http", "Trying to connect to {}:{} ({} of {})", addr, port, idx + 1, endpoints.size());
if (auto ret = co_await client.connect(IPEndpoint {addr, port}); !ret && idx == endpoints.size() - 1) {
continue;

if (!mProxy.empty()) {
// Proxy
auto proxyPort = mProxy.port();
if (!proxyPort || (mProxy.scheme() != "socks5" && mProxy.scheme() != "socks5h")) {
ILIAS_ERROR("Http", "Invalid proxy: {}", mProxy);
co_return Unexpected(Error::HttpBadRequest);
}
auto endpoint = IPEndpoint(std::string(mProxy.host()).c_str(), *proxyPort);
if (!endpoint.isValid()) {
ILIAS_ERROR("Http", "Invalid proxy: {}", mProxy);
co_return Unexpected(Error::HttpBadRequest);
}
else if (!ret) {
ILIAS_TRACE("Http", "Connecting to the {}:{} by proxy: {}", host, port, mProxy);
TcpClient client(mCtxt, endpoint.family());
if (auto ret = co_await client.connect(endpoint); !ret) {
co_return Unexpected(ret.error());
}
// Do Socks5 handshake
Socks5Connector socks5(client);
if (auto ret = co_await socks5.connect(host, port); !ret) {
co_return Unexpected(ret.error());
}
cur = std::move(client);
break;
}
else { //< No proxy
auto addrinfo = co_await AddressInfo::fromHostnameAsync(host.c_str());
if (!addrinfo) {
co_return Unexpected(addrinfo.error());
}
auto endpoints = addrinfo->addresses();
ILIAS_ASSERT(!endpoints.empty());

// Try connect to all addresses
for (size_t idx = 0; idx < endpoints.size(); ++idx) {
auto &addr = endpoints[idx];
TcpClient client(mCtxt, addr.family());
ILIAS_TRACE("Http", "Trying to connect to {} ({} of {})", IPEndpoint(addr, port), idx + 1, endpoints.size());
if (auto ret = co_await client.connect(IPEndpoint {addr, port}); !ret && idx == endpoints.size() - 1) {
continue;
}
else if (!ret) {
co_return Unexpected(ret.error());
}
cur = std::move(client);
break;
}
}

// Try adding a ssl if needed
Expand Down
11 changes: 11 additions & 0 deletions include/ilias/net/address.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,17 @@ class IPAddress6 : public ::in6_addr {
return ::in6_addr IN6ADDR_LOOPBACK_INIT;
}

/**
* @brief Copy data from buffer to create ipv6 address
*
* @param mem pointer to network-format ipv6 address
* @param n must be sizeof(::in6_addr)
* @return IPAddress6
*/
static auto fromRaw(const void *mem, size_t n) -> IPAddress6 {
return *reinterpret_cast<const ::in6_addr *>(mem);
}

/**
* @brief Parse the ipv6 address from string
*
Expand Down
235 changes: 235 additions & 0 deletions include/ilias/net/socks5.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/**
* @file socks5.hpp
* @author BusyStudent (fyw90mc@gmail.com)
* @brief The socks5 protocol implementation.
* @version 0.1
* @date 2024-09-17
*
* @copyright Copyright (c) 2024
*
*/
#pragma once

#include <ilias/net/endpoint.hpp>
#include <ilias/task/task.hpp>
#include <ilias/io/traits.hpp>
#include <ilias/buffer.hpp>
#include <ilias/error.hpp>
#include <ilias/log.hpp>
#include <string>

ILIAS_NS_BEGIN

namespace detail {

struct Socks5Header {
uint8_t ver;
uint8_t nmethods;
uint8_t methods[0];
};

static_assert(sizeof(Socks5Header) == 2);

} // namespace detail

/**
* @brief The Connector for the socks5 protocol. do the handshake and authentication.
*
* @tparam T
*/
template <Stream T>
class Socks5Connector {
public:
/**
* @brief Construct a new Socks 5 Connector object
*
* @param stream The stream to send / recv the socks 5 handshake and authentication. we doesnot take the ownership of the stream.
* @param user
* @param password
*/
Socks5Connector(T &stream, std::string_view user = {}, std::string_view password = {}) :
mStream(stream), mUser(user), mPassword(password)
{

}

/**
* @brief Do the handshake and authentication.
*
* @return Task<void>
*/
auto handshake() -> Task<void> {
uint8_t buf[256];
auto header = reinterpret_cast<detail::Socks5Header *>(buf);
header->ver = 0x05;

// Check if we has the password and user.
if (mUser.empty() && mPassword.empty()) {
header->nmethods = 1;
header->methods[0] = 0x00;
}
else {
header->nmethods = 2;
header->methods[0] = 0x02;
header->methods[1] = 0x00;
}
ILIAS_TRACE("Socks5", "Begin handshake, user: {}, password: {}", mUser, mPassword);
auto n = co_await mStream.write(makeBuffer(buf, sizeof(detail::Socks5Header) + header->nmethods));
if (!n || *n != sizeof(detail::Socks5Header) + header->nmethods) {
co_return Unexpected(n.error_or(Error::Socks5Unknown));
}

// Try recv the selected method
// Version uint8_t
// Method uint8_t
n = co_await mStream.read(makeBuffer(buf, 2));
if (!n || *n != 2 || buf[0] != 0x05 || buf[1] == 0xFF) {
// NetworkError | Unknown method | No acceptable method
co_return Unexpected(n.error_or(Error::Socks5Unknown));
}

if (buf[1] == 0x02) {
// User and password authentication
// TODO: Implement the authentication
co_return Unexpected(Error::Socks5AuthenticationFailed);
}

if (buf[1] != 0x00) {
// Unknown method
co_return Unexpected(Error::Socks5Unknown);
}

ILIAS_TRACE("Socks5", "Handshake done");
mHandshakeDone = true;
co_return {};
}

/**
* @brief Connect to the endpoint. (IPV4 or IPV6 address)
*
* @param endpoint
* @return Task<void>
*/
auto connect(const IPEndpoint &endpoint) -> Task<void> {
auto addr = endpoint.address();
co_return co_await connectImpl(
addr.family() == AF_INET ? 0x01 : 0x04,
addr.span(),
endpoint.port()
);
}

/**
* @brief Connect to the host and port. (Domain name)
*
* @param host
* @param port
* @return Task<void>
*/
auto connect(std::string_view host, uint16_t port) -> Task<void> {
// Buf first byte is the length of the host
std::string buf;
buf.reserve(host.size() + 1);
buf.push_back(host.size());
buf.append(host.begin(), host.end());
co_return co_await connectImpl(0x03, makeBuffer(buf), port);
}
private:
auto connectImpl(uint8_t type, std::span<const std::byte> addr, uint16_t port) -> Task<void> {
if (!mHandshakeDone) {
if (auto ret = co_await handshake(); !ret) {
co_return Unexpected(ret.error());
}
}

ILIAS_TRACE("Socks5", "Connecting to type: {} addrlen({}):{}", type, addr.size(), port);
port = ::htons(port);

// Version uint8_t
// Command uint8_t
// Reserved uint8_t
// AddressType uint8_t
// Address uint8_t[n]
// Port uint16_t
auto bufSize = 4 + addr.size() + sizeof(uint16_t);
auto buf = std::make_unique<uint8_t[]>(bufSize);
buf[0] = 0x05;
buf[1] = 0x01;
buf[2] = 0x00;
buf[3] = type;
::memcpy(buf.get() + 4, addr.data(), addr.size());
::memcpy(buf.get() + 4 + addr.size(), &port, sizeof(port));

auto n = co_await mStream.write(makeBuffer(buf.get(), bufSize));
if (!n || *n != bufSize) {
co_return Unexpected(n.error_or(Error::Socks5Unknown));
}

// Version uint8_t
// Reply uint8_t
// Reserved uint8_t
// AddressType uint8_t
// Address uint8_t[n]
// Port uint16_t
auto recvBufSize = 4 + 16 + sizeof(uint16_t); //< Max size of IPV6 address
if (bufSize < recvBufSize) {
bufSize = recvBufSize;
buf = std::make_unique<uint8_t[]>(bufSize);
}
n = co_await mStream.read(makeBuffer(buf.get(), bufSize));
if (!n || *n < 4 + 2 + sizeof(uint16_t)) {
// NetworkError | Unknown reply (less than minimum size)
co_return Unexpected(n.error_or(Error::Socks5Unknown));
}
if (buf[0] != 0x05 || buf[2] != 0x00) {
// Bad Reply
co_return Unexpected(Error::Socks5Unknown);
}
switch (buf[2]) { // The Reply field contains the result of the request.
case 0x00: break;
default: co_return Unexpected(Error::Socks5Unknown); //< TODO: Add more error codes
}
switch (buf[3]) {
case 0x01: { // IPV4
if (*n < 4 + 4 + sizeof(uint16_t)) {
co_return Unexpected(Error::Socks5Unknown);
}
auto port = ::ntohs(*reinterpret_cast<const uint16_t *>(buf.get() + 4 + 4));
auto addr = IPAddress4::fromRaw(buf.get() + 4, 4);
mServerBound = IPEndpoint(addr, port);
ILIAS_TRACE("Socks5", "Server bound to {}", mServerBound);
break;
}
case 0x04: { // IPV6
if (*n < 4 + 16 + sizeof(uint16_t)) {
co_return Unexpected(Error::Socks5Unknown);
}
auto port = ::ntohs(*reinterpret_cast<const uint16_t *>(buf.get() + 4 + 16));
auto addr = IPAddress6::fromRaw(buf.get() + 4, 16);
mServerBound = IPEndpoint(addr, port);
ILIAS_TRACE("Socks5", "Server bound to {}", mServerBound);
break;
}
case 0x03: { // Domain name
auto len = buf[4];
if (*n < 4 + 1 + len + sizeof(uint16_t)) {
co_return Unexpected(Error::Socks5Unknown);
}
break;
}
default: {
co_return Unexpected(Error::Socks5Unknown);
}
}
ILIAS_TRACE("Socks5", "Connect done");
co_return {};
}

T &mStream;
std::string mUser;
std::string mPassword;
bool mHandshakeDone = false;
IPEndpoint mServerBound;
};

ILIAS_NS_END
Loading

0 comments on commit 8bc43a4

Please sign in to comment.