mirror of
https://github.com/PurpleI2P/i2pd.git
synced 2025-01-22 13:27:17 +01:00
Several improvements to HTTPServer.
This commit is contained in:
parent
1ec31125b0
commit
17cd149e7f
|
@ -63,7 +63,18 @@ void HTTPConnection::HandleReceive(const boost::system::error_code& e, std::size
|
||||||
if(!e) {
|
if(!e) {
|
||||||
m_Buffer[nb_bytes] = 0;
|
m_Buffer[nb_bytes] = 0;
|
||||||
m_BufferLen = nb_bytes;
|
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)
|
} else if(e != boost::asio::error::operation_aborted)
|
||||||
Terminate();
|
Terminate();
|
||||||
}
|
}
|
||||||
|
@ -71,13 +82,13 @@ void HTTPConnection::HandleReceive(const boost::system::error_code& e, std::size
|
||||||
void HTTPConnection::RunRequest()
|
void HTTPConnection::RunRequest()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
m_Request = i2p::util::http::Request(std::string(m_Buffer, m_Buffer + m_BufferLen));
|
|
||||||
if(m_Request.getMethod() == "GET")
|
if(m_Request.getMethod() == "GET")
|
||||||
return HandleRequest();
|
return HandleRequest();
|
||||||
if(m_Request.getHeader("Content-Type").find("application/json") != std::string::npos)
|
if(m_Request.getHeader("Content-Type").find("application/json") != std::string::npos)
|
||||||
return HandleI2PControlRequest();
|
return HandleI2PControlRequest();
|
||||||
} catch(...) {
|
} catch(...) {
|
||||||
// Ignore the error for now, probably Content-Type doesn't exist
|
// Ignore the error for now, probably Content-Type doesn't exist
|
||||||
|
// Could also be invalid json data
|
||||||
}
|
}
|
||||||
// Unsupported method
|
// Unsupported method
|
||||||
m_Reply = i2p::util::http::Response(502, "");
|
m_Reply = i2p::util::http::Response(502, "");
|
||||||
|
@ -118,13 +129,15 @@ void HTTPConnection::HandleRequest()
|
||||||
if(uri == "/")
|
if(uri == "/")
|
||||||
uri = "index.html";
|
uri = "index.html";
|
||||||
|
|
||||||
// Use cannonical to avoid .. or . in path
|
// Use canonical to avoid .. or . in path
|
||||||
const std::string address = boost::filesystem::canonical(
|
const boost::filesystem::path address = boost::filesystem::canonical(
|
||||||
i2p::util::filesystem::GetWebuiDataDir() / uri, e
|
i2p::util::filesystem::GetWebuiDataDir() / uri, e
|
||||||
).string();
|
);
|
||||||
|
|
||||||
|
const std::string address_str = address.string();
|
||||||
|
|
||||||
std::ifstream ifs(address);
|
std::ifstream ifs(address_str);
|
||||||
if(e || !ifs || !isAllowed(address)) {
|
if(e || !ifs || !isAllowed(address_str)) {
|
||||||
m_Reply = i2p::util::http::Response(404, "");
|
m_Reply = i2p::util::http::Response(404, "");
|
||||||
return SendReply();
|
return SendReply();
|
||||||
}
|
}
|
||||||
|
@ -136,12 +149,13 @@ void HTTPConnection::HandleRequest()
|
||||||
ifs.read(&str[0], str.size());
|
ifs.read(&str[0], str.size());
|
||||||
ifs.close();
|
ifs.close();
|
||||||
|
|
||||||
|
str = i2p::util::http::preprocessContent(str, address.parent_path().string());
|
||||||
m_Reply = i2p::util::http::Response(200, str);
|
m_Reply = i2p::util::http::Response(200, str);
|
||||||
|
|
||||||
// TODO: get rid of this hack, actually determine the MIME type
|
// 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");
|
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");
|
m_Reply.setHeader("Content-Type", "text/javascript");
|
||||||
else
|
else
|
||||||
m_Reply.setHeader("Content-Type", "text/html");
|
m_Reply.setHeader("Content-Type", "text/html");
|
||||||
|
|
|
@ -41,7 +41,6 @@ private:
|
||||||
boost::asio::ip::tcp::socket* m_Socket;
|
boost::asio::ip::tcp::socket* m_Socket;
|
||||||
boost::asio::deadline_timer m_Timer;
|
boost::asio::deadline_timer m_Timer;
|
||||||
char m_Buffer[HTTP_CONNECTION_BUFFER_SIZE + 1];
|
char m_Buffer[HTTP_CONNECTION_BUFFER_SIZE + 1];
|
||||||
char m_StreamBuffer[HTTP_CONNECTION_BUFFER_SIZE + 1];
|
|
||||||
size_t m_BufferLen;
|
size_t m_BufferLen;
|
||||||
util::http::Request m_Request;
|
util::http::Request m_Request;
|
||||||
util::http::Response m_Reply;
|
util::http::Response m_Reply;
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
#include "HTTP.h"
|
#include "HTTP.h"
|
||||||
#include <boost/algorithm/string.hpp>
|
#include <boost/algorithm/string.hpp>
|
||||||
#include <sstream>
|
#include <iostream>
|
||||||
|
#include <regex>
|
||||||
|
#include <fstream>
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
|
#include "Log.h"
|
||||||
|
|
||||||
namespace i2p {
|
namespace i2p {
|
||||||
namespace util {
|
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));
|
headers[boost::trim_copy(line.substr(0, pos))] = boost::trim_copy(line.substr(pos + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
Request::Request(const std::string& data)
|
void Request::parseHeader(std::stringstream& ss)
|
||||||
{
|
{
|
||||||
std::stringstream ss(data);
|
|
||||||
std::string line;
|
std::string line;
|
||||||
std::getline(ss, line);
|
|
||||||
parseRequestLine(line);
|
|
||||||
|
|
||||||
while(std::getline(ss, line) && !boost::trim_copy(line).empty())
|
while(std::getline(ss, line) && !boost::trim_copy(line).empty())
|
||||||
parseHeaderLine(line);
|
parseHeaderLine(line);
|
||||||
|
|
||||||
if(ss) {
|
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);
|
||||||
|
|
||||||
|
parseHeader(ss);
|
||||||
|
|
||||||
|
if(has_header && ss) {
|
||||||
const std::string current = ss.str();
|
const std::string current = ss.str();
|
||||||
content = current.substr(ss.tellg());
|
content = current.substr(ss.tellg());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(has_header)
|
||||||
|
setIsComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Request::getMethod() const
|
std::string Request::getMethod() const
|
||||||
|
@ -65,6 +103,38 @@ std::string Request::getContent() const
|
||||||
return content;
|
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)
|
Response::Response(int status, const std::string& content)
|
||||||
: status(status), content(content), headers()
|
: status(status), content(content), headers()
|
||||||
{
|
{
|
||||||
|
@ -115,6 +185,48 @@ void Response::setContentLength()
|
||||||
setHeader("Content-Length", std::to_string(content.size()));
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
namespace i2p {
|
namespace i2p {
|
||||||
namespace util {
|
namespace util {
|
||||||
|
@ -13,6 +14,10 @@ class Request {
|
||||||
void parseRequestLine(const std::string& line);
|
void parseRequestLine(const std::string& line);
|
||||||
|
|
||||||
void parseHeaderLine(const std::string& line);
|
void parseHeaderLine(const std::string& line);
|
||||||
|
|
||||||
|
void parseHeader(std::stringstream& ss);
|
||||||
|
|
||||||
|
void setIsComplete();
|
||||||
public:
|
public:
|
||||||
Request() = default;
|
Request() = default;
|
||||||
|
|
||||||
|
@ -33,13 +38,26 @@ public:
|
||||||
|
|
||||||
std::string getContent() const;
|
std::string getContent() const;
|
||||||
|
|
||||||
|
bool hasData() const;
|
||||||
|
|
||||||
|
bool isComplete() const;
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
void update(const std::string& data);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
std::string header_part;
|
||||||
|
|
||||||
std::string method;
|
std::string method;
|
||||||
std::string uri;
|
std::string uri;
|
||||||
std::string host;
|
std::string host;
|
||||||
std::string content;
|
std::string content;
|
||||||
int port;
|
int port;
|
||||||
std::map<std::string, std::string> headers;
|
std::map<std::string, std::string> headers;
|
||||||
|
bool has_data;
|
||||||
|
bool has_header;
|
||||||
|
bool is_complete;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Response {
|
class Response {
|
||||||
|
@ -69,6 +87,11 @@ private:
|
||||||
std::map<std::string, std::string> headers;
|
std::map<std::string, std::string> headers;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle server side includes.
|
||||||
|
*/
|
||||||
|
std::string preprocessContent(const std::string& content, const std::string& path);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,6 +128,43 @@ BOOST_AUTO_TEST_CASE(ParseHTTPRequestWithContent)
|
||||||
BOOST_CHECK_EQUAL(req2.getContent(), "Random content.\r\nTest content.");
|
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_AUTO_TEST_CASE(HTTPResponseStatusMessage)
|
||||||
{
|
{
|
||||||
BOOST_CHECK_EQUAL(Response(0).getStatusMessage(), "");
|
BOOST_CHECK_EQUAL(Response(0).getStatusMessage(), "");
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
html {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@ -41,6 +43,8 @@ h2 {
|
||||||
background: #191818 none repeat scroll 0% 0%;
|
background: #191818 none repeat scroll 0% 0%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
display: block;
|
display: block;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-heading {
|
.menu-heading {
|
||||||
|
|
32
webui/help.html
Normal file
32
webui/help.html
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Purple I2P 0.10.0 Webconsole</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="css/main.css">
|
||||||
|
<script type="text/javascript" src="javascript/I2PControl.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>I2P help</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="content-subhead">Need help? Join us at IRC: #i2pd-dev at irc.freenode.net</h2>
|
||||||
|
<h2 class="content-subhead">
|
||||||
|
<a href="https://github.com/PurpleI2P/i2pd">i2pd at GitHub</a>
|
||||||
|
</h2>
|
||||||
|
<h2 class="content-subhead"><a href="https://geti2p.net/en/">I2P Project</a> </h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--#include virtual="menu.html" -->
|
||||||
|
|
||||||
|
<div id="main">
|
||||||
|
<noscript>
|
||||||
|
<div class="header"><h1>Please enable JavaScript!</h1></div>
|
||||||
|
</noscript>
|
||||||
|
</div>
|
||||||
|
<body>
|
||||||
|
</html>
|
|
@ -69,48 +69,12 @@ window.onload = function() {
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header">
|
<!--#include virtual="menu.html" -->
|
||||||
<h1>I2P configuration</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<h2 class="content-subhead">Not yet implemented :)</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="helpPage" style="display:hidden">
|
|
||||||
<div class="header">
|
|
||||||
<h1>I2P help</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<h2 class="content-subhead">Need help? Join us at IRC: #i2pd-dev at irc.freenode.net</h2>
|
|
||||||
<h2 class="content-subhead">
|
|
||||||
<a href="https://github.com/PurpleI2P/i2pd">i2pd at GitHub</a>
|
|
||||||
</h2>
|
|
||||||
<h2 class="content-subhead"><a href="https://geti2p.net/en/">I2P Project</a> </h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="menu">
|
|
||||||
<span class="menu-heading">i2pd</span>
|
|
||||||
<ul class="menu-list">
|
|
||||||
<li class="menu-item">
|
|
||||||
<a href="#/" class="menu-link">Home</a>
|
|
||||||
</li>
|
|
||||||
<li class="menu-item">
|
|
||||||
<a href="#/config" class="menu-link">Configure</a>
|
|
||||||
</li>
|
|
||||||
<li class="menu-item">
|
|
||||||
<a href="#/help" class="menu-link">Help</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<noscript>
|
<noscript>
|
||||||
<div class="header"><h1>Please enable JavaScript!</h1></div>
|
<div class="header"><h1>Please enable JavaScript!</h1></div>
|
||||||
</noscript>
|
</noscript>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
14
webui/menu.html
Normal file
14
webui/menu.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<div id="menu">
|
||||||
|
<span class="menu-heading">i2pd</span>
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li class="menu-item">
|
||||||
|
<a href="index.html" class="menu-link">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="menu-item">
|
||||||
|
<a href="#/config" class="menu-link">Configure</a>
|
||||||
|
</li>
|
||||||
|
<li class="menu-item">
|
||||||
|
<a href="help.html" class="menu-link">Help</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
Loading…
Reference in a new issue