mirror of
https://github.com/arvidn/libtorrent.git
synced 2026-07-29 07:14:08 -04:00
548 lines
15 KiB
C++
548 lines
15 KiB
C++
/*
|
|
|
|
Copyright (c) 2015, Arvid Norberg
|
|
All rights reserved.
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
#include "simulator/simulator.hpp"
|
|
#include "http_server.hpp"
|
|
#include "libtorrent/aux_/path.hpp"
|
|
#include "libtorrent/aux_/throw.hpp"
|
|
|
|
#include <boost/system/system_error.hpp>
|
|
|
|
#include <functional>
|
|
#include <cstdio> // for printf
|
|
|
|
using namespace sim::asio;
|
|
using namespace sim::asio::ip;
|
|
using namespace std::placeholders;
|
|
|
|
using boost::system::error_code;
|
|
|
|
namespace sim {
|
|
using namespace aux;
|
|
|
|
namespace {
|
|
char const* find(char const* hay, int const hsize, char const* needle, int const nsize)
|
|
{
|
|
for (int i = 0; i < hsize - nsize + 1; ++i)
|
|
{
|
|
if (memcmp(hay + i, needle, nsize) == 0) return hay + i;
|
|
}
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
std::string trim(std::string s)
|
|
{
|
|
if (s.empty()) return s;
|
|
|
|
int start = 0;
|
|
int end = int(s.size());
|
|
while (strchr(" \r\n\t", s[start]) != NULL && start < end)
|
|
{
|
|
++start;
|
|
}
|
|
|
|
while (strchr(" \r\n\t", s[end - 1]) != NULL && end > start)
|
|
{
|
|
--end;
|
|
}
|
|
return s.substr(start, end - start);
|
|
}
|
|
|
|
std::string lower_case(std::string s)
|
|
{
|
|
std::string ret;
|
|
std::transform(s.begin(), s.end(), std::back_inserter(ret), [](char c) {
|
|
return static_cast<char>(tolower(c));
|
|
});
|
|
return ret;
|
|
}
|
|
|
|
std::string normalize(const std::string& s)
|
|
{
|
|
std::vector<std::string> elements;
|
|
char const* start = s.c_str();
|
|
if (*start == '/') ++start;
|
|
char const* slash = strchr(start, '/');
|
|
while (slash != NULL)
|
|
{
|
|
std::string element(start, slash - start);
|
|
if (element != "..")
|
|
{
|
|
elements.push_back(element);
|
|
}
|
|
else if (!elements.empty())
|
|
{
|
|
elements.erase(elements.end() - 1);
|
|
}
|
|
start = slash + 1;
|
|
slash = strchr(start, '/');
|
|
}
|
|
elements.push_back(start);
|
|
|
|
std::string ret;
|
|
for (auto const& e : elements)
|
|
{
|
|
ret += '/';
|
|
ret += e;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
// TODO: extra_header should be a std::vector
|
|
std::string send_response(
|
|
int code, char const* status_message, int len, char const** extra_header)
|
|
{
|
|
std::string ret = "HTTP/1.1 " + std::to_string(code) + " " + status_message + "\r\n";
|
|
|
|
ret += "content-length: " + std::to_string(len) + "\r\n";
|
|
|
|
if (extra_header)
|
|
{
|
|
ret += extra_header[0];
|
|
ret += extra_header[1];
|
|
ret += extra_header[2];
|
|
ret += extra_header[3];
|
|
}
|
|
ret += "\r\n";
|
|
|
|
return ret;
|
|
}
|
|
|
|
#if TORRENT_USE_SSL
|
|
std::string ssl_fixture_path(std::string const& name)
|
|
{
|
|
// simulation test binaries run with a scratch directory under
|
|
// simulation/ as their working directory, hence the two ".."
|
|
return lt::combine_path(
|
|
"..", lt::combine_path("..", lt::combine_path("test", lt::combine_path("ssl", name))));
|
|
}
|
|
#endif
|
|
|
|
http_server::http_server(io_context& ios,
|
|
unsigned short listen_port,
|
|
http_server_flags_t flags
|
|
#if TORRENT_USE_SSL
|
|
,
|
|
std::string cert_file,
|
|
std::string key_file,
|
|
std::function<void(lt::aux::ssl::context&)> ssl_setup
|
|
#endif
|
|
)
|
|
: m_ios(ios)
|
|
, m_listen_socket(ios)
|
|
, m_bytes_used(0)
|
|
, m_close(false)
|
|
, m_flags(flags)
|
|
{
|
|
#if TORRENT_USE_SSL
|
|
if (m_flags & https)
|
|
{
|
|
m_ssl_ctx = std::make_unique<lt::aux::ssl::context>(lt::aux::ssl::context::tls);
|
|
// called before the certificate and private key are loaded, so a
|
|
// test can install a password callback (needed by some of the
|
|
// test/ssl/ fixtures, e.g. invalid_peer_private_key.pem) or
|
|
// restrict protocol options ahead of time.
|
|
if (ssl_setup)
|
|
ssl_setup(*m_ssl_ctx);
|
|
|
|
// loads a PEM file via the given ssl::context member function
|
|
// (use_certificate_file or use_private_key_file), throwing on
|
|
// failure.
|
|
using loader_fn = void (lt::aux::ssl::context::*)(
|
|
std::string const&, lt::aux::ssl::context::file_format, error_code&);
|
|
auto const load = [this](std::string const& file, loader_fn mem_fn) {
|
|
std::string const path = ssl_fixture_path(file);
|
|
error_code ec;
|
|
(m_ssl_ctx.get()->*mem_fn)(path, lt::aux::ssl::context::pem, ec);
|
|
if (ec)
|
|
{
|
|
lt::aux::throw_ex<boost::system::system_error>(
|
|
ec, "http_server: failed to load " + path);
|
|
}
|
|
};
|
|
load(cert_file, <::aux::ssl::context::use_certificate_file);
|
|
load(key_file, <::aux::ssl::context::use_private_key_file);
|
|
}
|
|
#endif
|
|
|
|
address local_ip = ios.get_ips().front();
|
|
if (local_ip.is_v4())
|
|
{
|
|
m_listen_socket.open(tcp::v4());
|
|
m_listen_socket.bind(tcp::endpoint(address_v4::any(), listen_port));
|
|
}
|
|
else
|
|
{
|
|
m_listen_socket.open(tcp::v6());
|
|
m_listen_socket.bind(tcp::endpoint(address_v6::any(), listen_port));
|
|
}
|
|
m_listen_socket.listen();
|
|
|
|
m_listen_socket.async_accept(std::bind(&http_server::on_accept, this, _1, _2));
|
|
}
|
|
|
|
void http_server::on_accept(error_code const& ec, tcp::socket peer)
|
|
{
|
|
if (ec)
|
|
{
|
|
std::printf("http_server::on_accept: (%d) %s\n", ec.value(), ec.message().c_str());
|
|
close_connection();
|
|
return;
|
|
}
|
|
|
|
++m_accepted_connections;
|
|
|
|
error_code e;
|
|
m_ep = peer.remote_endpoint(e);
|
|
if (e)
|
|
{
|
|
std::printf("http_server::on_accept: failed to get remote endpoint (%d) %s\n",
|
|
e.value(),
|
|
e.message().c_str());
|
|
}
|
|
else
|
|
{
|
|
std::printf("http_server accepted connection from: %s : %d\n",
|
|
m_ep.address().to_string().c_str(),
|
|
m_ep.port());
|
|
}
|
|
|
|
#if TORRENT_USE_SSL
|
|
if (m_flags & https)
|
|
{
|
|
m_connection.emplace(
|
|
lt::aux::ssl_stream<lt::tcp::socket>(lt::tcp::socket(std::move(peer)), *m_ssl_ctx));
|
|
std::get<lt::aux::ssl_stream<lt::tcp::socket>>(*m_connection)
|
|
.async_accept_handshake(std::bind(&http_server::on_handshake, this, _1));
|
|
return;
|
|
}
|
|
#endif
|
|
m_connection.emplace(lt::tcp::socket(std::move(peer)));
|
|
read();
|
|
}
|
|
|
|
#if TORRENT_USE_SSL
|
|
void http_server::on_handshake(error_code const& ec)
|
|
{
|
|
if (ec)
|
|
{
|
|
std::printf("http_server::on_handshake: (%d) %s\n", ec.value(), ec.message().c_str());
|
|
close_connection();
|
|
return;
|
|
}
|
|
|
|
read();
|
|
}
|
|
#endif
|
|
|
|
void http_server::register_handler(std::string const& path, handler_t h)
|
|
{
|
|
m_handlers[path] = std::move(h);
|
|
}
|
|
|
|
void http_server::register_content(
|
|
std::string const& path, std::int64_t const size, generator_t gen)
|
|
{
|
|
m_handlers[path] = [gen, size](
|
|
std::string, std::string, std::map<std::string, std::string>& hdr) {
|
|
std::int64_t start = 0;
|
|
std::int64_t end = size;
|
|
|
|
auto it = hdr.find("range");
|
|
bool const range_req = it != hdr.end();
|
|
if (range_req)
|
|
{
|
|
std::string range = it->second;
|
|
// skip "bytes "
|
|
range = range.substr(range.find_first_of('=') + 1);
|
|
start = std::stoll(range.substr(0, range.find('-')));
|
|
end = std::stoll(range.substr(range.find_first_of('-') + 1)) + 1;
|
|
}
|
|
|
|
std::string header = "Content-Range: bytes " + std::to_string(start) + "-"
|
|
+ std::to_string(end - 1) + "/" + std::to_string(end - start) + "\r\n";
|
|
char const* extra_headers[4] = {header.c_str(), "", "", ""};
|
|
|
|
return sim::send_response(range_req ? 206 : 200,
|
|
range_req ? "Partial Content" : "OK",
|
|
int(end - start),
|
|
range_req ? extra_headers : nullptr)
|
|
+ gen(start, end - start);
|
|
};
|
|
}
|
|
|
|
void http_server::register_redirect(std::string const& path, std::string const& target)
|
|
{
|
|
m_handlers[path] = [target](std::string, std::string, std::map<std::string, std::string>&) {
|
|
std::string header = "Location: " + target + "\r\n";
|
|
char const* extra_headers[4] = {header.c_str(), "", "", ""};
|
|
return sim::send_response(301, "Moved Permanently", 0, extra_headers);
|
|
};
|
|
}
|
|
|
|
void http_server::register_stall_handler(std::string const& path)
|
|
{
|
|
m_stall_handlers.insert(path);
|
|
}
|
|
|
|
void http_server::read()
|
|
{
|
|
if (m_bytes_used >= int(m_recv_buffer.size()) / 2)
|
|
{
|
|
m_recv_buffer.resize((std::max)(500, m_bytes_used * 2));
|
|
}
|
|
assert(int(m_recv_buffer.size()) > m_bytes_used);
|
|
m_connection->async_read_some(
|
|
asio::buffer(&m_recv_buffer[m_bytes_used], m_recv_buffer.size() - m_bytes_used),
|
|
std::bind(&http_server::on_read, this, _1, _2));
|
|
}
|
|
|
|
http_request parse_request(char const* start, int len)
|
|
{
|
|
http_request ret;
|
|
|
|
char const* const end_of_request = start + len;
|
|
char const* const space = find(start, len, " ", 1);
|
|
if (space == nullptr)
|
|
{
|
|
std::printf(
|
|
"http_server: failed to parse request:\n%s\n", std::string(start, len).c_str());
|
|
throw std::runtime_error("parse failed");
|
|
}
|
|
|
|
char const* const space2 = find(space + 1, int(len - (space - start + 1)), " ", 1);
|
|
if (space2 == nullptr)
|
|
{
|
|
std::printf(
|
|
"http_server: failed to parse request:\n%s\n", std::string(start, len).c_str());
|
|
throw std::runtime_error("parse failed");
|
|
}
|
|
ret.method.assign(start, space);
|
|
ret.req.assign(space + 1, space2);
|
|
if (ret.method != "CONNECT")
|
|
{
|
|
ret.path.assign(normalize(ret.req.substr(0, ret.req.find_first_of('?'))));
|
|
}
|
|
else
|
|
{
|
|
ret.path.assign(ret.req);
|
|
}
|
|
std::printf(
|
|
"parse_request: %s %s [%s]\n", ret.method.c_str(), ret.path.c_str(), ret.req.c_str());
|
|
|
|
char const* header = find(space2, int(len - (space2 - start)), "\r\n", 2);
|
|
while (header != end_of_request - 4)
|
|
{
|
|
if (header == nullptr)
|
|
{
|
|
std::printf(
|
|
"http_server: failed to parse request:\n%s\n", std::string(start, len).c_str());
|
|
throw std::runtime_error("parse failed");
|
|
}
|
|
char const* const next = find(header + 2, int(len - (header + 2 - start)), "\r\n", 2);
|
|
char const* const value =
|
|
static_cast<char const*>(memchr(header, ':', len - (header - start)));
|
|
if (value == nullptr || next == nullptr || value > next)
|
|
{
|
|
std::printf(
|
|
"http_server: failed to parse request:\n%s\n", std::string(start, len).c_str());
|
|
throw std::runtime_error("parse failed");
|
|
}
|
|
|
|
ret.headers[lower_case(trim(std::string(header, value)))] =
|
|
trim(std::string(value + 1, next));
|
|
|
|
header = next;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
int find_request_len(char const* buf, int const len)
|
|
{
|
|
char const* end_of_request = find(buf, len, "\r\n\r\n", 4);
|
|
if (end_of_request == nullptr) return -1;
|
|
return int(end_of_request - buf + 4);
|
|
}
|
|
|
|
void http_server::on_read(error_code const& ec, size_t bytes_transferred)
|
|
try
|
|
{
|
|
if (ec)
|
|
{
|
|
std::printf("http_server::on_read: (%d) %s\n", ec.value(), ec.message().c_str());
|
|
close_connection();
|
|
return;
|
|
}
|
|
|
|
m_bytes_used += int(bytes_transferred);
|
|
|
|
int const req_len = find_request_len(m_recv_buffer.data(), m_bytes_used);
|
|
if (req_len < 0)
|
|
{
|
|
read();
|
|
return;
|
|
}
|
|
|
|
http_request req = parse_request(m_recv_buffer.data(), req_len);
|
|
|
|
m_recv_buffer.erase(m_recv_buffer.begin(), m_recv_buffer.begin() + req_len);
|
|
m_bytes_used -= req_len;
|
|
|
|
auto it = m_handlers.find(req.path);
|
|
if (it == m_handlers.end())
|
|
{
|
|
if (m_stall_handlers.find(req.path) != m_stall_handlers.end())
|
|
{
|
|
return;
|
|
}
|
|
// no handler found, 404
|
|
m_send_buffer = send_response(404, "Not Found");
|
|
}
|
|
else
|
|
{
|
|
m_send_buffer = it->second(req.method, req.req, req.headers);
|
|
}
|
|
|
|
// decide whether to close the connection after this response, and signal
|
|
// it to the client appropriately.
|
|
bool close;
|
|
if (m_flags & http_1_0)
|
|
{
|
|
// an HTTP/1.0 server closes after every response and does not use the
|
|
// Connection header (that is an HTTP/1.1 mechanism). Downgrade the
|
|
// status line so the client detects this from the protocol version.
|
|
close = true;
|
|
auto const ver = m_send_buffer.find("HTTP/1.1");
|
|
if (ver != std::string::npos) m_send_buffer.replace(ver, 8, "HTTP/1.0");
|
|
}
|
|
else
|
|
{
|
|
// close if the client asked us to, or if this server is not
|
|
// configured for keep-alive. When we do, advertise it with a
|
|
// "Connection: close" response header so the client knows not to
|
|
// reuse the socket (rather than discovering it via a failed write).
|
|
close = lower_case(req.headers["connection"]) == "close" || !(m_flags & keep_alive);
|
|
if (close)
|
|
{
|
|
auto const status_end = m_send_buffer.find("\r\n");
|
|
if (status_end != std::string::npos)
|
|
{
|
|
assert(m_send_buffer.find("Connection:") == std::string::npos);
|
|
m_send_buffer.insert(status_end + 2, "Connection: close\r\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
async_write(*m_connection,
|
|
asio::buffer(m_send_buffer.data(), m_send_buffer.size()),
|
|
std::bind(&http_server::on_write, this, _1, _2, close));
|
|
}
|
|
catch (std::exception& e)
|
|
{
|
|
std::printf("http_server::on_read() failed: %s\n", e.what());
|
|
close_connection();
|
|
}
|
|
|
|
void http_server::on_write(error_code const& ec,
|
|
size_t /* bytes_transferred */
|
|
,
|
|
bool close)
|
|
{
|
|
if (ec)
|
|
{
|
|
std::printf("http_server::on_write: (%d) %s\n", ec.value(), ec.message().c_str());
|
|
close_connection();
|
|
return;
|
|
}
|
|
|
|
if (!close)
|
|
{
|
|
// try to read another request out of the buffer
|
|
post(m_ios, std::bind(&http_server::on_read, this, error_code(), 0));
|
|
}
|
|
else
|
|
{
|
|
close_connection();
|
|
}
|
|
}
|
|
|
|
void http_server::stop()
|
|
{
|
|
m_close = true;
|
|
m_listen_socket.close();
|
|
}
|
|
|
|
void http_server::close_connection()
|
|
{
|
|
m_recv_buffer.clear();
|
|
m_bytes_used = 0;
|
|
|
|
#if TORRENT_USE_SSL
|
|
// send a TLS close_notify before tearing down the TCP connection, so
|
|
// the client sees a clean SSL shutdown rather than an abrupt socket
|
|
// close (which surfaces as a "stream truncated" error instead of the
|
|
// plain EOF a closed plaintext connection produces).
|
|
if (m_connection
|
|
&& std::holds_alternative<lt::aux::ssl_stream<lt::tcp::socket>>(*m_connection))
|
|
{
|
|
std::get<lt::aux::ssl_stream<lt::tcp::socket>>(*m_connection)
|
|
.async_shutdown(std::bind(&http_server::finish_close, this, _1));
|
|
return;
|
|
}
|
|
#endif
|
|
finish_close();
|
|
}
|
|
|
|
void http_server::finish_close(error_code const& shutdown_ec)
|
|
{
|
|
if (shutdown_ec)
|
|
{
|
|
std::printf("http_server::close: TLS shutdown failed (%d) %s\n",
|
|
shutdown_ec.value(),
|
|
shutdown_ec.message().c_str());
|
|
}
|
|
|
|
// on_accept() may call close_connection() before a connection was
|
|
// ever accepted (e.g. the accept itself failed), in which case
|
|
// m_connection is still unset.
|
|
if (m_connection)
|
|
{
|
|
error_code err;
|
|
m_connection->close(err);
|
|
// clear the (now-closed) connection so a subsequent close_connection()
|
|
// call (e.g. from a spurious accept error while idle) doesn't re-run
|
|
// TLS shutdown logic against a stale, already-torn-down stream.
|
|
m_connection.reset();
|
|
if (err)
|
|
{
|
|
std::printf("http_server::close: failed to close connection (%d) %s\n",
|
|
err.value(),
|
|
err.message().c_str());
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (m_close) return;
|
|
|
|
// now we can accept another connection
|
|
m_listen_socket.async_accept(std::bind(&http_server::on_accept, this, _1, _2));
|
|
}
|
|
}
|