567 lines
16 KiB
C++
567 lines
16 KiB
C++
|
|
#include "Client.hpp"
|
|
|
|
/*********************************************
|
|
* COPLIENS
|
|
*********************************************/
|
|
|
|
Client::Client()
|
|
: status(0),
|
|
header_complete(false),
|
|
body_complete(false),
|
|
request_complete(false),
|
|
assigned_server(NULL),
|
|
assigned_location(NULL),
|
|
cgi_pipe_rfd(0),
|
|
cgi_pid(0),
|
|
_fd(0),
|
|
_port(""),
|
|
_ip(""),
|
|
_lsocket(NULL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Client::Client(int afd, listen_socket *lsocket, std::string aport, std::string aip)
|
|
: status(0),
|
|
header_complete(false),
|
|
body_complete(false),
|
|
request_complete(false),
|
|
assigned_server(NULL),
|
|
assigned_location(NULL),
|
|
cgi_pipe_rfd(0),
|
|
cgi_pid(0),
|
|
_fd(afd),
|
|
_port(aport),
|
|
_ip(aip),
|
|
_lsocket(lsocket)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Client::~Client() {
|
|
return;
|
|
}
|
|
|
|
/* // WIP not sure fo what is more logic here
|
|
Client::Client( Client const & src )
|
|
: status ( src.status ),
|
|
header_complete ( src.header_complete ),
|
|
assigned_server ( src.assigned_server ),
|
|
assigned_location ( src.assigned_location ),
|
|
_fd ( src._fd ),
|
|
_port ( src._port ),
|
|
_ip ( src._ip ),
|
|
_lsocket ( src._lsocket )
|
|
{
|
|
raw_request = src.raw_request;
|
|
response = src.response;
|
|
// buf = strdup(src.buf); // TODO: this doesn't work
|
|
return;
|
|
}
|
|
*/
|
|
|
|
/* // WIP placeholder because of const values
|
|
Client & Client::operator=( Client const & rhs )
|
|
{
|
|
if ( this != &rhs )
|
|
{
|
|
// stuff
|
|
}
|
|
return *this;
|
|
}
|
|
*/
|
|
|
|
/*********************************************
|
|
* PUBLIC MEMBER FUNCTIONS
|
|
*********************************************/
|
|
|
|
/* HTTP Headers :
|
|
https://www.iana.org/assignments/http-fields/http-fields.xhtml
|
|
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
|
|
https://www.ibm.com/docs/en/cics-ts/5.3?topic=protocol-http-requests
|
|
https://www.tutorialspoint.com/http/http_requests.htm
|
|
*/
|
|
void Client::parse_request_headers(std::vector<ServerConfig> &servers)
|
|
{
|
|
if (raw_request.find(CRLF CRLF) == NPOS)
|
|
return ;
|
|
header_complete = true;
|
|
clear_request(); // not mandatory
|
|
|
|
_parse_request_line();
|
|
if (status)
|
|
return;
|
|
_parse_request_fields();
|
|
|
|
// DEBUG
|
|
// print_client("headers");
|
|
|
|
if (status)
|
|
return;
|
|
assigned_server = _determine_process_server(this, servers);
|
|
assigned_location = _determine_location(*assigned_server, _request.abs_path);
|
|
_check_request_errors();
|
|
if (status)
|
|
return;
|
|
_parse_port_hostname(get_rq_headers("Host")); // use getter for headers because it works case insensitive
|
|
|
|
// DEBUG
|
|
// std::cerr << get_rq_method_str() << " " << get_rq_target() << " " << get_rq_version() << "\n";
|
|
|
|
// dont clear raw_request, we need it for future reparsing of body
|
|
// see call of parse_request() in _read_request()
|
|
// raw_request.clear();
|
|
}
|
|
|
|
void Client::parse_request_body()
|
|
{
|
|
std::cerr << "parse_request_body()\n";
|
|
size_t pos;
|
|
|
|
pos = raw_request.find(CRLF CRLF);
|
|
if (pos == NPOS)
|
|
{
|
|
std::cerr << "parse_request_body() bad call, header incomplete\n";
|
|
// QUESTION from hugo : don't we change the status here ?
|
|
// RESPONSE from luke : C'est vrai. Peut-être mettre un 500, c'etait plus du debug à la base.
|
|
// C'est seulement si on appelle la fonction au mauvais endroit, avant d'avoir un header complet, que ça arrive.
|
|
return;
|
|
}
|
|
|
|
if (!get_rq_headers("Transfer-Encoding").empty()
|
|
&& get_rq_headers("Transfer-Encoding") == "chunked")
|
|
{
|
|
// Chunked decoding WIP. How to test this ? dont know how to send chunks with telnet.
|
|
_parse_chunked_body(pos + CRLF_SIZE*2);
|
|
}
|
|
else if (raw_request.size() - pos >= std::strtoul(get_rq_headers("Content-Length").c_str(), NULL, 10))
|
|
{
|
|
if (get_rq_headers("Content-Type").find("multipart/form-data") != NPOS)
|
|
_parse_multipart_body(pos);
|
|
else
|
|
_request.body = raw_request.substr(pos + CRLF_SIZE*2);
|
|
body_complete = true;
|
|
}
|
|
// std::cerr << "Content-Length = " << std::strtoul(get_rq_headers("Content-Length").c_str(), NULL, 10) << "\n";
|
|
// std::cerr << "raw_request.size() - pos = " << raw_request.size() - pos << "\n";
|
|
// _request.body = raw_request.substr(pos);
|
|
// std::cerr << "_request.body.size() = " << _request.body.size() << "\n";
|
|
|
|
///////////////
|
|
// Body checks
|
|
if (_request.body.size() > assigned_server->client_body_limit)
|
|
status = 413; // HTTP Client Errors
|
|
}
|
|
|
|
void Client::_parse_chunked_body(size_t pos)
|
|
{
|
|
size_t chunk_size = 1;
|
|
size_t chunk_field_end = 0;
|
|
char *endptr = NULL;
|
|
char *endptr_copy = NULL;
|
|
/* TODO: verify if last chunk in raw_request (to avoid multiples complete parsing)
|
|
but how ? with "raw_request.rfind("0" CRLF CRLF)", there no confirmation
|
|
that we have found the last last-chunk OR just some data */
|
|
|
|
_request.body = raw_request.substr(pos);
|
|
|
|
std::cerr << "______Chunked\n" << _request.body << "\n______\n";
|
|
pos = 0;
|
|
while (chunk_size != 0)
|
|
{
|
|
/* if (pos > _request.body.size())
|
|
{
|
|
std::cerr << "parse_request_body(), pos > size()\n";
|
|
// status = 400;
|
|
return;
|
|
} */
|
|
/*
|
|
if (pos == _request.body.size())
|
|
{
|
|
std::cerr << "parse_request_body(), will reread till last chunk\n";
|
|
return;
|
|
} */
|
|
|
|
/* endptr_copy = endptr; */
|
|
(void)endptr_copy;
|
|
chunk_size = std::strtoul(&_request.body[pos], &endptr, 16);
|
|
if (errno == ERANGE)
|
|
{
|
|
status = 413;
|
|
return ;
|
|
}
|
|
/* if (endptr == endptr_copy)
|
|
{
|
|
std::cerr << "parse_request_body(), no conversion possible\n";
|
|
return;
|
|
} */
|
|
|
|
|
|
chunk_field_end = _request.body.find(CRLF, pos);
|
|
if (chunk_field_end == NPOS)
|
|
{
|
|
std::cerr << "parse_request_body(), chunk_field no CRLF\n";
|
|
// status = 400;
|
|
return;
|
|
}
|
|
|
|
chunk_field_end += CRLF_SIZE;
|
|
_request.body.erase(pos, chunk_field_end);
|
|
pos += chunk_size + CRLF_SIZE;
|
|
}
|
|
|
|
_request.headers.erase("Transfer-Encoding");
|
|
body_complete = true;
|
|
}
|
|
|
|
|
|
|
|
void Client::fill_script_path(std::string &path, size_t pos)
|
|
{
|
|
std::string tmp;
|
|
|
|
if (path[0] == '.')
|
|
{
|
|
path.erase(0, 1);
|
|
pos--;
|
|
}
|
|
_request.script.path = path.substr(0, pos);
|
|
_request.script.info = path.substr(pos);
|
|
}
|
|
|
|
void Client::clear()
|
|
{
|
|
clear_request();
|
|
raw_request.clear();
|
|
response.clear();
|
|
status = 0;
|
|
header_complete = false;
|
|
body_complete = false;
|
|
request_complete = false;
|
|
assigned_server = NULL;
|
|
assigned_location = NULL;
|
|
cgi_pipe_rfd = 0;
|
|
cgi_pid = 0;
|
|
cgi_output.clear();
|
|
}
|
|
|
|
void Client::clear_request()
|
|
{
|
|
clear_script();
|
|
_request.method = UNKNOWN;
|
|
_request.target.clear();
|
|
_request.version.clear();
|
|
_request.headers.clear();
|
|
_request.body.clear();
|
|
_request.multi_bodys.clear();
|
|
_request.abs_path.clear();
|
|
_request.query.clear();
|
|
_request.port.clear();
|
|
_request.hostname.clear();
|
|
}
|
|
|
|
void Client::clear_script()
|
|
{
|
|
_request.script.path.clear();
|
|
_request.script.info.clear();
|
|
}
|
|
|
|
// debug
|
|
void Client::print_client(std::string message)
|
|
{
|
|
std::map<std::string, std::string>::iterator it;
|
|
std::cout << "\n=== DEBUG PRINT CLIENT ===\n";
|
|
std::cout << message << ":\n----------\n\n" << "raw_request:\n__\n";
|
|
::print_special(raw_request);
|
|
std::cout << "\n__\n"
|
|
<< "get_cl_fd() : [" << get_cl_fd() << "]\n"
|
|
<< "get_cl_port() : [" << get_cl_port() << "]\n"
|
|
<< "get_cl_ip() : [" << get_cl_ip() << "]\n"
|
|
<< "get_rq_method_str() : [" << get_rq_method_str() << "]\n"
|
|
<< "get_rq_target() : [" << get_rq_target() << "]\n"
|
|
<< "get_rq_abs_path() : [" << get_rq_abs_path() << "]\n"
|
|
<< "get_rq_query() : [" << get_rq_query() << "]\n"
|
|
<< "get_rq_version() : [" << get_rq_version() << "]\n"
|
|
<< "get_rq_body() : [" << get_rq_body() << "]\n"
|
|
<< "get_rq_port() : [" << get_rq_port() << "]\n"
|
|
<< "get_rq_hostname() : [" << get_rq_hostname() << "]\n"
|
|
<< "get_rq_script_path() : [" << get_rq_script_path() << "]\n"
|
|
<< "get_rq_script_info() : [" << get_rq_script_info() << "]\n"
|
|
<< "headers :\n";
|
|
for (it = _request.headers.begin(); it != _request.headers.end(); it++)
|
|
std::cout << " " << it->first << ": [" << it->second << "]\n";
|
|
std::cout << "\n=== END PRINT CLIENT ===\n\n";
|
|
}
|
|
|
|
/*********************************************
|
|
* GETTERS
|
|
*********************************************/
|
|
|
|
// client side
|
|
int Client::get_cl_fd() const { return _fd; }
|
|
const std::string & Client::get_cl_ip() const { return _ip; }
|
|
const std::string & Client::get_cl_port() const { return _port; }
|
|
const listen_socket * Client::get_cl_lsocket() const { return _lsocket; }
|
|
|
|
// requette
|
|
http_method Client::get_rq_method() const { return _request.method; }
|
|
std::string Client::get_rq_method_str() const
|
|
{ return ::http_methods_to_str(_request.method); }
|
|
std::string Client::get_rq_target() const { return _request.target; }
|
|
std::string Client::get_rq_abs_path() const { return _request.abs_path; }
|
|
std::string Client::get_rq_query() const { return _request.query; }
|
|
std::string Client::get_rq_version() const { return _request.version; }
|
|
std::string Client::get_rq_body() const { return _request.body; }
|
|
std::string Client::get_rq_port() const { return _request.port; }
|
|
std::string Client::get_rq_hostname() const { return _request.hostname; }
|
|
std::string Client::get_rq_script_path()const { return _request.script.path; }
|
|
std::string Client::get_rq_script_info()const { return _request.script.info; }
|
|
|
|
std::string Client::get_rq_headers(const std::string & key) const
|
|
{
|
|
std::map<std::string, std::string>::const_iterator it;
|
|
|
|
it = _request.headers.find(::str_tolower(key));
|
|
if (it == _request.headers.end())
|
|
return "";
|
|
return it->second;
|
|
}
|
|
|
|
|
|
/*********************************************
|
|
* PRIVATE MEMBER FUNCTIONS
|
|
*********************************************/
|
|
|
|
void Client::_parse_request_line()
|
|
{
|
|
std::vector<std::string> line;
|
|
std::string raw_line;
|
|
|
|
raw_line = ::get_line(raw_request, 0, CRLF);
|
|
line = ::split_trim(raw_line, " ");
|
|
if (line.size() != 3)
|
|
{
|
|
std::cerr << "err _parse_first_line(): wrong number of elements (" << line.size() << " instead of 3)\n";
|
|
status = 400; // "bad request"
|
|
}
|
|
else
|
|
{
|
|
_request.method = str_to_http_method(line[0]);
|
|
_request.target = line[1];
|
|
_parse_request_target(line[1]);
|
|
_request.version = line[2];
|
|
}
|
|
}
|
|
|
|
void Client::_parse_request_target( std::string target )
|
|
{
|
|
size_t pos;
|
|
|
|
pos = target.find("?");
|
|
if (pos != NPOS)
|
|
_request.query = target.substr(pos + 1);
|
|
else
|
|
_request.query = "";
|
|
_request.abs_path = target.substr(0, pos);
|
|
if (_request.abs_path[_request.abs_path.size() - 1] == '/')
|
|
_request.abs_path.erase(_request.abs_path.size() - 1);
|
|
}
|
|
|
|
void Client::_parse_request_fields()
|
|
{
|
|
std::string headers;
|
|
size_t pos;
|
|
int ret;
|
|
|
|
headers = raw_request;
|
|
// delete first line
|
|
pos = headers.find(CRLF);
|
|
if (pos != NPOS)
|
|
headers.erase(0, pos + CRLF_SIZE);
|
|
// delete body part
|
|
pos = headers.find(CRLF CRLF);
|
|
if (pos != NPOS)
|
|
headers.erase(pos);
|
|
else {
|
|
std::cerr << "err _parse_request_fields(): request header doesn't end with empty line\n";
|
|
status = 400; // "bad request"
|
|
}
|
|
// copy result of parser into headers
|
|
ret = ::parse_http_headers(headers, _request.headers);
|
|
if (ret > 0) {
|
|
std::cerr << "err _parse_request_fields(): " << ret << " fields are bad formated\n";
|
|
status = 400; // "bad request"
|
|
}
|
|
::str_map_key_tolower(_request.headers);
|
|
}
|
|
|
|
// TODO : I think its now useless. Probably to delete.
|
|
void Client::_parse_port_hostname(std::string host)
|
|
{
|
|
size_t pos;
|
|
|
|
if (host == "")
|
|
std::cerr << "no host\n";
|
|
|
|
pos = host.find(':');
|
|
// port :
|
|
if (pos == NPOS)
|
|
_request.port = "4040"; // TODO: make equal to default port in config
|
|
else
|
|
_request.port = host.substr(pos);
|
|
if (_request.port == ":")
|
|
_request.port = "";
|
|
// hostname :
|
|
_request.hostname = host.substr(0, pos);
|
|
}
|
|
|
|
void Client::_check_request_errors()
|
|
{
|
|
// /* Debug */ std::cerr << "Content-Length=" << get_rq_headers("Content-Length") << "\n";
|
|
// /* Debug */ std::cerr << "strtoul=" << std::strtoul(get_rq_headers("Content-Length").c_str(), NULL, 10) << "\n";
|
|
// /* Debug */ std::cerr << "client_body_limit=" << assigned_server->client_body_limit << "\n";
|
|
///////////////////////
|
|
// Request line checks
|
|
if (_request.method == UNKNOWN)
|
|
status = 501; // HTTP Client Errors
|
|
else if (_request.version.compare(0, sizeof("HTTP/1") - 1, "HTTP/1") != 0)
|
|
status = 505; // HTTP Client Errors
|
|
else if (!(assigned_location->allow_methods & _request.method))
|
|
{
|
|
status = 405; // HTTP Client Errors
|
|
response.append("Allow: ");
|
|
response.append(::http_methods_to_str(assigned_location->allow_methods));
|
|
response.append(CRLF);
|
|
}
|
|
else if (assigned_location->redirect_status)
|
|
{ // Weird behavior. Sometimes, the web browser seems to wait for a complete response until timeout.
|
|
// (for codes 301, 302, 303, 307, and 308)
|
|
status = assigned_location->redirect_status;
|
|
response.append("Location: ");
|
|
response.append(assigned_location->redirect_uri);
|
|
response.append(CRLF CRLF);
|
|
}
|
|
|
|
//////////////////
|
|
// Headers checks
|
|
else if (!get_rq_headers("Content-Length").empty()
|
|
&& std::strtoul(get_rq_headers("Content-Length").c_str(), NULL, 10) > assigned_server->client_body_limit)
|
|
status = 413;
|
|
else if (!get_rq_headers("Transfer-Encoding").empty()
|
|
&& get_rq_headers("Transfer-Encoding") != "chunked" )
|
|
status = 501;
|
|
else if (!get_rq_headers("Content-Encoding").empty())
|
|
{
|
|
status = 415;
|
|
response.append("Accept-Encoding:"); // empty, no encoding accepted
|
|
response.append(CRLF);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
ServerConfig *Client::_determine_process_server(Client *client, std::vector<ServerConfig> &servers)
|
|
{
|
|
/*
|
|
Behavior like this :
|
|
http://nginx.org/en/docs/http/request_processing.html
|
|
*/
|
|
|
|
std::string server_name = client->get_rq_headers("Host");
|
|
// /* Debug */ std::cerr << "server_name = " << server_name << "\n";
|
|
size_t pos = server_name.rfind(':');
|
|
if (pos != NPOS)
|
|
server_name.erase(pos);
|
|
// /* Debug */ std::cerr << "server_name = " << server_name << "\n";
|
|
|
|
std::vector<ServerConfig>::iterator it = servers.begin();
|
|
std::vector<ServerConfig>::iterator default_server = servers.end();
|
|
|
|
while (it != servers.end())
|
|
{
|
|
if (it->host == client->get_cl_lsocket()->host
|
|
&& it->port == client->get_cl_lsocket()->port)
|
|
{
|
|
if ( std::find(it->server_name.begin(), it->server_name.end(), server_name) != it->server_name.end() )
|
|
break;
|
|
else if (default_server == servers.end())
|
|
default_server = it;
|
|
}
|
|
++it;
|
|
}
|
|
if (it != servers.end())
|
|
return (&(*it));
|
|
else
|
|
return (&(*default_server));
|
|
}
|
|
|
|
// const?
|
|
const LocationConfig *Client::_determine_location(const ServerConfig &server, const std::string &path)
|
|
{
|
|
/* RULES ***
|
|
|
|
If a path coresponds exactly to a location, use that one
|
|
if no path coresponds then use the most correct one
|
|
most correct means the most precise branch that is still above
|
|
the point we are aiming for
|
|
|
|
New Rule for location paths, they never end in /
|
|
Sooo
|
|
If we get a url that ends in / ignore the last /
|
|
|
|
*/
|
|
|
|
std::string uri = path;
|
|
if (uri[uri.size() - 1] == '/' && uri.size() != 1)
|
|
uri.erase(uri.size() - 1);
|
|
|
|
|
|
for (std::vector<LocationConfig>::const_iterator it = server.locations.begin(); it != server.locations.end(); it++)
|
|
{
|
|
// std::cout << it->path << " -- ";
|
|
if (it->path.size() > uri.size())
|
|
continue ;
|
|
|
|
if (uri.compare(0, it->path.size(), it->path) == 0)
|
|
{
|
|
if (it->path.size() == uri.size())
|
|
return (&(*it));
|
|
else if (uri[it->path.size()] == '/')
|
|
return (&(*it));
|
|
// this works cuz only ever looking for a / burried in a longer path
|
|
}
|
|
}
|
|
return (&(server.locations.back()));
|
|
|
|
|
|
// /test/mdr
|
|
// /test/mdr/
|
|
// /test/mdrBST
|
|
|
|
/////// More stuff to check this still works with
|
|
|
|
// /test/test_
|
|
// /test/test_/
|
|
// /test/test_deeper
|
|
// /test/test_deeper/
|
|
// /test/test_deepei
|
|
// /test/test_deepei/
|
|
// /test/test_deeperi
|
|
// /test/test_deeper/super_deep/
|
|
// /test/aaaaaaaaaaa/super_deep/
|
|
|
|
}
|
|
|
|
/*********************************************
|
|
* OVERLOAD
|
|
*********************************************/
|
|
|
|
bool operator==(const Client& lhs, const Client& rhs)
|
|
{ return lhs.get_cl_fd() == rhs.get_cl_fd(); }
|
|
bool operator==(const Client& lhs, int fd)
|
|
{ return lhs.get_cl_fd() == fd; }
|
|
bool operator==(int fd, const Client& rhs)
|
|
{ return fd == rhs.get_cl_fd(); }
|
|
|