diff --git a/Config.cpp b/Config.cpp index d7cef879..44dec286 100644 --- a/Config.cpp +++ b/Config.cpp @@ -141,6 +141,9 @@ namespace config { ("http.enabled", value()->default_value(true), "Enable or disable webconsole") ("http.address", value()->default_value("127.0.0.1"), "Webconsole listen address") ("http.port", value()->default_value(7070), "Webconsole listen port") + ("http.auth", value()->default_value(false), "Enable Basic HTTP auth for webconsole") + ("http.user", value()->default_value("i2pd"), "Username for basic auth") + ("http.pass", value()->default_value(""), "Password for basic auth (default: random, see logs)") ; options_description httpproxy("HTTP Proxy options"); diff --git a/Daemon.cpp b/Daemon.cpp index 81bbcdd5..c98bce05 100644 --- a/Daemon.cpp +++ b/Daemon.cpp @@ -13,6 +13,7 @@ #include "RouterInfo.h" #include "RouterContext.h" #include "Tunnel.h" +#include "HTTP.h" #include "NetDb.h" #include "Garlic.h" #include "Streaming.h" @@ -36,7 +37,7 @@ namespace i2p Daemon_Singleton_Private() {}; ~Daemon_Singleton_Private() {}; - std::unique_ptr httpServer; + std::unique_ptr httpServer; std::unique_ptr m_I2PControlService; #ifdef USE_UPNP @@ -202,7 +203,7 @@ namespace i2p std::string httpAddr; i2p::config::GetOption("http.address", httpAddr); uint16_t httpPort; i2p::config::GetOption("http.port", httpPort); LogPrint(eLogInfo, "Daemon: starting HTTP Server at ", httpAddr, ":", httpPort); - d.httpServer = std::unique_ptr(new i2p::util::HTTPServer(httpAddr, httpPort)); + d.httpServer = std::unique_ptr(new i2p::http::HTTPServer(httpAddr, httpPort)); d.httpServer->Start(); } diff --git a/HTTP.cpp b/HTTP.cpp new file mode 100644 index 00000000..74acfb7a --- /dev/null +++ b/HTTP.cpp @@ -0,0 +1,389 @@ +/* +* Copyright (c) 2013-2016, The PurpleI2P Project +* +* This file is part of Purple i2pd project and licensed under BSD3 +* +* See full license text in LICENSE file at top of project tree +*/ + +#include "HTTP.h" + +namespace i2p { +namespace http { + const char *HTTP_METHODS[] = { + "GET", "HEAD", "POST", "PUT", "PATCH", + "DELETE", "OPTIONS", "CONNECT", + NULL + }; + const char *HTTP_VERSIONS[] = { + "HTTP/1.0", "HTTP/1.1", NULL + }; + + bool in_cstr_array(const char **haystack, const char *needle) { + for (const char *p = haystack[0]; p != NULL; p++) { + if (strcmp(p, needle) == 0) + return true; + } + return false; + } + + void strsplit(const std::string & line, std::vector &tokens, char delim, std::size_t limit = 0) { + std::size_t count = 0; + std::stringstream ss(line); + std::string token; + while (1) { + count++; + if (limit > 0 && count >= limit) + delim = '\n'; /* reset delimiter */ + if (!std::getline(ss, token, delim)) + break; + tokens.push_back(token); + } + } + + bool parse_header_line(const std::string & line, std::map & headers) { + std::size_t pos = 0; + std::size_t len = 2; /* strlen(": ") */ + if ((pos = line.find(": ", pos)) == std::string::npos) + return false; + while (isspace(line.at(pos + len))) + len++; + std::string name = line.substr(0, pos); + std::string value = line.substr(pos + len); + headers[name] = value; + return true; + } + + bool URL::parse(const char *str, std::size_t len) { + std::string url(str, len ? len : strlen(str)); + return parse(url); + } + + bool URL::parse(const std::string& url) { + std::size_t pos_p = 0; /* < current parse position */ + std::size_t pos_c = 0; /* < work position */ + if (url.at(0) != '/') { + /* schema */ + pos_c = url.find("://"); + if (pos_c != std::string::npos) { + schema = url.substr(0, pos_c); + pos_p = pos_c + 3; + } + /* user[:pass] */ + pos_c = url.find('@', pos_p); + if (pos_c != std::string::npos) { + std::size_t delim = url.find(':', pos_p); + if (delim != std::string::npos && delim < pos_c) { + user = url.substr(pos_p, delim - pos_p); + delim += 1; + pass = url.substr(delim, pos_c - delim); + } else { + user = url.substr(pos_p, pos_c - pos_p); + } + pos_p = pos_c + 1; + } + /* hostname[:port][/path] */ + pos_c = url.find_first_of(":/", pos_p); + if (pos_c == std::string::npos) { + /* only hostname, without post and path */ + host = url.substr(pos_p, std::string::npos); + return true; + } else if (url.at(pos_c) == ':') { + host = url.substr(pos_p, pos_c - pos_p); + /* port[/path] */ + pos_p = pos_c + 1; + pos_c = url.find('/', pos_p); + std::string port_str = (pos_c == std::string::npos) + ? url.substr(pos_p, std::string::npos) + : url.substr(pos_p, pos_c - pos_p); + /* stoi throws exception on failure, we don't need it */ + for (char c : port_str) { + if (c < '0' || c > '9') + return false; + port *= 10; + port += c - '0'; + } + if (pos_c == std::string::npos) + return true; /* no path part */ + pos_p = pos_c; + } else { + /* start of path part found */ + host = url.substr(pos_p, pos_c - pos_p); + pos_p = pos_c; + } + } + + /* pos_p now at start of path part */ + pos_c = url.find_first_of("?#", pos_p); + if (pos_c == std::string::npos) { + /* only path, without fragment and query */ + path = url.substr(pos_p, std::string::npos); + return true; + } else if (url.at(pos_c) == '?') { + /* found query part */ + path = url.substr(pos_p, pos_c - pos_p); + pos_p = pos_c + 1; + pos_c = url.find('#', pos_p); + if (pos_c == std::string::npos) { + /* no fragment */ + query = url.substr(pos_p, std::string::npos); + return true; + } else { + query = url.substr(pos_p, pos_c - pos_p); + pos_p = pos_c + 1; + } + } else { + /* found fragment part */ + path = url.substr(pos_p, pos_c - pos_p); + pos_p = pos_c + 1; + } + + /* pos_p now at start of fragment part */ + frag = url.substr(pos_p, std::string::npos); + return true; + } + + bool URL::parse_query(std::map & params) { + std::vector tokens; + strsplit(query, tokens, '&'); + + params.clear(); + for (auto it : tokens) { + std::size_t eq = it.find ('='); + if (eq != std::string::npos) { + auto e = std::pair(it.substr(0, eq), it.substr(eq + 1)); + params.insert(e); + } else { + auto e = std::pair(it, ""); + params.insert(e); + } + } + return true; + } + + std::string URL::to_string() { + std::string out = ""; + if (schema != "") { + out = schema + "://"; + if (user != "" && pass != "") { + out += user + ":" + pass + "@"; + } else if (user != "") { + out += user + "@"; + } + if (port) { + out += host + ":" + std::to_string(port); + } else { + out += host; + } + } + out += path; + if (query != "") + out += "?" + query; + if (frag != "") + out += "#" + frag; + return out; + } + + int HTTPReq::parse(const char *buf, size_t len) { + std::string str(buf, len); + return parse(str); + } + + int HTTPReq::parse(const std::string& str) { + enum { REQ_LINE, HEADER_LINE } expect = REQ_LINE; + std::size_t eoh = str.find(HTTP_EOH); /* request head size */ + std::size_t eol = 0, pos = 0; + URL url; + + if (eoh == std::string::npos) + return 0; /* str not contains complete request */ + + while ((eol = str.find(CRLF, pos)) != std::string::npos) { + if (expect == REQ_LINE) { + std::string line = str.substr(pos, eol - pos); + std::vector tokens; + strsplit(line, tokens, ' '); + if (tokens.size() != 3) + return -1; + if (!in_cstr_array(HTTP_METHODS, tokens[0].c_str())) + return -1; + if (!in_cstr_array(HTTP_VERSIONS, tokens[2].c_str())) + return -1; + if (!url.parse(tokens[1])) + return -1; + /* all ok */ + method = tokens[0]; + uri = tokens[1]; + version = tokens[2]; + expect = HEADER_LINE; + } else { + std::string line = str.substr(pos, eol - pos); + if (!parse_header_line(line, headers)) + return -1; + } + pos = eol + strlen(CRLF); + if (pos >= eoh) + break; + } + auto it = headers.find("Host"); + if (it != headers.end ()) { + host = it->second; + } else if (version == "HTTP/1.1") { + return -1; /* 'Host' header required for HTTP/1.1 */ + } else if (url.host != "") { + host = url.host; + } + return eoh + strlen(HTTP_EOH); + } + + std::string HTTPReq::to_string() { + std::stringstream ss; + ss << method << " " << uri << " " << version << CRLF; + ss << "Host: " << host << CRLF; + for (auto & h : headers) { + ss << h.first << ": " << h.second << CRLF; + } + ss << CRLF; + return ss.str(); + } + + bool HTTPRes::is_chunked() { + auto it = headers.find("Transfer-Encoding"); + if (it == headers.end()) + return false; + if (it->second.find("chunked") == std::string::npos) + return true; + return false; + } + + long int HTTPRes::length() { + unsigned long int length = 0; + auto it = headers.find("Content-Length"); + if (it == headers.end()) + return -1; + errno = 0; + length = std::strtoul(it->second.c_str(), (char **) NULL, 10); + if (errno != 0) + return -1; + return length; + } + + int HTTPRes::parse(const char *buf, size_t len) { + std::string str(buf, len); + return parse(str); + } + + int HTTPRes::parse(const std::string& str) { + enum { RES_LINE, HEADER_LINE } expect = RES_LINE; + std::size_t eoh = str.find(HTTP_EOH); /* request head size */ + std::size_t eol = 0, pos = 0; + + if (eoh == std::string::npos) + return 0; /* str not contains complete request */ + + while ((eol = str.find(CRLF, pos)) != std::string::npos) { + if (expect == RES_LINE) { + std::string line = str.substr(pos, eol - pos); + std::vector tokens; + strsplit(line, tokens, ' ', 3); + if (tokens.size() != 3) + return -1; + if (!in_cstr_array(HTTP_VERSIONS, tokens[0].c_str())) + return -1; + code = atoi(tokens[1].c_str()); + if (code < 100 || code >= 600) + return -1; + /* all ok */ + version = tokens[0]; + status = tokens[2]; + expect = HEADER_LINE; + } else { + std::string line = str.substr(pos, eol - pos); + if (!parse_header_line(line, headers)) + return -1; + } + pos = eol + strlen(CRLF); + if (pos >= eoh) + break; + } + + return eoh + strlen(HTTP_EOH); + } + + std::string HTTPRes::to_string() { + std::stringstream ss; + ss << version << " " << code << " " << status << CRLF; + for (auto & h : headers) { + ss << h.first << ": " << h.second << CRLF; + } + ss << CRLF; + return ss.str(); + } + + const char * HTTPCodeToStatus(int code) { + const char *ptr; + switch (code) { + case 105: ptr = "Name Not Resolved"; break; + /* success */ + case 200: ptr = "OK"; break; + case 206: ptr = "Partial Content"; break; + /* redirect */ + case 301: ptr = "Moved Permanently"; break; + case 302: ptr = "Found"; break; + case 304: ptr = "Not Modified"; break; + case 307: ptr = "Temporary Redirect"; break; + /* client error */ + case 400: ptr = "Bad Request"; break; + case 401: ptr = "Unauthorized"; break; + case 403: ptr = "Forbidden"; break; + case 404: ptr = "Not Found"; break; + case 407: ptr = "Proxy Authentication Required"; break; + case 408: ptr = "Request Timeout"; break; + /* server error */ + case 500: ptr = "Internal Server Error"; break; + case 502: ptr = "Bad Gateway"; break; + case 503: ptr = "Not Implemented"; break; + case 504: ptr = "Gateway Timeout"; break; + default: ptr = "Unknown Status"; break; + } + return ptr; + } + + std::string UrlDecode(const std::string& data, bool allow_null) { + std::string decoded(data); + size_t pos = 0; + while ((pos = decoded.find('%', pos)) != std::string::npos) { + char c = strtol(decoded.substr(pos + 1, 2).c_str(), NULL, 16); + if (c == '\0' && !allow_null) { + pos += 3; + continue; + } + decoded.replace(pos, 3, 1, c); + pos++; + } + return decoded; + } + + bool MergeChunkedResponse (std::istream& in, std::ostream& out) { + std::string hexLen; + long int len; + while (!in.eof ()) { + std::getline (in, hexLen); + errno = 0; + len = strtoul(hexLen.c_str(), (char **) NULL, 16); + if (errno != 0) + return false; /* conversion error */ + if (len == 0) + return true; /* end of stream */ + if (len < 0 || len > 10 * 1024 * 1024) /* < 10Mb */ + return false; /* too large chunk */ + char * buf = new char[len]; + in.read (buf, len); + out.write (buf, len); + delete[] buf; + std::getline (in, hexLen); // read \r\n after chunk + } + return true; + } +} // http +} // i2p diff --git a/HTTP.h b/HTTP.h new file mode 100644 index 00000000..864ad88b --- /dev/null +++ b/HTTP.h @@ -0,0 +1,121 @@ +/* +* Copyright (c) 2013-2016, The PurpleI2P Project +* +* This file is part of Purple i2pd project and licensed under BSD3 +* +* See full license text in LICENSE file at top of project tree +*/ + +#ifndef HTTP_H__ +#define HTTP_H__ + +#include +#include +#include +#include +#include + +namespace i2p { +namespace http { + const char CRLF[] = "\r\n"; /**< HTTP line terminator */ + const char HTTP_EOH[] = "\r\n\r\n"; /**< HTTP end-of-headers mark */ + extern const char *HTTP_METHODS[]; /**< list of valid HTTP methods */ + extern const char *HTTP_VERSIONS[]; /**< list of valid HTTP versions */ + + struct URL { + std::string schema; + std::string user; + std::string pass; + std::string host; + unsigned short int port; + std::string path; + std::string query; + std::string frag; + + URL(): schema(""), user(""), pass(""), host(""), port(0), path(""), query(""), frag("") {}; + + /** + * @brief Tries to parse url from string + * @return true on success, false on invalid url + */ + bool parse (const char *str, size_t len = 0); + bool parse (const std::string& url); + + /** + * @brief Parse query part of url to key/value map + * @note Honestly, this should be implemented with std::multimap + */ + bool parse_query(std::map & params); + + /** + * @brief Serialize URL structure to url + * @note Returns relative url if schema if empty, absolute url otherwise + */ + std::string to_string (); + }; + + struct HTTPReq { + std::map headers; + std::string version; + std::string method; + std::string uri; + std::string host; + + HTTPReq (): version("HTTP/1.0"), method("GET"), uri("/") {}; + + /** + * @brief Tries to parse HTTP request from string + * @return -1 on error, 0 on incomplete query, >0 on success + * @note Positive return value is a size of header + */ + int parse(const char *buf, size_t len); + int parse(const std::string& buf); + + /** @brief Serialize HTTP request to string */ + std::string to_string(); + }; + + struct HTTPRes { + std::map headers; + std::string version; + std::string status; + unsigned short int code; + + HTTPRes (): version("HTTP/1.1"), status("OK"), code(200) {} + + /** + * @brief Tries to parse HTTP response from string + * @return -1 on error, 0 on incomplete query, >0 on success + * @note Positive return value is a size of header + */ + int parse(const char *buf, size_t len); + int parse(const std::string& buf); + + /** @brief Serialize HTTP response to string */ + std::string to_string(); + + /** @brief Checks that response declared as chunked data */ + bool is_chunked(); + + /** @brief Returns declared response length or -1 if unknown */ + long int length(); + }; + + /** + * @brief returns HTTP status string by integer code + * @param code HTTP code [100, 599] + * @return Immutable string with status + */ + const char * HTTPCodeToStatus(int code); + + /** + * @brief Replaces %-encoded characters in string with their values + * @param data Source string + * @param null If set to true - decode also %00 sequence, otherwise - skip + * @return Decoded string + */ + std::string UrlDecode(const std::string& data, bool null = false); +} // http +} // i2p + +#endif /* HTTP_H__ */ diff --git a/HTTPServer.cpp b/HTTPServer.cpp index 458a717f..7ad8279d 100644 --- a/HTTPServer.cpp +++ b/HTTPServer.cpp @@ -1,29 +1,34 @@ #include #include +#include +#include +#include + +#include #include -#include + #include "Base.h" #include "FS.h" #include "Log.h" +#include "Config.h" #include "Tunnel.h" #include "TransitTunnel.h" #include "Transports.h" #include "NetDb.h" -#include "I2PEndian.h" -#include "Streaming.h" +#include "HTTP.h" +#include "LeaseSet.h" #include "Destination.h" #include "RouterContext.h" #include "ClientContext.h" #include "HTTPServer.h" +#include "Daemon.h" // For image and info #include "version.h" -namespace i2p -{ -namespace util -{ - const std::string HTTPConnection::itoopieImage = +namespace i2p { +namespace http { + const char *itoopieImage = "\"ICToopie"; - const std::string HTTPConnection::itoopieFavicon = + const char *itoopieFavicon = "data:image/png;base64," "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv" "8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My4wOGVynO" @@ -188,226 +193,130 @@ namespace util "MYez0Gm9P2iWna0GOcDp8KY2JhAsnbSQS6Ahh9OgrlklINeM40bWhAkBd4SLIEh8cBURLhOeiBIArVA" "U4yTRvJItk5PRehQVFaYfpbt9PBtTmdziaXyyUzjaHT/QZBQuKHAA0UxAAAAABJRU5ErkJggg=="; - const char HTTP_COMMAND_TUNNELS[] = "tunnels"; - const char HTTP_COMMAND_TRANSIT_TUNNELS[] = "transit_tunnels"; - const char HTTP_COMMAND_TRANSPORTS[] = "transports"; + const char *cssStyles = + "\r\n"; + + const char HTTP_PAGE_TUNNELS[] = "tunnels"; + const char HTTP_PAGE_TRANSIT_TUNNELS[] = "transit_tunnels"; + const char HTTP_PAGE_TRANSPORTS[] = "transports"; + const char HTTP_PAGE_LOCAL_DESTINATIONS[] = "local_destinations"; + const char HTTP_PAGE_LOCAL_DESTINATION[] = "local_destination"; + const char HTTP_PAGE_SAM_SESSIONS[] = "sam_sessions"; + const char HTTP_PAGE_SAM_SESSION[] = "sam_session"; + const char HTTP_PAGE_I2P_TUNNELS[] = "i2p_tunnels"; + const char HTTP_PAGE_JUMPSERVICES[] = "jumpservices"; + const char HTTP_PAGE_COMMANDS[] = "commands"; const char HTTP_COMMAND_START_ACCEPTING_TUNNELS[] = "start_accepting_tunnels"; const char HTTP_COMMAND_STOP_ACCEPTING_TUNNELS[] = "stop_accepting_tunnels"; + const char HTTP_COMMAND_SHUTDOWN_START[] = "shutdown_start"; + const char HTTP_COMMAND_SHUTDOWN_CANCEL[] = "shutdown_cancel"; + const char HTTP_COMMAND_SHUTDOWN_NOW[] = "terminate"; const char HTTP_COMMAND_RUN_PEER_TEST[] = "run_peer_test"; - const char HTTP_COMMAND_LOCAL_DESTINATIONS[] = "local_destinations"; - const char HTTP_COMMAND_LOCAL_DESTINATION[] = "local_destination"; const char HTTP_PARAM_BASE32_ADDRESS[] = "b32"; - const char HTTP_COMMAND_SAM_SESSIONS[] = "sam_sessions"; - const char HTTP_COMMAND_SAM_SESSION[] = "sam_session"; const char HTTP_PARAM_SAM_SESSION_ID[] = "id"; - const char HTTP_COMMAND_I2P_TUNNELS[] = "i2p_tunnels"; - const char HTTP_COMMAND_JUMPSERVICES[] = "jumpservices="; const char HTTP_PARAM_ADDRESS[] = "address"; - - namespace misc_strings - { - - const char name_value_separator[] = { ':', ' ' }; - const char crlf[] = { '\r', '\n' }; - - } // namespace misc_strings - - std::vector HTTPConnection::reply::to_buffers(int status) - { - std::vector buffers; - if (headers.size () > 0) - { - status_string = "HTTP/1.1 "; - status_string += std::to_string (status); - status_string += " "; - switch (status) - { - case 105: status_string += "Name Not Resolved"; break; - case 200: status_string += "OK"; break; - case 400: status_string += "Bad Request"; break; - case 404: status_string += "Not Found"; break; - case 408: status_string += "Request Timeout"; break; - case 500: status_string += "Internal Server Error"; break; - case 502: status_string += "Bad Gateway"; break; - case 503: status_string += "Not Implemented"; break; - case 504: status_string += "Gateway Timeout"; break; - default: status_string += "WTF"; - } - buffers.push_back(boost::asio::buffer(status_string, status_string.size())); - buffers.push_back(boost::asio::buffer(misc_strings::crlf)); - - for (std::size_t i = 0; i < headers.size(); ++i) - { - header& h = headers[i]; - buffers.push_back(boost::asio::buffer(h.name)); - buffers.push_back(boost::asio::buffer(misc_strings::name_value_separator)); - buffers.push_back(boost::asio::buffer(h.value)); - buffers.push_back(boost::asio::buffer(misc_strings::crlf)); - } - buffers.push_back(boost::asio::buffer(misc_strings::crlf)); - } - buffers.push_back(boost::asio::buffer(content)); - return buffers; - } - void HTTPConnection::Terminate () - { - if (!m_Stream) return; - m_Stream->Close (); - m_Stream = nullptr; - m_Socket->close (); - } + std::map jumpservices = { + { "inr.i2p", "http://joajgazyztfssty4w2on5oaqksz6tqoxbduy553y34mf4byv6gpq.b32.i2p/search/?q=" }, + { "stats.i2p", "http://7tbay5p4kzeekxvyvbf6v7eauazemsnnl2aoyqhg5jzpr5eke7tq.b32.i2p/cgi-bin/jump.cgi?a=" }, + }; - void HTTPConnection::Receive () - { - m_Socket->async_read_some (boost::asio::buffer (m_Buffer, HTTP_CONNECTION_BUFFER_SIZE), - std::bind(&HTTPConnection::HandleReceive, shared_from_this (), - std::placeholders::_1, std::placeholders::_2)); - } + void ShowUptime (std::stringstream& s, int seconds) { + int num; - void HTTPConnection::HandleReceive (const boost::system::error_code& ecode, std::size_t bytes_transferred) - { - if (!ecode) - { - if (!m_Stream) // new request - { - m_Buffer[bytes_transferred] = 0; - m_BufferLen = bytes_transferred; - RunRequest(); - } - else // follow-on - m_Stream->Send ((uint8_t *)m_Buffer, bytes_transferred); - Receive (); + if ((num = seconds / 86400) > 0) { + s << num << " days, "; + seconds -= num; } - else if (ecode != boost::asio::error::operation_aborted) - Terminate (); - } - - void HTTPConnection::RunRequest () - { - auto address = ExtractAddress (); - if (address.length () > 1 && address[1] != '?') // not just '/' or '/?' - { - std::string uri ("/"), b32; - size_t pos = address.find ('/', 1); - if (pos == std::string::npos) - b32 = address.substr (1); // excluding leading '/' to end of line - else - { - b32 = address.substr (1, pos - 1); // excluding leading '/' to next '/' - uri = address.substr (pos); // rest of line - } - - HandleDestinationRequest (b32, uri); + if ((num = seconds / 3600) > 0) { + s << num << " hours, "; + seconds -= num; } - else - HandleRequest (address); + if ((num = seconds / 60) > 0) { + s << num << " min, "; + seconds -= num; + } + s << seconds << " seconds"; } - std::string HTTPConnection::ExtractAddress () + void ShowTunnelDetails (std::stringstream& s, enum i2p::tunnel::TunnelState eState, int bytes) { - char * get = strstr (m_Buffer, "GET"); - if (get) - { - char * http = strstr (get, "HTTP"); - if (http) - return std::string (get + 4, http - get - 5); + std::string state; + switch (eState) { + case i2p::tunnel::eTunnelStateBuildReplyReceived : + case i2p::tunnel::eTunnelStatePending : state = "building"; break; + case i2p::tunnel::eTunnelStateBuildFailed : + case i2p::tunnel::eTunnelStateTestFailed : + case i2p::tunnel::eTunnelStateFailed : state = "failed"; break; + case i2p::tunnel::eTunnelStateExpiring : state = "expiring"; break; + case i2p::tunnel::eTunnelStateEstablished : state = "established"; break; + default: state = "unknown"; break; } - return ""; + s << " " << state << ", "; + s << " " << (int) (bytes / 1024) << " KiB
\r\n"; } - void HTTPConnection::ExtractParams (const std::string& str, std::map& params) - { - if (str[0] != '&') return; - size_t pos = 1, end; - do - { - end = str.find ('&', pos); - std::string param = str.substr (pos, end - pos); - LogPrint (eLogDebug, "HTTPServer: extracted parameters: ", param); - size_t e = param.find ('='); - if (e != std::string::npos) - params[param.substr(0, e)] = param.substr(e+1); - pos = end + 1; - } - while (end != std::string::npos); - } - - void HTTPConnection::HandleWriteReply (const boost::system::error_code& ecode) + void ShowPageHead (std::stringstream& s) { - if (ecode != boost::asio::error::operation_aborted) - { - boost::system::error_code ignored_ec; - m_Socket->shutdown(boost::asio::ip::tcp::socket::shutdown_both, ignored_ec); - Terminate (); - } + s << + "\r\n" + "\r\n" /* TODO: Add support for locale */ + " \r\n" + " \r\n" /* TODO: Find something to parse html/template system. This is horrible. */ + " \r\n" + " Purple I2P " VERSION " Webconsole\r\n" + << cssStyles << + "\r\n"; + s << + "\r\n" + "
i2pd webconsole
\r\n" + "
\r\n" + "
\r\n" + " Main page
\r\n
\r\n" + " Router commands
\r\n" + " Local destinations
\r\n" + " Tunnels
\r\n" + " Transit tunnels
\r\n" + " Transports
\r\n" + " I2P tunnels
\r\n" + " Jump services
\r\n" + " SAM sessions
\r\n" + "
\r\n" + "
"; } - void HTTPConnection::HandleWrite (const boost::system::error_code& ecode) + void ShowPageTail (std::stringstream& s) { - if (ecode || (m_Stream && !m_Stream->IsOpen ())) - { - if (ecode != boost::asio::error::operation_aborted) - Terminate (); - } - else // data keeps coming - AsyncStreamReceive (); + s << + "
\r\n" + "\r\n" + "\r\n"; } - void HTTPConnection::HandleRequest (const std::string& address) + void ShowError(std::stringstream& s, const std::string& string) { - std::stringstream s; - // Html5 head start - s << "\r\n"; // TODO: Add support for locale. - s << "\r\n\r\n"; // TODO: Find something to parse html/template system. This is horrible. - s << "\r\n"; - s << "Purple I2P " << VERSION " Webconsole\r\n"; - s << "\r\n\r\n\r\n"; - s << "
i2pd webconsole
"; - s << "
"; - s << "
\r\n"; - s << "Main page
\r\n
\r\n"; - s << "Local destinations
\r\n"; - s << "Tunnels
\r\n"; - s << "Transit tunnels
\r\n"; - s << "Transports
\r\n
\r\n"; - s << "I2P tunnels
\r\n"; - if (i2p::client::context.GetSAMBridge ()) - s << "SAM sessions
\r\n
\r\n"; - if (i2p::context.AcceptsTunnels ()) - s << "Stop accepting tunnels
\r\n
\r\n"; - else - s << "Start accepting tunnels
\r\n
\r\n"; - s << "Run peer test
\r\n
\r\n"; - s << "Jump services
\r\n
\r\n"; - s << "
"; - if (address.length () > 1) - HandleCommand (address.substr (2), s); - else - FillContent (s); - s << "
\r\n\r\n"; - SendReply (s.str ()); + s << "ERROR: " << string << "
\r\n"; } - void HTTPConnection::FillContent (std::stringstream& s) + void ShowStatus (std::stringstream& s) { - s << "Uptime: " << boost::posix_time::to_simple_string ( - boost::posix_time::time_duration (boost::posix_time::seconds ( - i2p::context.GetUptime ()))) << "
\r\n"; + s << "Uptime: "; + ShowUptime(s, i2p::context.GetUptime ()); + s << "
\r\n"; s << "Status: "; switch (i2p::context.GetStatus ()) { @@ -475,73 +384,32 @@ namespace util s << "Transit Tunnels: " << std::to_string(transitTunnelCount) << "
\r\n"; } - void HTTPConnection::HandleCommand (const std::string& command, std::stringstream& s) + void ShowJumpServices (std::stringstream& s, const std::string& address) { - size_t paramsPos = command.find('&'); - std::string cmd = command.substr (0, paramsPos); - if (cmd == HTTP_COMMAND_TRANSPORTS) - ShowTransports (s); - else if (cmd == HTTP_COMMAND_TUNNELS) - ShowTunnels (s); - else if (cmd == HTTP_COMMAND_JUMPSERVICES) - { - std::map params; - ExtractParams (command.substr (paramsPos), params); - auto address = params[HTTP_PARAM_ADDRESS]; - ShowJumpServices (address, s); - } else if (cmd == HTTP_COMMAND_TRANSIT_TUNNELS) - ShowTransitTunnels (s); - else if (cmd == HTTP_COMMAND_START_ACCEPTING_TUNNELS) - StartAcceptingTunnels (s); - else if (cmd == HTTP_COMMAND_STOP_ACCEPTING_TUNNELS) - StopAcceptingTunnels (s); - else if (cmd == HTTP_COMMAND_RUN_PEER_TEST) - RunPeerTest (s); - else if (cmd == HTTP_COMMAND_LOCAL_DESTINATIONS) - ShowLocalDestinations (s); - else if (cmd == HTTP_COMMAND_LOCAL_DESTINATION) - { - std::map params; - ExtractParams (command.substr (paramsPos), params); - auto b32 = params[HTTP_PARAM_BASE32_ADDRESS]; - ShowLocalDestination (b32, s); - } - else if (cmd == HTTP_COMMAND_SAM_SESSIONS) - ShowSAMSessions (s); - else if (cmd == HTTP_COMMAND_SAM_SESSION) - { - std::map params; - ExtractParams (command.substr (paramsPos), params); - auto id = params[HTTP_PARAM_SAM_SESSION_ID]; - ShowSAMSession (id, s); - } - else if (cmd == HTTP_COMMAND_I2P_TUNNELS) - ShowI2PTunnels (s); - } + s << "
"; + s << ""; + s << ""; + s << ""; + s << "

\r\n"; + s << "Jump services for " << address << "\r\n\r\n"; + } - void HTTPConnection::ShowJumpServices (const std::string& address, std::stringstream& s) - { - s << "
"; - s << ""; - s << "

\r\n"; - s << "Jump services for " << address << ""; - s << ""; - } - - void HTTPConnection::ShowLocalDestinations (std::stringstream& s) + void ShowLocalDestinations (std::stringstream& s) { s << "Local Destinations:
\r\n
\r\n"; for (auto& it: i2p::client::context.GetDestinations ()) { auto ident = it.second->GetIdentHash ();; - s << ""; + s << ""; s << i2p::client::context.GetAddressBook ().ToAddress(ident) << "
\r\n" << std::endl; } } - void HTTPConnection::ShowLocalDestination (const std::string& b32, std::stringstream& s) + void ShowLocalDestination (std::stringstream& s, const std::string& b32) { s << "Local Destination:
\r\n
\r\n"; i2p::data::IdentHash ident; @@ -555,28 +423,19 @@ namespace util auto pool = dest->GetTunnelPool (); if (pool) { - s << "Tunnels:
\r\n"; - for (auto it: pool->GetOutboundTunnels ()) - { - it->Print (s); - auto state = it->GetState (); - if (state == i2p::tunnel::eTunnelStateFailed) - s << " " << "Failed"; - else if (state == i2p::tunnel::eTunnelStateExpiring) - s << " " << "Exp"; - s << "
\r\n" << std::endl; + s << "Inbound tunnels:
\r\n"; + for (auto & it : pool->GetInboundTunnels ()) { + it->Print(s); + ShowTunnelDetails(s, it->GetState (), it->GetNumReceivedBytes ()); } - for (auto it: pool->GetInboundTunnels ()) - { - it->Print (s); - auto state = it->GetState (); - if (state == i2p::tunnel::eTunnelStateFailed) - s << " " << "Failed"; - else if (state == i2p::tunnel::eTunnelStateExpiring) - s << " " << "Exp"; - s << "
\r\n" << std::endl; + s << "
\r\n"; + s << "Outbound tunnels:
\r\n"; + for (auto & it : pool->GetOutboundTunnels ()) { + it->Print(s); + ShowTunnelDetails(s, it->GetState (), it->GetNumSentBytes ()); } } + s << "
\r\n"; s << "Tags
Incoming: " << dest->GetNumIncomingTags () << "
Outgoing:
" << std::endl; for (auto it: dest->GetSessions ()) { @@ -627,36 +486,44 @@ namespace util } } - void HTTPConnection::ShowTunnels (std::stringstream& s) + void ShowTunnels (std::stringstream& s) { - s << "Tunnels:
\r\n
\r\n"; s << "Queue size: " << i2p::tunnel::tunnels.GetQueueSize () << "
\r\n"; - for (auto it: i2p::tunnel::tunnels.GetOutboundTunnels ()) - { - it->Print (s); - auto state = it->GetState (); - if (state == i2p::tunnel::eTunnelStateFailed) - s << " " << "Failed"; - else if (state == i2p::tunnel::eTunnelStateExpiring) - s << " " << "Exp"; - s << " " << (int)it->GetNumSentBytes () << "
\r\n"; - s << std::endl; - } - for (auto it: i2p::tunnel::tunnels.GetInboundTunnels ()) - { - it->Print (s); - auto state = it->GetState (); - if (state == i2p::tunnel::eTunnelStateFailed) - s << " " << "Failed"; - else if (state == i2p::tunnel::eTunnelStateExpiring) - s << " " << "Exp"; - s << " " << (int)it->GetNumReceivedBytes () << "
\r\n"; - s << std::endl; + s << "Inbound tunnels:
\r\n"; + for (auto & it : i2p::tunnel::tunnels.GetInboundTunnels ()) { + it->Print(s); + ShowTunnelDetails(s, it->GetState (), it->GetNumReceivedBytes ()); + } + s << "
\r\n"; + s << "Outbound tunnels:
\r\n"; + for (auto & it : i2p::tunnel::tunnels.GetOutboundTunnels ()) { + it->Print(s); + ShowTunnelDetails(s, it->GetState (), it->GetNumSentBytes ()); } + s << "
\r\n"; } - void HTTPConnection::ShowTransitTunnels (std::stringstream& s) + void ShowCommands (std::stringstream& s) + { + /* commands */ + s << "Router Commands
\r\n"; + s << " Run peer test
\r\n"; + if (i2p::context.AcceptsTunnels ()) + s << " Stop accepting tunnels
\r\n"; + else + s << " Start accepting tunnels
\r\n"; + if (Daemon.gracefullShutdownInterval) { + s << " Cancel gracefull shutdown ("; + s << Daemon.gracefullShutdownInterval; + s << " seconds remains)
\r\n"; + } else { + s << " Start gracefull shutdown
\r\n"; + } + s << " Force shutdown
\r\n"; + } + + void ShowTransitTunnels (std::stringstream& s) { s << "Transit tunnels:
\r\n
\r\n"; for (auto it: i2p::tunnel::tunnels.GetTransitTunnels ()) @@ -671,7 +538,7 @@ namespace util } } - void HTTPConnection::ShowTransports (std::stringstream& s) + void ShowTransports (std::stringstream& s) { s << "Transports:
\r\n
\r\n"; auto ntcpServer = i2p::transport::transports.GetNTCPServer (); @@ -720,66 +587,60 @@ namespace util } } - void HTTPConnection::ShowSAMSessions (std::stringstream& s) + void ShowSAMSessions (std::stringstream& s) { - s << "SAM Sessions:
\r\n
\r\n"; auto sam = i2p::client::context.GetSAMBridge (); - if (sam) - { - for (auto& it: sam->GetSessions ()) - { - s << ""; - s << it.first << "
\r\n" << std::endl; - } + if (!sam) { + ShowError(s, "SAM disabled"); + return; + } + s << "SAM Sessions:
\r\n
\r\n"; + for (auto& it: sam->GetSessions ()) + { + s << ""; + s << it.first << "
\r\n" << std::endl; } } - void HTTPConnection::ShowSAMSession (const std::string& id, std::stringstream& s) + void ShowSAMSession (std::stringstream& s, const std::string& id) { s << "SAM Session:
\r\n
\r\n"; auto sam = i2p::client::context.GetSAMBridge (); - if (sam) + if (!sam) { + ShowError(s, "SAM disabled"); + return; + } + auto session = sam->FindSession (id); + if (!session) { + ShowError(s, "SAM session not found"); + return; + } + auto& ident = session->localDestination->GetIdentHash(); + s << ""; + s << i2p::client::context.GetAddressBook ().ToAddress(ident) << "
\r\n"; + s << "
\r\n"; + s << "Streams:
\r\n"; + for (auto it: session->ListSockets()) { - auto session = sam->FindSession (id); - if (session) + switch (it->GetSocketType ()) { - auto& ident = session->localDestination->GetIdentHash(); - s << ""; - s << i2p::client::context.GetAddressBook ().ToAddress(ident) << "
\r\n" << std::endl; - s << "Streams:
\r\n"; - for (auto it: session->ListSockets()) - { - switch (it->GetSocketType ()) - { - case i2p::client::eSAMSocketTypeSession: - s << "session"; - break; - case i2p::client::eSAMSocketTypeStream: - s << "stream"; - break; - case i2p::client::eSAMSocketTypeAcceptor: - s << "acceptor"; - break; - default: - s << "unknown"; - } - s << " [" << it->GetSocket ().remote_endpoint() << "]"; - s << "
\r\n" << std::endl; - } + case i2p::client::eSAMSocketTypeSession : s << "session"; break; + case i2p::client::eSAMSocketTypeStream : s << "stream"; break; + case i2p::client::eSAMSocketTypeAcceptor : s << "acceptor"; break; + default: s << "unknown"; break; } + s << " [" << it->GetSocket ().remote_endpoint() << "]"; + s << "
\r\n"; } } - void HTTPConnection::ShowI2PTunnels (std::stringstream& s) + void ShowI2PTunnels (std::stringstream& s) { s << "Client Tunnels:
\r\n
\r\n"; for (auto& it: i2p::client::context.GetClientTunnels ()) { auto& ident = it.second->GetLocalDestination ()->GetIdentHash(); - s << ""; + s << ""; s << it.second->GetName () << " ⇐ "; s << i2p::client::context.GetAddressBook ().ToAddress(ident); s << "
\r\n"<< std::endl; @@ -788,143 +649,208 @@ namespace util for (auto& it: i2p::client::context.GetServerTunnels ()) { auto& ident = it.second->GetLocalDestination ()->GetIdentHash(); - s << ""; + s << ""; s << it.second->GetName () << " ⇒ "; s << i2p::client::context.GetAddressBook ().ToAddress(ident); s << ":" << it.second->GetLocalPort (); s << "
\r\n"<< std::endl; } } - - void HTTPConnection::StopAcceptingTunnels (std::stringstream& s) - { - s << "Stop Accepting Tunnels:
\r\n
\r\n"; - i2p::context.SetAcceptsTunnels (false); - s << "Accepting tunnels stopped" << std::endl; - } - void HTTPConnection::StartAcceptingTunnels (std::stringstream& s) + HTTPConnection::HTTPConnection (std::shared_ptr socket): + m_Socket (socket), m_Timer (socket->get_io_service ()), m_BufferLen (0) { - s << "Start Accepting Tunnels:
\r\n
\r\n"; - i2p::context.SetAcceptsTunnels (true); - s << "Accepting tunnels started" << std::endl; - } + /* cache options */ + i2p::config::GetOption("http.auth", needAuth); + i2p::config::GetOption("http.user", user); + i2p::config::GetOption("http.pass", pass); + }; - void HTTPConnection::RunPeerTest (std::stringstream& s) + void HTTPConnection::Receive () { - s << "Run Peer Test:
\r\n
\r\n"; - i2p::transport::transports.PeerTest (); - s << "Peer test is running" << std::endl; + m_Socket->async_read_some (boost::asio::buffer (m_Buffer, HTTP_CONNECTION_BUFFER_SIZE), + std::bind(&HTTPConnection::HandleReceive, shared_from_this (), + std::placeholders::_1, std::placeholders::_2)); } - void HTTPConnection::HandleDestinationRequest (const std::string& address, const std::string& uri) + void HTTPConnection::HandleReceive (const boost::system::error_code& ecode, std::size_t bytes_transferred) { - std::string request = "GET " + uri + " HTTP/1.1\r\nHost:" + address + "\r\n\r\n"; - LogPrint(eLogInfo, "HTTPServer: client request: ", request); - SendToAddress (address, 80, request.c_str (), request.size ()); - } - - void HTTPConnection::SendToAddress (const std::string& address, int port, const char * buf, size_t len) - { - i2p::data::IdentHash destination; - if (!i2p::client::context.GetAddressBook ().GetIdentHash (address, destination)) - { - LogPrint (eLogWarning, "HTTPServer: Unknown address ", address); - SendError ("Unknown address " + address); + if (ecode) { + if (ecode != boost::asio::error::operation_aborted) + Terminate (ecode); return; - } - - auto leaseSet = i2p::client::context.GetSharedLocalDestination ()->FindLeaseSet (destination); - if (leaseSet && !leaseSet->IsExpired ()) - SendToDestination (leaseSet, port, buf, len); - else - { - memcpy (m_Buffer, buf, len); - m_BufferLen = len; - i2p::client::context.GetSharedLocalDestination ()->RequestDestination (destination); - m_Timer.expires_from_now (boost::posix_time::seconds(HTTP_DESTINATION_REQUEST_TIMEOUT)); - m_Timer.async_wait (std::bind (&HTTPConnection::HandleDestinationRequestTimeout, - shared_from_this (), std::placeholders::_1, destination, port, m_Buffer, m_BufferLen)); } + m_Buffer[bytes_transferred] = '\0'; + m_BufferLen = bytes_transferred; + RunRequest(); + Receive (); } - - void HTTPConnection::HandleDestinationRequestTimeout (const boost::system::error_code& ecode, - i2p::data::IdentHash destination, int port, const char * buf, size_t len) - { - if (ecode != boost::asio::error::operation_aborted) - { - auto leaseSet = i2p::client::context.GetSharedLocalDestination ()->FindLeaseSet (destination); - if (leaseSet && !leaseSet->IsExpired ()) { - SendToDestination (leaseSet, port, buf, len); - } else if (leaseSet) { - SendError ("LeaseSet expired"); - } else { - SendError ("LeaseSet not found"); - } - } - } - - void HTTPConnection::SendToDestination (std::shared_ptr remote, int port, const char * buf, size_t len) + + void HTTPConnection::RunRequest () { - if (!m_Stream) - m_Stream = i2p::client::context.GetSharedLocalDestination ()->CreateStream (remote, port); - if (m_Stream) - { - m_Stream->Send ((uint8_t *)buf, len); - AsyncStreamReceive (); + HTTPReq request; + int ret = request.parse(m_Buffer); + if (ret < 0) { + m_Buffer[0] = '\0'; + m_BufferLen = 0; + return; /* error */ } + if (ret == 0) + return; /* need more data */ + + HandleRequest (request); } - void HTTPConnection::AsyncStreamReceive () + void HTTPConnection::Terminate (const boost::system::error_code& ecode) { - if (m_Stream) - m_Stream->AsyncReceive (boost::asio::buffer (m_StreamBuffer, 8192), - std::bind (&HTTPConnection::HandleStreamReceive, shared_from_this (), - std::placeholders::_1, std::placeholders::_2), - 45); // 45 seconds timeout + if (ecode == boost::asio::error::operation_aborted) + return; + boost::system::error_code ignored_ec; + m_Socket->shutdown(boost::asio::ip::tcp::socket::shutdown_both, ignored_ec); + m_Socket->close (); } - void HTTPConnection::HandleStreamReceive (const boost::system::error_code& ecode, std::size_t bytes_transferred) - { - if (!ecode) - { - boost::asio::async_write (*m_Socket, boost::asio::buffer (m_StreamBuffer, bytes_transferred), - std::bind (&HTTPConnection::HandleWrite, shared_from_this (), std::placeholders::_1)); + bool HTTPConnection::CheckAuth (const HTTPReq & req) { + /* method #1: http://user:pass@127.0.0.1:7070/ */ + if (req.uri.find('@') != std::string::npos) { + URL url; + if (url.parse(req.uri) && url.user == user && url.pass == pass) + return true; } - else - { - if (ecode == boost::asio::error::timed_out) - SendError ("Host not responding"); - else if (ecode != boost::asio::error::operation_aborted) - Terminate (); + /* method #2: 'Authorization' header sent */ + if (req.headers.count("Authorization") > 0) { + std::string provided = req.headers.find("Authorization")->second; + std::string expected = user + ":" + pass; + char b64_creds[64]; + std::size_t len = 0; + len = i2p::data::ByteStreamToBase64((unsigned char *)expected.c_str(), expected.length(), b64_creds, sizeof(b64_creds)); + b64_creds[len] = '\0'; + expected = "Basic "; + expected += b64_creds; + if (provided == expected) + return true; } + + LogPrint(eLogWarning, "HTTPServer: auth failure from ", m_Socket->remote_endpoint().address ()); + return false; } - void HTTPConnection::SendReply (const std::string& content, int status) + void HTTPConnection::HandleRequest (const HTTPReq & req) { - m_Reply.content = content; - m_Reply.headers.resize(3); - // we need the date header to be complaint with http 1.1 - std::time_t time_now = std::time(nullptr); - char time_buff[128]; - if (std::strftime(time_buff, sizeof(time_buff), "%a, %d %b %Y %H:%M:%S GMT", std::gmtime(&time_now))) - { - m_Reply.headers[0].name = "Date"; - m_Reply.headers[0].value = std::string(time_buff); - m_Reply.headers[1].name = "Content-Length"; - m_Reply.headers[1].value = std::to_string(m_Reply.content.size()); - m_Reply.headers[2].name = "Content-Type"; - m_Reply.headers[2].value = "text/html"; - } - - boost::asio::async_write (*m_Socket, m_Reply.to_buffers(status), - std::bind (&HTTPConnection::HandleWriteReply, shared_from_this (), std::placeholders::_1)); + std::stringstream s; + std::string content; + HTTPRes res; + + LogPrint(eLogDebug, "HTTPServer: request: ", req.uri); + + if (needAuth && !CheckAuth(req)) { + res.code = 401; + res.headers.insert(std::pair("WWW-Authenticate", "Basic realm=\"WebAdmin\"")); + SendReply(res, content); + return; + } + + // Html5 head start + ShowPageHead (s); + if (req.uri.find("page=") != std::string::npos) + HandlePage (req, res, s); + else if (req.uri.find("cmd=") != std::string::npos) + HandleCommand (req, res, s); + else + ShowStatus (s); + ShowPageTail (s); + content = s.str (); + SendReply (res, content); } - void HTTPConnection::SendError(const std::string& content) + void HTTPConnection::HandlePage (const HTTPReq& req, HTTPRes& res, std::stringstream& s) + { + std::map params; + std::string page(""); + URL url; + + url.parse(req.uri); + url.parse_query(params); + page = params["page"]; + + if (page == HTTP_PAGE_TRANSPORTS) + ShowTransports (s); + else if (page == HTTP_PAGE_TUNNELS) + ShowTunnels (s); + else if (page == HTTP_PAGE_COMMANDS) + ShowCommands (s); + else if (page == HTTP_PAGE_JUMPSERVICES) + ShowJumpServices (s, params["address"]); + else if (page == HTTP_PAGE_TRANSIT_TUNNELS) + ShowTransitTunnels (s); + else if (page == HTTP_PAGE_LOCAL_DESTINATIONS) + ShowLocalDestinations (s); + else if (page == HTTP_PAGE_LOCAL_DESTINATION) + ShowLocalDestination (s, params["b32"]); + else if (page == HTTP_PAGE_SAM_SESSIONS) + ShowSAMSessions (s); + else if (page == HTTP_PAGE_SAM_SESSION) + ShowSAMSession (s, params["sam_id"]); + else if (page == HTTP_PAGE_I2P_TUNNELS) + ShowI2PTunnels (s); + else { + res.code = 400; + ShowError(s, "Unknown page: " + page); + return; + } + } + + void HTTPConnection::HandleCommand (const HTTPReq& req, HTTPRes& res, std::stringstream& s) + { + std::map params; + std::string cmd(""); + URL url; + + url.parse(req.uri); + url.parse_query(params); + cmd = params["cmd"]; + + if (cmd == HTTP_COMMAND_RUN_PEER_TEST) + i2p::transport::transports.PeerTest (); + else if (cmd == HTTP_COMMAND_START_ACCEPTING_TUNNELS) + i2p::context.SetAcceptsTunnels (true); + else if (cmd == HTTP_COMMAND_STOP_ACCEPTING_TUNNELS) + i2p::context.SetAcceptsTunnels (false); + else if (cmd == HTTP_COMMAND_SHUTDOWN_START) { + i2p::context.SetAcceptsTunnels (false); + Daemon.gracefullShutdownInterval = 10*60; + } else if (cmd == HTTP_COMMAND_SHUTDOWN_CANCEL) { + i2p::context.SetAcceptsTunnels (true); + Daemon.gracefullShutdownInterval = 0; + } else if (cmd == HTTP_COMMAND_SHUTDOWN_NOW) { + Daemon.running = false; + } else { + res.code = 400; + ShowError(s, "Unknown command: " + cmd); + return; + } + s << "SUCCESS: Command accepted

\r\n"; + s << "Back to commands list"; + } + + void HTTPConnection::SendReply (HTTPRes& reply, std::string& content) { - SendReply ("" + itoopieImage + "
\r\n" + content + "", 504); + std::time_t time_now = std::time(nullptr); + char time_buff[128]; + std::strftime(time_buff, sizeof(time_buff), "%a, %d %b %Y %H:%M:%S GMT", std::gmtime(&time_now)); + reply.status = HTTPCodeToStatus(reply.code); + reply.headers.insert(std::pair("Date", time_buff)); + reply.headers.insert(std::pair("Content-Type", "text/html")); + reply.headers.insert(std::pair("Content-Length", std::to_string(content.size()))); + + std::string res = reply.to_string(); + std::vector buffers; + + buffers.push_back(boost::asio::buffer(res)); + buffers.push_back(boost::asio::buffer(content)); + + boost::asio::async_write (*m_Socket, buffers, + std::bind (&HTTPConnection::Terminate, shared_from_this (), std::placeholders::_1)); } HTTPServer::HTTPServer (const std::string& address, int port): @@ -940,6 +866,21 @@ namespace util void HTTPServer::Start () { + bool needAuth; i2p::config::GetOption("http.auth", needAuth); + std::string user; i2p::config::GetOption("http.user", user); + std::string pass; i2p::config::GetOption("http.pass", pass); + /* generate pass if needed */ + if (needAuth && pass == "") { + char alnum[] = "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + pass.resize(16); + for (size_t i = 0; i < pass.size(); i++) { + pass[i] = alnum[rand() % (sizeof(alnum) - 1)]; + } + i2p::config::SetOption("http.pass", pass); + LogPrint(eLogInfo, "HTTPServer: password set to ", pass); + } m_Thread = std::unique_ptr(new std::thread (std::bind (&HTTPServer::Run, this))); m_Acceptor.listen (); Accept (); @@ -949,11 +890,10 @@ namespace util { m_Acceptor.close(); m_Service.stop (); - if (m_Thread) - { - m_Thread->join (); - m_Thread = nullptr; - } + if (m_Thread) { + m_Thread->join (); + m_Thread = nullptr; + } } void HTTPServer::Run () @@ -971,11 +911,10 @@ namespace util void HTTPServer::HandleAccept(const boost::system::error_code& ecode, std::shared_ptr newSocket) { - if (!ecode) - { - CreateConnection(newSocket); - Accept (); - } + if (ecode) + return; + CreateConnection(newSocket); + Accept (); } void HTTPServer::CreateConnection(std::shared_ptr newSocket) @@ -983,5 +922,5 @@ namespace util auto conn = std::make_shared (newSocket); conn->Receive (); } -} -} +} // http +} // i2p diff --git a/HTTPServer.h b/HTTPServer.h index 66083d85..2635c3be 100644 --- a/HTTPServer.h +++ b/HTTPServer.h @@ -1,108 +1,40 @@ #ifndef HTTP_SERVER_H__ #define HTTP_SERVER_H__ -#include -#include -#include -#include -#include -#include "LeaseSet.h" -#include "Streaming.h" - -namespace i2p -{ -namespace util -{ +namespace i2p { +namespace http { + extern const char *itoopieImage; + extern const char *itoopieFavicon; const size_t HTTP_CONNECTION_BUFFER_SIZE = 8192; - const int HTTP_DESTINATION_REQUEST_TIMEOUT = 10; // in seconds + class HTTPConnection: public std::enable_shared_from_this { - protected: - - struct header - { - std::string name; - std::string value; - }; - - struct request - { - std::string method; - std::string uri; - std::string host; - int port; - int http_version_major; - int http_version_minor; - std::vector
headers; - }; - - struct reply - { - std::vector
headers; - std::string status_string, content; - std::vector to_buffers (int status); - }; - public: - HTTPConnection (std::shared_ptr socket): - m_Socket (socket), m_Timer (socket->get_io_service ()), - m_Stream (nullptr), m_BufferLen (0) {}; + HTTPConnection (std::shared_ptr socket); void Receive (); private: - void Terminate (); void HandleReceive (const boost::system::error_code& ecode, std::size_t bytes_transferred); - void AsyncStreamReceive (); - void HandleStreamReceive (const boost::system::error_code& ecode, std::size_t bytes_transferred); - void HandleWriteReply(const boost::system::error_code& ecode); - void HandleWrite (const boost::system::error_code& ecode); - void SendReply (const std::string& content, int status = 200); - void SendError (const std::string& message); + void Terminate (const boost::system::error_code& ecode); - void HandleRequest (const std::string& address); - void HandleCommand (const std::string& command, std::stringstream& s); - void ShowJumpServices (const std::string& address, std::stringstream& s); - void ShowTransports (std::stringstream& s); - void ShowTunnels (std::stringstream& s); - void ShowTransitTunnels (std::stringstream& s); - void ShowLocalDestinations (std::stringstream& s); - void ShowLocalDestination (const std::string& b32, std::stringstream& s); - void ShowSAMSessions (std::stringstream& s); - void ShowSAMSession (const std::string& id, std::stringstream& s); - void ShowI2PTunnels (std::stringstream& s); - void StartAcceptingTunnels (std::stringstream& s); - void StopAcceptingTunnels (std::stringstream& s); - void RunPeerTest (std::stringstream& s); - void FillContent (std::stringstream& s); - std::string ExtractAddress (); - void ExtractParams (const std::string& str, std::map& params); - - - protected: + void RunRequest (); + bool CheckAuth (const HTTPReq & req); + void HandleRequest (const HTTPReq & req); + void HandlePage (const HTTPReq & req, HTTPRes & res, std::stringstream& data); + void HandleCommand (const HTTPReq & req, HTTPRes & res, std::stringstream& data); + void SendReply (HTTPRes & res, std::string & content); + + private: std::shared_ptr m_Socket; boost::asio::deadline_timer m_Timer; - std::shared_ptr m_Stream; - char m_Buffer[HTTP_CONNECTION_BUFFER_SIZE + 1], m_StreamBuffer[HTTP_CONNECTION_BUFFER_SIZE + 1]; + char m_Buffer[HTTP_CONNECTION_BUFFER_SIZE + 1]; size_t m_BufferLen; - request m_Request; - reply m_Reply; - - protected: - - virtual void RunRequest (); - void HandleDestinationRequest(const std::string& address, const std::string& uri); - void SendToAddress (const std::string& address, int port, const char * buf, size_t len); - void HandleDestinationRequestTimeout (const boost::system::error_code& ecode, - i2p::data::IdentHash destination, int port, const char * buf, size_t len); - void SendToDestination (std::shared_ptr remote, int port, const char * buf, size_t len); - - public: - - static const std::string itoopieImage; - static const std::string itoopieFavicon; + bool needAuth; + std::string user; + std::string pass; }; class HTTPServer @@ -110,7 +42,7 @@ namespace util public: HTTPServer (const std::string& address, int port); - virtual ~HTTPServer (); + ~HTTPServer (); void Start (); void Stop (); @@ -121,6 +53,7 @@ namespace util void Accept (); void HandleAccept(const boost::system::error_code& ecode, std::shared_ptr newSocket); + void CreateConnection(std::shared_ptr newSocket); private: @@ -128,13 +61,8 @@ namespace util boost::asio::io_service m_Service; boost::asio::io_service::work m_Work; boost::asio::ip::tcp::acceptor m_Acceptor; - - protected: - virtual void CreateConnection(std::shared_ptr newSocket); }; -} -} - -#endif - +} // http +} // i2p +#endif /* HTTP_SERVER_H__ */ diff --git a/build/CMakeLists.txt b/build/CMakeLists.txt index 76963026..22029e85 100644 --- a/build/CMakeLists.txt +++ b/build/CMakeLists.txt @@ -74,6 +74,7 @@ set (CLIENT_SRC "${CMAKE_SOURCE_DIR}/I2PService.cpp" "${CMAKE_SOURCE_DIR}/SAM.cpp" "${CMAKE_SOURCE_DIR}/SOCKS.cpp" + "${CMAKE_SOURCE_DIR}/HTTP.cpp" "${CMAKE_SOURCE_DIR}/HTTPProxy.cpp" ) diff --git a/docs/configuration.md b/docs/configuration.md index 2a639be3..11e8b4a8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -36,6 +36,9 @@ All options below still possible in cmdline, but better write it in config file: * --http.address= - The address to listen on (HTTP server) * --http.port= - The port to listen on (HTTP server) +* --http.auth - Enable basic HTTP auth for webconsole +* --http.user= - Username for basic auth (default: i2pd) +* --http.pass= - Password for basic auth (default: random, see logs) * --httpproxy.address= - The address to listen on (HTTP Proxy) * --httpproxy.port= - The port to listen on (HTTP Proxy) 4446 by default diff --git a/filelist.mk b/filelist.mk index c35edcf0..e2a3da36 100644 --- a/filelist.mk +++ b/filelist.mk @@ -9,7 +9,7 @@ LIB_SRC = \ LIB_CLIENT_SRC = \ AddressBook.cpp BOB.cpp ClientContext.cpp I2PTunnel.cpp I2PService.cpp \ - SAM.cpp SOCKS.cpp HTTPProxy.cpp + SAM.cpp SOCKS.cpp HTTP.cpp HTTPProxy.cpp # also: Daemon{Linux,Win32}.cpp will be added later DAEMON_SRC = \ diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 00000000..199b7353 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,14 @@ +CXXFLAGS += -Wall -Wextra -pedantic -O0 -g -std=c++11 -D_GLIBCXX_USE_NANOSLEEP=1 + +TESTS = test-http-url test-http-req test-http-res test-http-url_decode + +all: $(TESTS) run + +test-http-%: test-http-%.cpp ../HTTP.cpp + $(CXX) $(CXXFLAGS) $(NEEDED_CXXFLAGS) $(INCFLAGS) -o $@ $^ + +run: $(TESTS) + @for TEST in $(TESTS); do ./$$TEST ; done + +clean: + rm -f $(TESTS) diff --git a/tests/test-http-req.cpp b/tests/test-http-req.cpp new file mode 100644 index 00000000..484a7ad6 --- /dev/null +++ b/tests/test-http-req.cpp @@ -0,0 +1,82 @@ +#include +#include "../HTTP.h" + +using namespace i2p::http; + +int main(int argc, char *argv[]) { + HTTPReq *req; + int ret = 0, len = 0; + const char *buf; + + buf = + "GET / HTTP/1.0\r\n" + "User-Agent: curl/7.26.0\r\n" + "Host: inr.i2p\r\n" + "Accept: */*\r\n" + "\r\n" + "test"; + len = strlen(buf); + req = new HTTPReq; + assert((ret = req->parse(buf, len)) == len - 4); + assert(req->version == "HTTP/1.0"); + assert(req->method == "GET"); + assert(req->uri == "/"); + assert(req->host == "inr.i2p"); + assert(req->headers.size() == 3); + assert(req->headers.count("Host") == 1); + assert(req->headers.count("Accept") == 1); + assert(req->headers.count("User-Agent") == 1); + assert(req->headers.find("Host")->second == "inr.i2p"); + assert(req->headers.find("Accept")->second == "*/*"); + assert(req->headers.find("User-Agent")->second == "curl/7.26.0"); + delete req; + + buf = + "GET / HTTP/1.0\r\n" + "\r\n"; + len = strlen(buf); + req = new HTTPReq; + assert((ret = req->parse(buf, len)) == len); + assert(req->version == "HTTP/1.0"); + assert(req->method == "GET"); + assert(req->uri == "/"); + assert(req->host == ""); + assert(req->headers.size() == 0); + delete req; + + buf = + "GET / HTTP/1.1\r\n" + "\r\n"; + len = strlen(buf); + req = new HTTPReq; + assert((ret = req->parse(buf, len)) == -1); /* no host header */ + delete req; + + buf = + "GET / HTTP/1.0\r\n" + ""; + len = strlen(buf); + req = new HTTPReq; + assert((ret = req->parse(buf, len)) == 0); /* request not completed */ + delete req; + + buf = + "GET http://inr.i2p HTTP/1.1\r\n" + "Host: stats.i2p\r\n" + "Accept: */*\r\n" + "\r\n"; + len = strlen(buf); + req = new HTTPReq; + assert((ret = req->parse(buf, len)) == len); /* no host header */ + assert(req->method == "GET"); + assert(req->uri == "http://inr.i2p"); + assert(req->host == "stats.i2p"); + assert(req->headers.size() == 2); + assert(req->headers.count("Host") == 1); + assert(req->headers.count("Accept") == 1); + delete req; + + return 0; +} + +/* vim: expandtab:ts=2 */ diff --git a/tests/test-http-res.cpp b/tests/test-http-res.cpp new file mode 100644 index 00000000..6188a68d --- /dev/null +++ b/tests/test-http-res.cpp @@ -0,0 +1,37 @@ +#include +#include "../HTTP.h" + +using namespace i2p::http; + +int main(int argc, char *argv[]) { + HTTPRes *res; + int ret = 0, len = 0; + const char *buf; + + buf = + "HTTP/1.1 304 Not Modified\r\n" + "Date: Thu, 14 Apr 2016 00:00:00 GMT\r\n" + "Server: nginx/1.2.1\r\n" + "Content-Length: 536\r\n" + "\r\n"; + len = strlen(buf); + res = new HTTPRes; + assert((ret = res->parse(buf, len)) == len); + assert(res->version == "HTTP/1.1"); + assert(res->status == "Not Modified"); + assert(res->code == 304); + assert(res->headers.size() == 3); + assert(res->headers.count("Date") == 1); + assert(res->headers.count("Server") == 1); + assert(res->headers.count("Content-Length") == 1); + assert(res->headers.find("Date")->second == "Thu, 14 Apr 2016 00:00:00 GMT"); + assert(res->headers.find("Server")->second == "nginx/1.2.1"); + assert(res->headers.find("Content-Length")->second == "536"); + assert(res->is_chunked() == false); + assert(res->length() == 536); + delete res; + + return 0; +} + +/* vim: expandtab:ts=2 */ diff --git a/tests/test-http-url.cpp b/tests/test-http-url.cpp new file mode 100644 index 00000000..71b2f703 --- /dev/null +++ b/tests/test-http-url.cpp @@ -0,0 +1,110 @@ +#include +#include "../HTTP.h" + +using namespace i2p::http; + +int main(int argc, char *argv[]) { + std::map params; + URL *url; + + url = new URL; + assert(url->parse("https://127.0.0.1:7070/asdasd?12345") == true); + assert(url->schema == "https"); + assert(url->user == ""); + assert(url->pass == ""); + assert(url->host == "127.0.0.1"); + assert(url->port == 7070); + assert(url->path == "/asdasd"); + assert(url->query == "12345"); + assert(url->to_string() == "https://127.0.0.1:7070/asdasd?12345"); + delete url; + + url = new URL; + assert(url->parse("http://user:password@site.com:8080/asdasd?123456") == true); + assert(url->schema == "http"); + assert(url->user == "user"); + assert(url->pass == "password"); + assert(url->host == "site.com"); + assert(url->port == 8080); + assert(url->path == "/asdasd"); + assert(url->query == "123456"); + delete url; + + url = new URL; + assert(url->parse("http://user:password@site.com/asdasd?name=value") == true); + assert(url->schema == "http"); + assert(url->user == "user"); + assert(url->pass == "password"); + assert(url->host == "site.com"); + assert(url->port == 0); + assert(url->path == "/asdasd"); + assert(url->query == "name=value"); + delete url; + + url = new URL; + assert(url->parse("http://user:@site.com/asdasd?name=value1&name=value2") == true); + assert(url->schema == "http"); + assert(url->user == "user"); + assert(url->pass == ""); + assert(url->host == "site.com"); + assert(url->port == 0); + assert(url->path == "/asdasd"); + assert(url->query == "name=value1&name=value2"); + delete url; + + url = new URL; + assert(url->parse("http://user@site.com/asdasd?name1=value1&name2&name3=value2") == true); + assert(url->schema == "http"); + assert(url->user == "user"); + assert(url->pass == ""); + assert(url->host == "site.com"); + assert(url->port == 0); + assert(url->path == "/asdasd"); + assert(url->query == "name1=value1&name2&name3=value2"); + assert(url->parse_query(params)); + assert(params.size() == 3); + assert(params.count("name1") == 1); + assert(params.count("name2") == 1); + assert(params.count("name3") == 1); + assert(params.find("name1")->second == "value1"); + assert(params.find("name2")->second == ""); + assert(params.find("name3")->second == "value2"); + delete url; + + url = new URL; + assert(url->parse("http://@site.com:800/asdasd?") == true); + assert(url->schema == "http"); + assert(url->user == ""); + assert(url->pass == ""); + assert(url->host == "site.com"); + assert(url->port == 800); + assert(url->path == "/asdasd"); + assert(url->query == ""); + delete url; + + url = new URL; + assert(url->parse("http://@site.com:17") == true); + assert(url->schema == "http"); + assert(url->user == ""); + assert(url->pass == ""); + assert(url->host == "site.com"); + assert(url->port == 17); + assert(url->path == ""); + assert(url->query == ""); + delete url; + + url = new URL; + assert(url->parse("http://user:password@site.com:err_port/asdasd") == false); + assert(url->schema == "http"); + assert(url->user == "user"); + assert(url->pass == "password"); + assert(url->host == "site.com"); + assert(url->port == 0); + assert(url->path == ""); + assert(url->query == ""); + delete url; + + return 0; +} + +/* vim: expandtab:ts=2 */ diff --git a/tests/test-http-url_decode.cpp b/tests/test-http-url_decode.cpp new file mode 100644 index 00000000..1c548e6f --- /dev/null +++ b/tests/test-http-url_decode.cpp @@ -0,0 +1,19 @@ +#include +#include "../HTTP.h" + +using namespace i2p::http; + +int main(int argc, char *argv[]) { + std::string in("/%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0/"); + std::string out = UrlDecode(in); + + assert(strcmp(out.c_str(), "/страница/") == 0); + + in = "/%00/"; + out = UrlDecode(in, false); + assert(strcmp(out.c_str(), "/%00/") == 0); + out = UrlDecode(in, true); + assert(strcmp(out.c_str(), "/\0/") == 0); + + return 0; +}