diff --git a/client/HTTPServer.cpp b/client/HTTPServer.cpp index 8aea3ae7..f4e5efff 100644 --- a/client/HTTPServer.cpp +++ b/client/HTTPServer.cpp @@ -63,7 +63,18 @@ void HTTPConnection::HandleReceive(const boost::system::error_code& e, std::size if(!e) { m_Buffer[nb_bytes] = 0; m_BufferLen = nb_bytes; - RunRequest(); + const std::string data = std::string(m_Buffer, m_Buffer + m_BufferLen); + if(!m_Request.hasData()) // New request + m_Request = i2p::util::http::Request(data); + else + m_Request.update(data); + + if(m_Request.isComplete()) { + RunRequest(); + m_Request.clear(); + } else { + Receive(); + } } else if(e != boost::asio::error::operation_aborted) Terminate(); } @@ -71,13 +82,13 @@ void HTTPConnection::HandleReceive(const boost::system::error_code& e, std::size void HTTPConnection::RunRequest() { try { - m_Request = i2p::util::http::Request(std::string(m_Buffer, m_Buffer + m_BufferLen)); if(m_Request.getMethod() == "GET") return HandleRequest(); if(m_Request.getHeader("Content-Type").find("application/json") != std::string::npos) return HandleI2PControlRequest(); } catch(...) { // Ignore the error for now, probably Content-Type doesn't exist + // Could also be invalid json data } // Unsupported method m_Reply = i2p::util::http::Response(502, ""); @@ -118,13 +129,15 @@ void HTTPConnection::HandleRequest() if(uri == "/") uri = "index.html"; - // Use cannonical to avoid .. or . in path - const std::string address = boost::filesystem::canonical( + // Use canonical to avoid .. or . in path + const boost::filesystem::path address = boost::filesystem::canonical( i2p::util::filesystem::GetWebuiDataDir() / uri, e - ).string(); + ); + + const std::string address_str = address.string(); - std::ifstream ifs(address); - if(e || !ifs || !isAllowed(address)) { + std::ifstream ifs(address_str); + if(e || !ifs || !isAllowed(address_str)) { m_Reply = i2p::util::http::Response(404, ""); return SendReply(); } @@ -136,12 +149,13 @@ void HTTPConnection::HandleRequest() ifs.read(&str[0], str.size()); ifs.close(); + str = i2p::util::http::preprocessContent(str, address.parent_path().string()); m_Reply = i2p::util::http::Response(200, str); // TODO: get rid of this hack, actually determine the MIME type - if(address.substr(address.find_last_of(".")) == ".css") + if(address_str.substr(address_str.find_last_of(".")) == ".css") m_Reply.setHeader("Content-Type", "text/css"); - else if(address.substr(address.find_last_of(".")) == ".js") + else if(address_str.substr(address_str.find_last_of(".")) == ".js") m_Reply.setHeader("Content-Type", "text/javascript"); else m_Reply.setHeader("Content-Type", "text/html"); diff --git a/client/HTTPServer.h b/client/HTTPServer.h index 9cc149fe..412f05fd 100644 --- a/client/HTTPServer.h +++ b/client/HTTPServer.h @@ -41,7 +41,6 @@ private: boost::asio::ip::tcp::socket* m_Socket; boost::asio::deadline_timer m_Timer; char m_Buffer[HTTP_CONNECTION_BUFFER_SIZE + 1]; - char m_StreamBuffer[HTTP_CONNECTION_BUFFER_SIZE + 1]; size_t m_BufferLen; util::http::Request m_Request; util::http::Response m_Reply; diff --git a/core/util/HTTP.cpp b/core/util/HTTP.cpp index 1f94c6c5..931e0b6a 100644 --- a/core/util/HTTP.cpp +++ b/core/util/HTTP.cpp @@ -1,6 +1,10 @@ #include "HTTP.h" #include -#include +#include +#include +#include +#include +#include "Log.h" namespace i2p { namespace util { @@ -19,20 +23,54 @@ void Request::parseHeaderLine(const std::string& line) headers[boost::trim_copy(line.substr(0, pos))] = boost::trim_copy(line.substr(pos + 1)); } +void Request::parseHeader(std::stringstream& ss) +{ + std::string line; + while(std::getline(ss, line) && !boost::trim_copy(line).empty()) + parseHeaderLine(line); + + has_header = boost::trim_copy(line).empty(); + if(!has_header) + header_part = line; + else + header_part = ""; +} + +void Request::setIsComplete() +{ + auto it = headers.find("Content-Length"); + if(it == headers.end()) { + // If Content-Length is not set, assume there is no more content + // TODO: Support chunked transfer, or explictly reject it + is_complete = true; + return; + } + const std::size_t length = std::stoi(it->second); + is_complete = content.size() >= length; +} + Request::Request(const std::string& data) { + if(!data.empty()) + has_data = true; + std::stringstream ss(data); + std::string line; std::getline(ss, line); + + // Assume the request line is always passed in one go parseRequestLine(line); - while(std::getline(ss, line) && !boost::trim_copy(line).empty()) - parseHeaderLine(line); + parseHeader(ss); - if(ss) { + if(has_header && ss) { const std::string current = ss.str(); content = current.substr(ss.tellg()); } + + if(has_header) + setIsComplete(); } std::string Request::getMethod() const @@ -65,6 +103,38 @@ std::string Request::getContent() const return content; } +bool Request::hasData() const +{ + return has_data; +} + +bool Request::isComplete() const +{ + return is_complete; +} + +void Request::clear() +{ + has_data = false; + has_header = false; + is_complete = false; +} + +void Request::update(const std::string& data) +{ + std::stringstream ss(header_part + data); + if(!has_header) + parseHeader(ss); + + if(has_header && ss) { + const std::string current = ss.str(); + content += current.substr(ss.tellg()); + } + + if(has_header) + setIsComplete(); +} + Response::Response(int status, const std::string& content) : status(status), content(content), headers() { @@ -115,6 +185,48 @@ void Response::setContentLength() setHeader("Content-Length", std::to_string(content.size())); } +std::string preprocessContent(const std::string& content, const std::string& path) +{ + const boost::filesystem::path directory(path); // Given path is assumed to be clean + + static const std::regex re( + "<\\!\\-\\-\\s*#include\\s+virtual\\s*\\=\\s*\"([^\"]*)\"\\s*\\-\\->" + ); + + boost::system::error_code e; + + std::string result; + + std::smatch match; + auto it = content.begin(); + while(std::regex_search(it, content.end(), match, re)) { + const auto last = it; + std::advance(it, match.position()); + result.append(last, it); + std::advance(it, match.length()); + + // Read the contents of the included file + std::ifstream ifs( + boost::filesystem::canonical(directory / std::string(match[1]), e).string() + ); + if(e || !ifs) + continue; + + std::string data; + ifs.seekg(0, ifs.end); + data.resize(ifs.tellg()); + ifs.seekg(0, ifs.beg); + ifs.read(&data[0], data.size()); + + result += data; + } + + // Append all of the remaining content + result.append(it, content.end()); + + return result; +} + } } } diff --git a/core/util/HTTP.h b/core/util/HTTP.h index a559938d..41e4d2af 100644 --- a/core/util/HTTP.h +++ b/core/util/HTTP.h @@ -3,6 +3,7 @@ #include #include +#include namespace i2p { namespace util { @@ -13,6 +14,10 @@ class Request { void parseRequestLine(const std::string& line); void parseHeaderLine(const std::string& line); + + void parseHeader(std::stringstream& ss); + + void setIsComplete(); public: Request() = default; @@ -33,13 +38,26 @@ public: std::string getContent() const; + bool hasData() const; + + bool isComplete() const; + + void clear(); + + void update(const std::string& data); + private: + std::string header_part; + std::string method; std::string uri; std::string host; std::string content; int port; std::map headers; + bool has_data; + bool has_header; + bool is_complete; }; class Response { @@ -69,6 +87,11 @@ private: std::map headers; }; +/** + * Handle server side includes. + */ +std::string preprocessContent(const std::string& content, const std::string& path); + } } } diff --git a/tests/Utility.cpp b/tests/Utility.cpp index 795fa517..c876d493 100644 --- a/tests/Utility.cpp +++ b/tests/Utility.cpp @@ -128,6 +128,43 @@ BOOST_AUTO_TEST_CASE(ParseHTTPRequestWithContent) BOOST_CHECK_EQUAL(req2.getContent(), "Random content.\r\nTest content."); } +BOOST_AUTO_TEST_CASE(ParseHTTPRequestWithPartialHeaders) +{ + Request req( + "GET /index.html HTTP/1.1\r\n" + "Host: local" + ); + BOOST_CHECK(req.hasData()); + BOOST_CHECK(!req.isComplete()); + BOOST_CHECK_EQUAL(req.getMethod(), "GET"); + req.update("host\r\n"); + BOOST_CHECK(req.isComplete()); + BOOST_CHECK_EQUAL(req.getHeader("Host"), "localhost"); + req.clear(); + BOOST_CHECK(!req.hasData()); +} + +BOOST_AUTO_TEST_CASE(ParseHTTPRequestHeadersFirst) +{ + Request req( + "GET /index.html HTTP/1.1\r\n" + "Content-Length: 5\r\n" + "Host: localhost\r\n\r\n" + ); + + BOOST_CHECK_EQUAL(req.getMethod(), "GET"); + BOOST_CHECK_EQUAL(req.getHeader("Content-Length"), "5"); + BOOST_CHECK_EQUAL(req.getHeader("Host"), "localhost"); + + BOOST_CHECK(!req.isComplete()); + req.update("ab"); + BOOST_CHECK(!req.isComplete()); + req.update("cde"); + BOOST_CHECK(req.isComplete()); + + BOOST_CHECK_EQUAL(req.getContent(), "abcde"); +} + BOOST_AUTO_TEST_CASE(HTTPResponseStatusMessage) { BOOST_CHECK_EQUAL(Response(0).getStatusMessage(), ""); diff --git a/webui/css/main.css b/webui/css/main.css index d7bcecf1..597bb9c2 100644 --- a/webui/css/main.css +++ b/webui/css/main.css @@ -1,5 +1,7 @@ -html { +body { font-family: sans-serif; + margin: 0px; + padding: 0px; } .header { @@ -41,6 +43,8 @@ h2 { background: #191818 none repeat scroll 0% 0%; overflow-y: auto; display: block; + margin: 0px; + padding: 0px; } .menu-heading { diff --git a/webui/help.html b/webui/help.html new file mode 100644 index 00000000..6949ae1b --- /dev/null +++ b/webui/help.html @@ -0,0 +1,32 @@ + + + +Purple I2P 0.10.0 Webconsole + + + + + + + +
+

I2P help

+
+ +
+

Need help? Join us at IRC: #i2pd-dev at irc.freenode.net

+

+ i2pd at GitHub +

+

I2P Project

+
+ + + +
+ +
+ + diff --git a/webui/index.html b/webui/index.html index 240e6816..68eedd71 100644 --- a/webui/index.html +++ b/webui/index.html @@ -69,48 +69,12 @@ window.onload = function() { -
-

I2P configuration

-
- -
-

Not yet implemented :)

-
- -
-
-

I2P help

-
- -
-

Need help? Join us at IRC: #i2pd-dev at irc.freenode.net

-

- i2pd at GitHub -

-

I2P Project

-
-
- - +
- diff --git a/webui/menu.html b/webui/menu.html new file mode 100644 index 00000000..b5c7d83b --- /dev/null +++ b/webui/menu.html @@ -0,0 +1,14 @@ +