restructuring
This commit is contained in:
parent
ac669ba504
commit
6c9f7f7c49
|
@ -1,7 +1,9 @@
|
|||
#include "../http.cpp"
|
||||
#include <memory>
|
||||
|
||||
#include "../http/http_request.cpp"
|
||||
#include "../http/http_response.cpp"
|
||||
|
||||
class backend {
|
||||
public:
|
||||
virtual unique_ptr<http_response> handle_request(http_request& req) = 0;
|
||||
virtual std::unique_ptr<http_response> handle_request(http_request& req) = 0;
|
||||
};
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
#include "backend.cpp"
|
||||
#include <filesystem>
|
||||
#include "backend.cpp"
|
||||
|
||||
class file_backend : public backend {
|
||||
private:
|
||||
unordered_map<string, string> file_cache;
|
||||
string file_dir;
|
||||
std::unordered_map<std::string,std::string> file_cache;
|
||||
std::string file_dir;
|
||||
|
||||
unique_ptr<http_response> handle_request_cache(http_request& req) {
|
||||
string filename = req.path() == "/" ? "/index.html" : req.path();
|
||||
std::unique_ptr<http_response> handle_request_cache(http_request& req) {
|
||||
std::string filename = req.path() == "/" ? "/index.html" : req.path();
|
||||
filename = file_dir + filename;
|
||||
auto file_info = file_cache.find(filename);
|
||||
|
||||
|
@ -18,21 +18,21 @@ private:
|
|||
file_info = file_cache.find(filename);
|
||||
}
|
||||
|
||||
return make_unique<http_response>(file_info->second, filename, status);
|
||||
return std::make_unique<http_response>(file_info->second, filename, status);
|
||||
}
|
||||
|
||||
void populate_cache_dir(string dir) {
|
||||
filesystem::recursive_directory_iterator cur = begin(filesystem::recursive_directory_iterator(dir));
|
||||
filesystem::recursive_directory_iterator fin = end(filesystem::recursive_directory_iterator(dir));
|
||||
void populate_cache_dir(std::string dir) {
|
||||
std::filesystem::recursive_directory_iterator cur = begin(std::filesystem::recursive_directory_iterator(dir));
|
||||
std::filesystem::recursive_directory_iterator fin = end(std::filesystem::recursive_directory_iterator(dir));
|
||||
|
||||
while (cur != fin) {
|
||||
auto p = cur->path();
|
||||
string filename = p.string();
|
||||
stringstream buffer;
|
||||
ifstream stream(filename);
|
||||
std::string filename = p.string();
|
||||
std::stringstream buffer;
|
||||
std::ifstream stream(filename);
|
||||
buffer << stream.rdbuf();
|
||||
file_cache[filename] = buffer.str();
|
||||
cout << "File at " << filename << " cached (" << file_cache[filename].size() << " bytes)" << endl;
|
||||
std::cout << "File at " << filename << " cached (" << file_cache[filename].size() << " bytes)" << std::endl;
|
||||
++cur;
|
||||
}
|
||||
}
|
||||
|
@ -40,14 +40,15 @@ private:
|
|||
void populate_cache() {
|
||||
populate_cache_dir(file_dir);
|
||||
populate_cache_dir("./error_pages/");
|
||||
|
||||
}
|
||||
|
||||
public:
|
||||
file_backend(string dir = "./www") : file_dir(dir) {
|
||||
file_backend(std::string dir = "./www") : file_dir(dir) {
|
||||
populate_cache();
|
||||
}
|
||||
|
||||
unique_ptr<http_response> handle_request(http_request& req) override {
|
||||
std::unique_ptr<http_response> handle_request(http_request& req) override {
|
||||
return handle_request_cache(req);
|
||||
}
|
||||
};
|
||||
|
|
80
src/datastructures/smart_map.cpp
Normal file
80
src/datastructures/smart_map.cpp
Normal file
|
@ -0,0 +1,80 @@
|
|||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
constexpr int benchmark_loops = 1000000;
|
||||
|
||||
template <typename KeyType, typename ValueType>
|
||||
class smart_map {
|
||||
private:
|
||||
bool use_hmap = false;
|
||||
std::unordered_map<KeyType, ValueType> hmap;
|
||||
|
||||
double assess_hmap(const std::vector<std::pair<KeyType, ValueType>> check) {
|
||||
const auto& start = std::chrono::high_resolution_clock::now();
|
||||
for(int i = 0; i < benchmark_loops; i++) {
|
||||
for(const auto& check_item : check) {
|
||||
assert(check_item.second == hmap[check_item.first]);
|
||||
}
|
||||
}
|
||||
const auto& end = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration<double, std::nano>(end - start).count();
|
||||
return duration;
|
||||
}
|
||||
|
||||
double assess_vmap(const std::vector<std::pair<KeyType, ValueType>> check) {
|
||||
const auto& start = std::chrono::high_resolution_clock::now();
|
||||
for(int i = 0; i < benchmark_loops; i++) {
|
||||
for(const auto& check_item : check) {
|
||||
for(const auto& item : hmap) {
|
||||
if(check_item.first == item.first) {
|
||||
assert(check_item.second == item.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto& end = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration<double, std::nano>(end - start).count();
|
||||
return duration;
|
||||
}
|
||||
|
||||
public:
|
||||
smart_map () = default;
|
||||
|
||||
void assess_datastructure() {
|
||||
std::vector<std::pair<KeyType, ValueType>> vals(hmap.begin(), hmap.end());
|
||||
std::shuffle(vals.begin(), vals.end(), std::default_random_engine(570));
|
||||
use_hmap = assess_hmap(vals) > assess_vmap(vals);
|
||||
}
|
||||
|
||||
bool will_use_hmap() {
|
||||
return use_hmap;
|
||||
}
|
||||
|
||||
ValueType* get(const KeyType& key) {
|
||||
if(use_hmap) {
|
||||
if(hmap.find(key) != hmap.end()) {
|
||||
return &hmap[key];
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
for(auto& item : hmap) {
|
||||
if(item.first == key) {
|
||||
std::string& ref = item.second;
|
||||
return &ref;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void insert(const KeyType key, const ValueType value) {
|
||||
hmap[key] = value;
|
||||
}
|
||||
};
|
|
@ -6,7 +6,7 @@
|
|||
<h1>404 - Not Found</h1>
|
||||
<hr>
|
||||
<p>Anthracite/0.0.1</p>
|
||||
<p><small>Created by Nicholas Orlowsky </small> - <a href="https://github.com/nickorlow/anthracite"><small>This is Open Source Software</small></a></p>
|
||||
<p><small><a href="https://github.com/nickorlow/anthracite"><small>This is Open Source Software</small></a></p>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<h1>500 - Internal Server Error</h1>
|
||||
<hr>
|
||||
<p>Anthracite/0.0.1</p>
|
||||
<p><small>Created by Nicholas Orlowsky </small> - <a href="https://github.com/nickorlow/anthracite"><small>This is Open Source Software</small></a></p>
|
||||
<p><a href="https://github.com/nickorlow/anthracite"><small>This is Open Source Software</small></a></p>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
|
|
456
src/http.cpp
456
src/http.cpp
|
@ -1,456 +0,0 @@
|
|||
#include <arpa/inet.h>
|
||||
#include <malloc.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "socket.cpp"
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
|
||||
constexpr int HTTP_HEADER_BYTES = 8190;
|
||||
|
||||
enum http_method {
|
||||
GET,
|
||||
POST,
|
||||
DELETE,
|
||||
PUT,
|
||||
PATCH,
|
||||
HEAD,
|
||||
OPTIONS,
|
||||
CONNECT,
|
||||
TRACE,
|
||||
COPY,
|
||||
LINK,
|
||||
UNLINK,
|
||||
PURGE,
|
||||
LOCK,
|
||||
UNLOCK,
|
||||
PROPFIND,
|
||||
VIEW,
|
||||
UNKNOWN
|
||||
};
|
||||
|
||||
static unordered_map<string, http_method> const http_method_map = {
|
||||
{ "GET", http_method::GET },
|
||||
{ "POST", http_method::POST },
|
||||
{ "DELETE", http_method::DELETE },
|
||||
{ "PUT", http_method::PUT },
|
||||
{ "PATCH", http_method::PATCH },
|
||||
{ "HEAD", http_method::HEAD },
|
||||
{ "OPTIONS", http_method::OPTIONS },
|
||||
{ "CONNECT", http_method::CONNECT },
|
||||
{ "TRACE", http_method::TRACE },
|
||||
{ "COPY", http_method::COPY },
|
||||
{ "LINK", http_method::LINK },
|
||||
{ "UNLINK", http_method::UNLINK },
|
||||
{ "PURGE", http_method::PURGE },
|
||||
{ "LOCK", http_method::LOCK },
|
||||
{ "UNLOCK", http_method::UNLOCK },
|
||||
{ "PROPFIND", http_method::PROPFIND },
|
||||
{ "VIEW", http_method::VIEW },
|
||||
{ "UNKNOWN", http_method::UNKNOWN }
|
||||
};
|
||||
|
||||
static unordered_map<http_method, string> const http_reverse_method_map = {
|
||||
{ http_method::GET, "GET" },
|
||||
{ http_method::POST, "POST" },
|
||||
{ http_method::DELETE, "DELETE" },
|
||||
{ http_method::PUT, "PUT" },
|
||||
{ http_method::PATCH, "PATCH" },
|
||||
{ http_method::HEAD, "HEAD" },
|
||||
{ http_method::OPTIONS, "OPTIONS" },
|
||||
{ http_method::CONNECT, "CONNECT" },
|
||||
{ http_method::TRACE, "TRACE" },
|
||||
{ http_method::COPY, "COPY" },
|
||||
{ http_method::LINK, "LINK" },
|
||||
{ http_method::UNLINK, "UNLINK" },
|
||||
{ http_method::PURGE, "PURGE" },
|
||||
{ http_method::LOCK, "LOCK" },
|
||||
{ http_method::UNLOCK, "UNLOCK" },
|
||||
{ http_method::PROPFIND, "PROPFIND" },
|
||||
{ http_method::VIEW, "VIEW" },
|
||||
{ http_method::UNKNOWN, "UNKNOWN" }
|
||||
};
|
||||
|
||||
static unordered_map<int, string> const http_status_map = {
|
||||
{ 100, "CONTINUE" },
|
||||
{ 101, "SWITCHING PROTOCOLS" },
|
||||
{ 200, "OK" },
|
||||
{ 201, "CREATED" },
|
||||
{ 202, "ACCEPTED" },
|
||||
{ 203, "NON-AUTHORITATIVE INFORMATION" },
|
||||
{ 204, "NO CONTENT" },
|
||||
{ 205, "RESET CONTENT" },
|
||||
{ 206, "PARTIAL CONTENT" },
|
||||
{ 300, "MULTIPLE CHOICES" },
|
||||
{ 301, "MOVED PERMANENTLY" },
|
||||
{ 302, "FOUND" },
|
||||
{ 303, "SEE OTHER" },
|
||||
{ 304, "NOT MODIFIED" },
|
||||
{ 305, "USE PROXY" },
|
||||
{ 307, "TEMPORARY REDIRECT" },
|
||||
{ 400, "BAD REQUEST" },
|
||||
{ 401, "UNAUTHORIZED" },
|
||||
{ 402, "PAYMENT REQUIRED" },
|
||||
{ 403, "FORBIDDEN" },
|
||||
{ 404, "NOT FOUND" },
|
||||
{ 405, "METHOD NOT ALLOWED" },
|
||||
{ 406, "NOT ACCEPTABLE" },
|
||||
{ 407, "PROXY AUTHENTICATION REQUIRED" },
|
||||
{ 408, "REQUEST TIMEOUT" },
|
||||
{ 409, "CONFLICT" },
|
||||
{ 410, "GONE" },
|
||||
{ 411, "LENGTH REQUIRED" },
|
||||
{ 412, "PRECONDITION FAILED" },
|
||||
{ 413, "PAYLOAD TOO LARGE" },
|
||||
{ 414, "URI TOO LONG" },
|
||||
{ 415, "UNSUPPORTED MEDIA TYPE" },
|
||||
{ 416, "RANGE NOT SATISFIABLE" },
|
||||
{ 417, "EXPECTATION FAILED" },
|
||||
{ 418, "I'M A TEAPOT" },
|
||||
{ 421, "MISDIRECTED REQUEST" },
|
||||
{ 422, "UNPROCESSABLE ENTITY" },
|
||||
{ 423, "LOCKED" },
|
||||
{ 424, "FAILED DEPENDENCY" },
|
||||
{ 426, "UPGRADE REQUIRED" },
|
||||
{ 428, "PRECONDITION REQUIRED" },
|
||||
{ 429, "TOO MANY REQUESTS" },
|
||||
{ 431, "REQUEST HEADER FIELDS TOO LARGE" },
|
||||
{ 451, "UNAVAILABLE FOR LEGAL REASONS" },
|
||||
{ 500, "INTERNAL SERVER ERROR" },
|
||||
{ 501, "NOT IMPLEMENTED" },
|
||||
{ 502, "BAD GATEWAY" },
|
||||
{ 503, "SERVICE UNAVAILABLE" },
|
||||
{ 504, "GATEWAY TIMEOUT" },
|
||||
{ 505, "HTTP VERSION NOT SUPPORTED" },
|
||||
{ 506, "VARIANT ALSO NEGOTIATES" },
|
||||
{ 507, "INSUFFICIENT STORAGE" },
|
||||
{ 508, "LOOP DETECTED" },
|
||||
{ 510, "NOT EXTENDED" },
|
||||
{ 511, "NETWORK AUTHENTICATION REQUIRED" },
|
||||
{ 420, "ENHANCE YOUR CALM" }
|
||||
};
|
||||
|
||||
static unordered_map<std::string, std::string> const mime_types = {
|
||||
{ "html", "text/html" },
|
||||
{ "css", "text/css" },
|
||||
|
||||
{ "js", "application/javascript" },
|
||||
{ "pdf", "application/pdf" },
|
||||
|
||||
{ "ico", "image/x-icon" },
|
||||
{ "jpg", "image/jpeg" },
|
||||
{ "jpeg", "image/jpeg" },
|
||||
{ "png", "image/png" },
|
||||
{ "gif", "image/gif" },
|
||||
{ "bmp", "image/bmp" },
|
||||
|
||||
{ "mp4", "video/mp4" },
|
||||
{ "avi", "video/x-msvideo" },
|
||||
{ "mkv", "video/x-matroska" },
|
||||
{ "mov", "video/quicktime" },
|
||||
{ "wmv", "video/x-ms-wmv" },
|
||||
};
|
||||
|
||||
class name_value {
|
||||
private:
|
||||
string _name;
|
||||
string _value;
|
||||
|
||||
protected:
|
||||
name_value() {}
|
||||
|
||||
public:
|
||||
name_value(string name, string value)
|
||||
: _name(std::move(name))
|
||||
, _value(std::move(value))
|
||||
{
|
||||
}
|
||||
virtual ~name_value() = default;
|
||||
name_value(const name_value&) = default;
|
||||
name_value& operator=(name_value const&) = default;
|
||||
name_value(name_value&&) = default;
|
||||
name_value& operator=(name_value&&) = default;
|
||||
|
||||
string name() { return _name; }
|
||||
string value() { return _value; }
|
||||
|
||||
virtual string to_string() { return ""; }
|
||||
};
|
||||
|
||||
class http_header : public name_value {
|
||||
public:
|
||||
http_header()
|
||||
: name_value()
|
||||
{
|
||||
}
|
||||
http_header(string name, string value)
|
||||
: name_value(name, value)
|
||||
{
|
||||
}
|
||||
|
||||
string to_string() override { return name() + ": " + value() + "\r\n"; }
|
||||
};
|
||||
|
||||
class query_param : public name_value {
|
||||
public:
|
||||
query_param()
|
||||
: name_value()
|
||||
{
|
||||
}
|
||||
query_param(string name, string value)
|
||||
: name_value(name, value)
|
||||
{
|
||||
}
|
||||
|
||||
string to_string() override { return name() + "=" + value(); }
|
||||
};
|
||||
|
||||
enum http_version { HTTP_0_9,
|
||||
HTTP_1_0,
|
||||
HTTP_1_1,
|
||||
HTTP_2_0,
|
||||
HTTP_3_0 };
|
||||
|
||||
static std::unordered_map<std::string, http_version> const http_version_map = {
|
||||
{ "HTTP/0.9", HTTP_0_9 },
|
||||
{ "HTTP/1.0", HTTP_1_0 },
|
||||
{ "HTTP/1.1", HTTP_1_1 },
|
||||
{ "HTTP/2.0", HTTP_2_0 },
|
||||
{ "HTTP/3.0", HTTP_3_0 }
|
||||
};
|
||||
|
||||
static std::unordered_map<http_version, std::string> const http_reverse_version_map = {
|
||||
{ HTTP_0_9, "HTTP/0.9" },
|
||||
{ HTTP_1_0, "HTTP/1.0" },
|
||||
{ HTTP_1_1, "HTTP/1.1" },
|
||||
{ HTTP_2_0, "HTTP/2.0" },
|
||||
{ HTTP_3_0, "HTTP/3.0" }
|
||||
};
|
||||
|
||||
class http_request {
|
||||
private:
|
||||
enum parser_state { METHOD,
|
||||
PATH,
|
||||
QUERY_PARAM_NAME,
|
||||
QUERY_PARAM_VALUE,
|
||||
VERSION,
|
||||
HEADER_NAME,
|
||||
HEADER_VALUE,
|
||||
BODY_CONTENT };
|
||||
http_method _method;
|
||||
http_version _http_version;
|
||||
string _path;
|
||||
string _client_ipaddr;
|
||||
string _body_content;
|
||||
unordered_map<string, http_header> _headers; // kinda goofy, whatever
|
||||
unordered_map<string, query_param> _query_params; // kinda goofy, whatever
|
||||
|
||||
public:
|
||||
http_request(anthracite_socket& s)
|
||||
: _path("")
|
||||
{
|
||||
string raw_data = s.recv_message(HTTP_HEADER_BYTES);
|
||||
_client_ipaddr = s.get_client_ip();
|
||||
|
||||
parser_state state = METHOD;
|
||||
|
||||
string scratch = "";
|
||||
string scratch_2 = "";
|
||||
for (int i = 0; i < raw_data.length(); i++) {
|
||||
switch (state) {
|
||||
case METHOD: {
|
||||
if (raw_data[i] == ' ') {
|
||||
if (http_method_map.find(scratch) == http_method_map.end()) {
|
||||
_method = http_method::UNKNOWN;
|
||||
} else {
|
||||
_method = http_method_map.find(scratch)->second;
|
||||
}
|
||||
scratch = "";
|
||||
state = PATH;
|
||||
} else {
|
||||
scratch += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case PATH: {
|
||||
switch (raw_data[i]) {
|
||||
case ' ':
|
||||
state = VERSION;
|
||||
break;
|
||||
case '?':
|
||||
state = QUERY_PARAM_NAME;
|
||||
break;
|
||||
default:
|
||||
_path += raw_data[i];
|
||||
break;
|
||||
}
|
||||
} break;
|
||||
|
||||
case QUERY_PARAM_NAME: {
|
||||
if (raw_data[i] == ' ') {
|
||||
scratch = "";
|
||||
state = VERSION;
|
||||
} else if (raw_data[i] == '=') {
|
||||
state = QUERY_PARAM_VALUE;
|
||||
} else {
|
||||
scratch += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case QUERY_PARAM_VALUE: {
|
||||
if (raw_data[i] == ' ') {
|
||||
_query_params[scratch] = query_param(scratch, scratch_2);
|
||||
scratch = "";
|
||||
scratch_2 = "";
|
||||
state = VERSION;
|
||||
} else if (raw_data[i] == '&') {
|
||||
_query_params[scratch] = query_param(scratch, scratch_2);
|
||||
scratch = "";
|
||||
scratch_2 = "";
|
||||
state = QUERY_PARAM_NAME;
|
||||
} else {
|
||||
scratch_2 += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case VERSION: {
|
||||
if (raw_data[i] == '\n') {
|
||||
_http_version = http_version_map.find(scratch)->second;
|
||||
scratch = "";
|
||||
state = HEADER_NAME;
|
||||
} else if (raw_data[i] != '\r') {
|
||||
scratch += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case HEADER_NAME: {
|
||||
if (raw_data[i] == '\n') {
|
||||
scratch = "";
|
||||
scratch_2 = "";
|
||||
state = BODY_CONTENT;
|
||||
break;
|
||||
} else if (raw_data[i] == ' ') {
|
||||
scratch = "";
|
||||
cout << "Error: Whitespace found in header name\n";
|
||||
break;
|
||||
} else if (raw_data[i] == ':') {
|
||||
state = HEADER_VALUE;
|
||||
i++;
|
||||
} else {
|
||||
scratch += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case HEADER_VALUE: {
|
||||
if (raw_data[i] == '\n') {
|
||||
_headers[scratch] = http_header(scratch, scratch_2);
|
||||
scratch = "";
|
||||
scratch_2 = "";
|
||||
state = HEADER_NAME;
|
||||
} else if (raw_data[i] != '\r') {
|
||||
scratch_2 += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case BODY_CONTENT: {
|
||||
_body_content += raw_data[i];
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string path() { return _path; }
|
||||
|
||||
http_method method() { return _method; }
|
||||
|
||||
string client_ip() { return _client_ipaddr; }
|
||||
|
||||
string to_string()
|
||||
{
|
||||
string response = "";
|
||||
response += http_reverse_method_map.find(_method)->second + " " + _path + "?";
|
||||
|
||||
for (auto qp : _query_params) {
|
||||
response += qp.second.to_string() + "&";
|
||||
}
|
||||
|
||||
response += " " + http_reverse_version_map.find(_http_version)->second + "\r\n";
|
||||
|
||||
for (auto header : _headers) {
|
||||
response += header.second.to_string();
|
||||
}
|
||||
|
||||
response += "\r\n";
|
||||
response += _body_content;
|
||||
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
class http_response {
|
||||
private:
|
||||
int _status_code;
|
||||
string& _content;
|
||||
string _filename;
|
||||
unordered_map<string, http_header> _headers; // kinda goofy, whatever
|
||||
|
||||
public:
|
||||
http_response(string& content, string filename, int status_code = 200)
|
||||
: _content(content)
|
||||
, _status_code(status_code)
|
||||
, _filename(std::move(filename))
|
||||
{
|
||||
}
|
||||
|
||||
int status_code() { return _status_code; }
|
||||
|
||||
void add_header(http_header header, bool override_existing = true)
|
||||
{
|
||||
if (override_existing || _headers.find(header.name()) == _headers.end()) {
|
||||
_headers[header.name()] = header;
|
||||
}
|
||||
}
|
||||
|
||||
string& content()
|
||||
{
|
||||
return _content;
|
||||
}
|
||||
|
||||
string header_to_string()
|
||||
{
|
||||
string response = "";
|
||||
response += "HTTP/1.1 " + ::to_string(_status_code) + " " + http_status_map.find(_status_code)->second + "\r\n";
|
||||
string content_type = "text/html";
|
||||
string file_extension = _filename.substr(_filename.rfind('.') + 1);
|
||||
auto mime_type = mime_types.find(file_extension);
|
||||
if (mime_type != mime_types.end()) {
|
||||
content_type = mime_type->second;
|
||||
}
|
||||
add_header(http_header("Content-Type", content_type), false);
|
||||
add_header(http_header("Content-Length", ::to_string(_content.length())), false);
|
||||
add_header(http_header("Server", "Anthracite/0.0.1"), false);
|
||||
add_header(http_header("Origin-Server", "Anthracite/0.0.1"), false);
|
||||
|
||||
for (auto header : _headers) {
|
||||
response += header.second.to_string();
|
||||
}
|
||||
|
||||
response += "\r\n";
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
string to_string()
|
||||
{
|
||||
return header_to_string() + _content;
|
||||
}
|
||||
};
|
173
src/http/constants.cpp
Normal file
173
src/http/constants.cpp
Normal file
|
@ -0,0 +1,173 @@
|
|||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
constexpr int HTTP_HEADER_BYTES = 8190;
|
||||
|
||||
enum http_method {
|
||||
GET,
|
||||
POST,
|
||||
DELETE,
|
||||
PUT,
|
||||
PATCH,
|
||||
HEAD,
|
||||
OPTIONS,
|
||||
CONNECT,
|
||||
TRACE,
|
||||
COPY,
|
||||
LINK,
|
||||
UNLINK,
|
||||
PURGE,
|
||||
LOCK,
|
||||
UNLOCK,
|
||||
PROPFIND,
|
||||
VIEW,
|
||||
UNKNOWN
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, http_method> const http_method_map = {
|
||||
{ "GET", http_method::GET },
|
||||
{ "POST", http_method::POST },
|
||||
{ "DELETE", http_method::DELETE },
|
||||
{ "PUT", http_method::PUT },
|
||||
{ "PATCH", http_method::PATCH },
|
||||
{ "HEAD", http_method::HEAD },
|
||||
{ "OPTIONS", http_method::OPTIONS },
|
||||
{ "CONNECT", http_method::CONNECT },
|
||||
{ "TRACE", http_method::TRACE },
|
||||
{ "COPY", http_method::COPY },
|
||||
{ "LINK", http_method::LINK },
|
||||
{ "UNLINK", http_method::UNLINK },
|
||||
{ "PURGE", http_method::PURGE },
|
||||
{ "LOCK", http_method::LOCK },
|
||||
{ "UNLOCK", http_method::UNLOCK },
|
||||
{ "PROPFIND", http_method::PROPFIND },
|
||||
{ "VIEW", http_method::VIEW },
|
||||
{ "UNKNOWN", http_method::UNKNOWN }
|
||||
};
|
||||
|
||||
static std::unordered_map<http_method, std::string> const http_reverse_method_map = {
|
||||
{ http_method::GET, "GET" },
|
||||
{ http_method::POST, "POST" },
|
||||
{ http_method::DELETE, "DELETE" },
|
||||
{ http_method::PUT, "PUT" },
|
||||
{ http_method::PATCH, "PATCH" },
|
||||
{ http_method::HEAD, "HEAD" },
|
||||
{ http_method::OPTIONS, "OPTIONS" },
|
||||
{ http_method::CONNECT, "CONNECT" },
|
||||
{ http_method::TRACE, "TRACE" },
|
||||
{ http_method::COPY, "COPY" },
|
||||
{ http_method::LINK, "LINK" },
|
||||
{ http_method::UNLINK, "UNLINK" },
|
||||
{ http_method::PURGE, "PURGE" },
|
||||
{ http_method::LOCK, "LOCK" },
|
||||
{ http_method::UNLOCK, "UNLOCK" },
|
||||
{ http_method::PROPFIND, "PROPFIND" },
|
||||
{ http_method::VIEW, "VIEW" },
|
||||
{ http_method::UNKNOWN, "UNKNOWN" }
|
||||
};
|
||||
|
||||
static std::unordered_map<int, std::string> const http_status_map = {
|
||||
{ 100, "CONTINUE" },
|
||||
{ 101, "SWITCHING PROTOCOLS" },
|
||||
{ 200, "OK" },
|
||||
{ 201, "CREATED" },
|
||||
{ 202, "ACCEPTED" },
|
||||
{ 203, "NON-AUTHORITATIVE INFORMATION" },
|
||||
{ 204, "NO CONTENT" },
|
||||
{ 205, "RESET CONTENT" },
|
||||
{ 206, "PARTIAL CONTENT" },
|
||||
{ 300, "MULTIPLE CHOICES" },
|
||||
{ 301, "MOVED PERMANENTLY" },
|
||||
{ 302, "FOUND" },
|
||||
{ 303, "SEE OTHER" },
|
||||
{ 304, "NOT MODIFIED" },
|
||||
{ 305, "USE PROXY" },
|
||||
{ 307, "TEMPORARY REDIRECT" },
|
||||
{ 400, "BAD REQUEST" },
|
||||
{ 401, "UNAUTHORIZED" },
|
||||
{ 402, "PAYMENT REQUIRED" },
|
||||
{ 403, "FORBIDDEN" },
|
||||
{ 404, "NOT FOUND" },
|
||||
{ 405, "METHOD NOT ALLOWED" },
|
||||
{ 406, "NOT ACCEPTABLE" },
|
||||
{ 407, "PROXY AUTHENTICATION REQUIRED" },
|
||||
{ 408, "REQUEST TIMEOUT" },
|
||||
{ 409, "CONFLICT" },
|
||||
{ 410, "GONE" },
|
||||
{ 411, "LENGTH REQUIRED" },
|
||||
{ 412, "PRECONDITION FAILED" },
|
||||
{ 413, "PAYLOAD TOO LARGE" },
|
||||
{ 414, "URI TOO LONG" },
|
||||
{ 415, "UNSUPPORTED MEDIA TYPE" },
|
||||
{ 416, "RANGE NOT SATISFIABLE" },
|
||||
{ 417, "EXPECTATION FAILED" },
|
||||
{ 418, "I'M A TEAPOT" },
|
||||
{ 421, "MISDIRECTED REQUEST" },
|
||||
{ 422, "UNPROCESSABLE ENTITY" },
|
||||
{ 423, "LOCKED" },
|
||||
{ 424, "FAILED DEPENDENCY" },
|
||||
{ 426, "UPGRADE REQUIRED" },
|
||||
{ 428, "PRECONDITION REQUIRED" },
|
||||
{ 429, "TOO MANY REQUESTS" },
|
||||
{ 431, "REQUEST HEADER FIELDS TOO LARGE" },
|
||||
{ 451, "UNAVAILABLE FOR LEGAL REASONS" },
|
||||
{ 500, "INTERNAL SERVER ERROR" },
|
||||
{ 501, "NOT IMPLEMENTED" },
|
||||
{ 502, "BAD GATEWAY" },
|
||||
{ 503, "SERVICE UNAVAILABLE" },
|
||||
{ 504, "GATEWAY TIMEOUT" },
|
||||
{ 505, "HTTP VERSION NOT SUPPORTED" },
|
||||
{ 506, "VARIANT ALSO NEGOTIATES" },
|
||||
{ 507, "INSUFFICIENT STORAGE" },
|
||||
{ 508, "LOOP DETECTED" },
|
||||
{ 510, "NOT EXTENDED" },
|
||||
{ 511, "NETWORK AUTHENTICATION REQUIRED" },
|
||||
{ 420, "ENHANCE YOUR CALM" }
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, std::string> const mime_types = {
|
||||
{ "html", "text/html" },
|
||||
{ "css", "text/css" },
|
||||
|
||||
{ "js", "application/javascript" },
|
||||
{ "pdf", "application/pdf" },
|
||||
|
||||
{ "ico", "image/x-icon" },
|
||||
{ "jpg", "image/jpeg" },
|
||||
{ "jpeg", "image/jpeg" },
|
||||
{ "png", "image/png" },
|
||||
{ "gif", "image/gif" },
|
||||
{ "bmp", "image/bmp" },
|
||||
|
||||
{ "mp4", "video/mp4" },
|
||||
{ "avi", "video/x-msvideo" },
|
||||
{ "mkv", "video/x-matroska" },
|
||||
{ "mov", "video/quicktime" },
|
||||
{ "wmv", "video/x-ms-wmv" },
|
||||
};
|
||||
|
||||
enum http_version { HTTP_0_9,
|
||||
HTTP_1_0,
|
||||
HTTP_1_1,
|
||||
HTTP_2_0,
|
||||
HTTP_3_0 };
|
||||
|
||||
static std::unordered_map<std::string, http_version> const http_version_map = {
|
||||
{ "HTTP/0.9", HTTP_0_9 },
|
||||
{ "HTTP/1.0", HTTP_1_0 },
|
||||
{ "HTTP/1.1", HTTP_1_1 },
|
||||
{ "HTTP/2.0", HTTP_2_0 },
|
||||
{ "HTTP/3.0", HTTP_3_0 }
|
||||
};
|
||||
|
||||
static std::unordered_map<http_version, std::string> const http_reverse_version_map = {
|
||||
{ HTTP_0_9, "HTTP/0.9" },
|
||||
{ HTTP_1_0, "HTTP/1.0" },
|
||||
{ HTTP_1_1, "HTTP/1.1" },
|
||||
{ HTTP_2_0, "HTTP/2.0" },
|
||||
{ HTTP_3_0, "HTTP/3.0" }
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
55
src/http/header_query.cpp
Normal file
55
src/http/header_query.cpp
Normal file
|
@ -0,0 +1,55 @@
|
|||
#include <string>
|
||||
|
||||
class name_value {
|
||||
private:
|
||||
std::string _name;
|
||||
std::string _value;
|
||||
|
||||
protected:
|
||||
name_value() {}
|
||||
|
||||
public:
|
||||
name_value(std::string name, std::string value)
|
||||
: _name(std::move(name))
|
||||
, _value(std::move(value))
|
||||
{
|
||||
}
|
||||
virtual ~name_value() = default;
|
||||
name_value(const name_value&) = default;
|
||||
name_value& operator=(name_value const&) = default;
|
||||
name_value(name_value&&) = default;
|
||||
name_value& operator=(name_value&&) = default;
|
||||
|
||||
std::string name() { return _name; }
|
||||
std::string value() { return _value; }
|
||||
|
||||
virtual std::string to_string() { return ""; }
|
||||
};
|
||||
|
||||
class http_header : public name_value {
|
||||
public:
|
||||
http_header()
|
||||
: name_value()
|
||||
{
|
||||
}
|
||||
http_header(std::string name, std::string value)
|
||||
: name_value(name, value)
|
||||
{
|
||||
}
|
||||
|
||||
std::string to_string() override { return name() + ": " + value() + "\r\n"; }
|
||||
};
|
||||
|
||||
class query_param : public name_value {
|
||||
public:
|
||||
query_param()
|
||||
: name_value()
|
||||
{
|
||||
}
|
||||
query_param(std::string name, std::string value)
|
||||
: name_value(name, value)
|
||||
{
|
||||
}
|
||||
|
||||
std::string to_string() override { return name() + "=" + value(); }
|
||||
};
|
7
src/http/http.hpp
Normal file
7
src/http/http.hpp
Normal file
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "constants.cpp"
|
||||
#include "header_query.cpp"
|
||||
#include "../socket.cpp"
|
159
src/http/http_request.cpp
Normal file
159
src/http/http_request.cpp
Normal file
|
@ -0,0 +1,159 @@
|
|||
#include "http.hpp"
|
||||
|
||||
class http_request {
|
||||
private:
|
||||
enum parser_state { METHOD,
|
||||
PATH,
|
||||
QUERY_PARAM_NAME,
|
||||
QUERY_PARAM_VALUE,
|
||||
VERSION,
|
||||
HEADER_NAME,
|
||||
HEADER_VALUE,
|
||||
BODY_CONTENT };
|
||||
http_method _method;
|
||||
http_version _http_version;
|
||||
std::string _path;
|
||||
std::string _client_ipaddr;
|
||||
std::string _body_content;
|
||||
std::unordered_map<std::string, http_header> _headers; // kinda goofy, whatever
|
||||
std::unordered_map<std::string, query_param> _query_params; // kinda goofy, whatever
|
||||
|
||||
public:
|
||||
http_request(anthracite_socket& s)
|
||||
: _path(""), _client_ipaddr(s.get_client_ip())
|
||||
{
|
||||
std::string raw_data = s.recv_message(HTTP_HEADER_BYTES);
|
||||
|
||||
parser_state state = METHOD;
|
||||
|
||||
std::string scratch = "";
|
||||
std::string scratch_2 = "";
|
||||
for (int i = 0; i < raw_data.length(); i++) {
|
||||
switch (state) {
|
||||
case METHOD: {
|
||||
if (raw_data[i] == ' ') {
|
||||
if (http_method_map.find(scratch) == http_method_map.end()) {
|
||||
_method = http_method::UNKNOWN;
|
||||
} else {
|
||||
_method = http_method_map.find(scratch)->second;
|
||||
}
|
||||
scratch = "";
|
||||
state = PATH;
|
||||
} else {
|
||||
scratch += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case PATH: {
|
||||
switch (raw_data[i]) {
|
||||
case ' ':
|
||||
state = VERSION;
|
||||
break;
|
||||
case '?':
|
||||
state = QUERY_PARAM_NAME;
|
||||
break;
|
||||
default:
|
||||
_path += raw_data[i];
|
||||
break;
|
||||
}
|
||||
} break;
|
||||
|
||||
case QUERY_PARAM_NAME: {
|
||||
if (raw_data[i] == ' ') {
|
||||
scratch = "";
|
||||
state = VERSION;
|
||||
} else if (raw_data[i] == '=') {
|
||||
state = QUERY_PARAM_VALUE;
|
||||
} else {
|
||||
scratch += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case QUERY_PARAM_VALUE: {
|
||||
if (raw_data[i] == ' ') {
|
||||
_query_params[scratch] = query_param(scratch, scratch_2);
|
||||
scratch = "";
|
||||
scratch_2 = "";
|
||||
state = VERSION;
|
||||
} else if (raw_data[i] == '&') {
|
||||
_query_params[scratch] = query_param(scratch, scratch_2);
|
||||
scratch = "";
|
||||
scratch_2 = "";
|
||||
state = QUERY_PARAM_NAME;
|
||||
} else {
|
||||
scratch_2 += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case VERSION: {
|
||||
if (raw_data[i] == '\n') {
|
||||
_http_version = http_version_map.find(scratch)->second;
|
||||
scratch = "";
|
||||
state = HEADER_NAME;
|
||||
} else if (raw_data[i] != '\r') {
|
||||
scratch += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case HEADER_NAME: {
|
||||
if (raw_data[i] == '\n') {
|
||||
scratch = "";
|
||||
scratch_2 = "";
|
||||
state = BODY_CONTENT;
|
||||
break;
|
||||
} else if (raw_data[i] == ' ') {
|
||||
scratch = "";
|
||||
break;
|
||||
} else if (raw_data[i] == ':') {
|
||||
state = HEADER_VALUE;
|
||||
i++;
|
||||
} else {
|
||||
scratch += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case HEADER_VALUE: {
|
||||
if (raw_data[i] == '\n') {
|
||||
_headers[scratch] = http_header(scratch, scratch_2);
|
||||
scratch = "";
|
||||
scratch_2 = "";
|
||||
state = HEADER_NAME;
|
||||
} else if (raw_data[i] != '\r') {
|
||||
scratch_2 += raw_data[i];
|
||||
}
|
||||
} break;
|
||||
|
||||
case BODY_CONTENT: {
|
||||
_body_content += raw_data[i];
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string path() { return _path; }
|
||||
|
||||
http_method method() { return _method; }
|
||||
|
||||
std::string client_ip() { return _client_ipaddr; }
|
||||
|
||||
std::string to_string()
|
||||
{
|
||||
std::string response = "";
|
||||
response += http_reverse_method_map.find(_method)->second + " " + _path + "?";
|
||||
|
||||
for (auto qp : _query_params) {
|
||||
response += qp.second.to_string() + "&";
|
||||
}
|
||||
|
||||
response += " " + http_reverse_version_map.find(_http_version)->second + "\r\n";
|
||||
|
||||
for (auto header : _headers) {
|
||||
response += header.second.to_string();
|
||||
}
|
||||
|
||||
response += "\r\n";
|
||||
response += _body_content;
|
||||
|
||||
return response;
|
||||
}
|
||||
};
|
60
src/http/http_response.cpp
Normal file
60
src/http/http_response.cpp
Normal file
|
@ -0,0 +1,60 @@
|
|||
#include "http.hpp"
|
||||
|
||||
class http_response {
|
||||
private:
|
||||
int _status_code;
|
||||
std::string& _content;
|
||||
std::string _filename;
|
||||
std::unordered_map<std::string, http_header> _headers; // kinda goofy, whatever
|
||||
|
||||
public:
|
||||
http_response(std::string& content, std::string filename, int status_code = 200)
|
||||
: _content(content)
|
||||
, _status_code(status_code)
|
||||
, _filename(std::move(filename))
|
||||
{
|
||||
}
|
||||
|
||||
int status_code() { return _status_code; }
|
||||
|
||||
void add_header(http_header header, bool override_existing = true)
|
||||
{
|
||||
if (override_existing || _headers.find(header.name()) == _headers.end()) {
|
||||
_headers[header.name()] = header;
|
||||
}
|
||||
}
|
||||
|
||||
std::string& content()
|
||||
{
|
||||
return _content;
|
||||
}
|
||||
|
||||
std::string header_to_string()
|
||||
{
|
||||
std::string response = "";
|
||||
response += "HTTP/1.1 " + std::to_string(_status_code) + " " + http_status_map.find(_status_code)->second + "\r\n";
|
||||
std::string content_type = "text/html";
|
||||
std::string file_extension = _filename.substr(_filename.rfind('.') + 1);
|
||||
auto mime_type = mime_types.find(file_extension);
|
||||
if (mime_type != mime_types.end()) {
|
||||
content_type = mime_type->second;
|
||||
}
|
||||
add_header(http_header("Content-Type", content_type), false);
|
||||
add_header(http_header("Content-Length", std::to_string(_content.length())), false);
|
||||
add_header(http_header("Server", "Anthracite/0.0.1"), false);
|
||||
add_header(http_header("Origin-Server", "Anthracite/0.0.1"), false);
|
||||
|
||||
for (auto header : _headers) {
|
||||
response += header.second.to_string();
|
||||
}
|
||||
|
||||
response += "\r\n";
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
std::string to_string()
|
||||
{
|
||||
return header_to_string() + _content;
|
||||
}
|
||||
};
|
24
src/main.cpp
24
src/main.cpp
|
@ -11,23 +11,21 @@
|
|||
#include <unistd.h>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace std;
|
||||
|
||||
void log_request_and_response(http_request& req, unique_ptr<http_response>& resp);
|
||||
void log_request_and_response(http_request& req, std::unique_ptr<http_response>& resp);
|
||||
|
||||
constexpr int default_port = 80;
|
||||
constexpr int max_worker_threads = 128;
|
||||
|
||||
int active_threads = 0;
|
||||
mutex mtx;
|
||||
condition_variable cv;
|
||||
std::mutex mtx;
|
||||
std::condition_variable cv;
|
||||
|
||||
void handle_client(anthracite_socket s, file_backend& fb)
|
||||
{
|
||||
http_request req(s);
|
||||
unique_ptr<http_response> resp = fb.handle_request(req);
|
||||
std::unique_ptr<http_response> resp = fb.handle_request(req);
|
||||
log_request_and_response(req, resp);
|
||||
string header = resp->header_to_string();
|
||||
std::string header = resp->header_to_string();
|
||||
s.send_message(header);
|
||||
s.send_message(resp->content());
|
||||
resp.reset();
|
||||
|
@ -47,24 +45,24 @@ int main(int argc, char** argv)
|
|||
port_number = atoi(argv[1]);
|
||||
}
|
||||
|
||||
cout << "Initializing Anthracite" << endl;
|
||||
std::cout << "Initializing Anthracite" << std::endl;
|
||||
anthracite_socket s(port_number);
|
||||
file_backend fb(argc > 2 ? argv[2] : "./www");
|
||||
cout << "Initialization Complete" << endl;
|
||||
cout << "Listening for HTTP connections on port " << port_number << endl;
|
||||
std::cout << "Initialization Complete" << std::endl;
|
||||
std::cout << "Listening for HTTP connections on port " << port_number << std::endl;
|
||||
|
||||
while (true) {
|
||||
s.wait_for_conn();
|
||||
std::unique_lock<std::mutex> lock(mtx);
|
||||
cv.wait(lock, [] { return active_threads < max_worker_threads; });
|
||||
active_threads++;
|
||||
thread(handle_client, s, ref(fb)).detach();
|
||||
std::thread(handle_client, s, std::ref(fb)).detach();
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
void log_request_and_response(http_request& req, unique_ptr<http_response>& resp)
|
||||
void log_request_and_response(http_request& req, std::unique_ptr<http_response>& resp)
|
||||
{
|
||||
cout << "[" << resp->status_code() << " " + http_status_map.find(resp->status_code())->second + "] " + req.client_ip() + " " + http_reverse_method_map.find(req.method())->second + " " + req.path() << endl;
|
||||
std::cout << "[" << resp->status_code() << " " + http_status_map.find(resp->status_code())->second + "] " + req.client_ip() + " " + http_reverse_method_map.find(req.method())->second + " " + req.path() << std::endl;
|
||||
}
|
||||
|
|
|
@ -9,13 +9,11 @@
|
|||
#include <unistd.h>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace std;
|
||||
|
||||
class anthracite_socket {
|
||||
private:
|
||||
int server_socket;
|
||||
int client_socket {};
|
||||
string client_ip;
|
||||
std::string client_ip;
|
||||
struct sockaddr_in client_addr {};
|
||||
socklen_t client_addr_len {};
|
||||
|
||||
|
@ -42,10 +40,10 @@ public:
|
|||
client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
|
||||
char ip_str[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &client_addr.sin_addr, ip_str, INET_ADDRSTRLEN);
|
||||
client_ip = string(ip_str);
|
||||
client_ip = std::string(ip_str);
|
||||
}
|
||||
|
||||
string get_client_ip()
|
||||
std::string get_client_ip()
|
||||
{
|
||||
return client_ip;
|
||||
}
|
||||
|
@ -56,7 +54,7 @@ public:
|
|||
client_socket = -1;
|
||||
}
|
||||
|
||||
void send_message(string& msg)
|
||||
void send_message(std::string& msg)
|
||||
{
|
||||
if (client_socket == -1) {
|
||||
return;
|
||||
|
@ -64,7 +62,7 @@ public:
|
|||
send(client_socket, &msg[0], msg.length(), 0);
|
||||
}
|
||||
|
||||
string recv_message(int buffer_size = 1024)
|
||||
std::string recv_message(int buffer_size = 1024)
|
||||
{
|
||||
if (client_socket == -1) {
|
||||
return "";
|
||||
|
@ -73,6 +71,6 @@ public:
|
|||
char response[buffer_size + 1];
|
||||
recv(client_socket, response, sizeof(response), 0);
|
||||
response[buffer_size] = '\0';
|
||||
return string(response);
|
||||
return std::string(response);
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue