diff options
| -rw-r--r-- | README.md | 45 | ||||
| -rw-r--r-- | build.ninja | 6 | ||||
| -rw-r--r-- | maple/gemini.cc | 53 | ||||
| -rw-r--r-- | maple/gemini.hh | 35 | ||||
| -rw-r--r-- | maple/maple.cc | 327 | ||||
| -rw-r--r-- | maple/maple.hh | 35 | ||||
| -rw-r--r-- | maple/titan.cc | 168 | ||||
| -rw-r--r-- | maple/titan.hh | 40 |
8 files changed, 588 insertions, 121 deletions
@@ -1,7 +1,17 @@ # Maple -A very simple static Gemini server; written within a single file and liberally -spanning 152 lines-of-code. +A very simple static Gemini server, now with Titan support! + +### Lines-of-code +This codebase is now 351 lines of lines-of-code! + +The statement "... written within a single file and liberally +spanning 159 lines-of-code." has been removed. + +If you would still like the minimal 159-LOC experience, checkout +[49ce0f8](https://github.com/gemrest/maple/tree/49ce0f83b8abd1af4760e56c1673c6997ef8a2c4)! +(Docker tag `0.1.4`) Do note, that version of this project is outdated and only +has Gemini support. ## Usage @@ -24,6 +34,34 @@ Gemini content must be placed in a directory -- directly adjacent to the Docker Compose file or executable -- named [`.maple/gmi`](.maple/gmi) and ending with the file extension `.gmi`. +### Environment Variables + +Maple can be configured with three optional environment variables: + +#### `TITAN` + +Enable Titan support. + +Takes a value of either `true` or `1`, case-insensitive. + +Defaults to off. + +#### `TITAN_TOKEN` + +Set a token to restrict Titan access. + +Takes a string value, case-sensitive. + +Defaults to no token, **anyone can upload!** + +#### `TITAN_MAX_SIZE` + +The maximum size in bytes which a Titan upload is permitted to perform. + +Takes an integer. + +Default to `1024`. + ### Docker Compose A Docker Compose file is already setup [here](./docker-compose.yaml). The only @@ -35,7 +73,8 @@ field which should be modified is the `ports`, if you have to. ### Executable 1. Build: `ninja` (requires [Ninja](https://ninja-build.org/)) -2. Run: `out/maple` +2. Run: `out/maple`, or + `TITAN=1 TITAN_TOKEN=secret TITAN_MAX_SIZE=2048 out/maple` ### Hacking diff --git a/build.ninja b/build.ninja index b510ec3..56ac2fe 100644 --- a/build.ninja +++ b/build.ninja @@ -11,8 +11,10 @@ rule compile rule link command = $cc $ldflags $in -o $out -build $out_dir/$name.o: compile $src_dir/$name.cc +build $out_dir/$name.o: compile $src_dir/$name.cc +build $out_dir/gemini.o: compile $src_dir/gemini.cc +build $out_dir/titan.o: compile $src_dir/titan.cc -build $out_dir/$name: link $out_dir/$name.o +build $out_dir/$name: link $out_dir/$name.o $out_dir/gemini.o $out_dir/titan.o default $out_dir/$name diff --git a/maple/gemini.cc b/maple/gemini.cc new file mode 100644 index 0000000..af6d194 --- /dev/null +++ b/maple/gemini.cc @@ -0,0 +1,53 @@ +/* + * This file is part of Maple <https://github.com/gemrest/maple>. + * Copyright (C) 2022-2022 Fuwn <[email protected]> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright (C) 2022-2022 Fuwn <[email protected]> + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include <fstream> +#include <iostream> + +#include "gemini.hh" + +namespace maple::gemini { + auto handle_client( + std::vector<std::string> gemini_files, + std::string path, + std::stringstream &response + ) -> void { + // Check if the route is a file being served + if (std::find( + gemini_files.begin(), + gemini_files.end(), + ".maple/gmi" + path + ) != gemini_files.end()) { + // If the route is a file being served; get the file contents + response << "20 text/gemini\r\n" + << std::ifstream(".maple/gmi" + path).rdbuf(); + } else { + if (path.empty() || path.at(path.length() - 1) == '/') { + response << "20 text/gemini\r\n" + << std::ifstream(".maple/gmi" + path + "index.gmi").rdbuf(); + } else { + response + << "51 The server (Maple) could not find the specified file.\r\n"; + } + } + + std::cout << "requested " << path << std::endl; + } +} diff --git a/maple/gemini.hh b/maple/gemini.hh new file mode 100644 index 0000000..35a23e6 --- /dev/null +++ b/maple/gemini.hh @@ -0,0 +1,35 @@ +/* + * This file is part of Maple <https://github.com/gemrest/maple>. + * Copyright (C) 2022-2022 Fuwn <[email protected]> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright (C) 2022-2022 Fuwn <[email protected]> + * SPDX-License-Identifier: GPL-3.0-only + */ + +#ifndef GEMINI_HH +#define GEMINI_HH + +#include <sstream> +#include <vector> + +namespace maple::gemini { + auto handle_client( + std::vector<std::string>, + std::string, + std::stringstream & + ) -> void; +} + +#endif // GEMINI_HH diff --git a/maple/maple.cc b/maple/maple.cc index 8a8d85a..ed434ab 100644 --- a/maple/maple.cc +++ b/maple/maple.cc @@ -21,31 +21,32 @@ #include <arpa/inet.h> #include <csignal> #include <filesystem> -#include <fstream> #include <iostream> +#include <map> #include <openssl/err.h> -#include <openssl/ssl.h> #include <sys/socket.h> #include <unistd.h> #include <vector> -static int maple_socket; -static SSL_CTX *ssl_context; - -auto exit_with[[noreturn]](const char *, bool) -> void; +#include "gemini.hh" +#include "maple.hh" +#include "titan.hh" auto main() -> int { - sockaddr_in socket_address {}; std::vector<std::string> gemini_files; - const std::string GEMINI_FILE_EXTENSION = "gmi"; - const std::string GEMINI_PROTOCOL = "gemini://"; + bool titan = false; + std::string titan_token; + size_t titan_max_size = 0; + + // Check if the user is want to support Titan and set it up + maple::setup_environment(titan, titan_token, titan_max_size); // Try a graceful shutdown when a SIGINT is detected - signal(SIGINT, [](int signal_) -> void { - std::cout << "shutdown(" << signal_ << ")" << std::endl; + signal(SIGINT, [](int _signal) -> void { + std::cout << "shutdown(" << _signal << ")" << std::endl; - close(maple_socket); - SSL_CTX_free(ssl_context); + close(maple::maple_socket); + SSL_CTX_free(maple::ssl_context); }); // Find and keep track of all Gemini files to serve @@ -60,10 +61,12 @@ auto main() -> int { if (std::equal( file_extension.begin(), file_extension.end(), - GEMINI_FILE_EXTENSION.begin(), - GEMINI_FILE_EXTENSION.end(), + std::string("gmi").begin(), + std::string("gmi").end(), [](char a, char b) -> bool { return std::tolower(a) == std::tolower(b); } - )) { gemini_files.push_back(entry.path()); } + )) { + gemini_files.push_back(entry.path()); + } } // Inform user of which files will be served @@ -71,48 +74,8 @@ auto main() -> int { std::cout << "serving " << file << std::endl; } - // Setup OpenSSL - SSL_library_init(); - SSL_load_error_strings(); - - ssl_context = SSL_CTX_new(TLS_server_method()); - if (!ssl_context) { exit_with("unable to create ssl context", true); } - - if (SSL_CTX_use_certificate_file( - ssl_context, - ".maple/public.pem", - SSL_FILETYPE_PEM - ) <= 0) { exit_with("unable to use certificate file", true); } - if (SSL_CTX_use_PrivateKey_file( - ssl_context, - ".maple/private.pem", - SSL_FILETYPE_PEM - ) <= 0) { exit_with("unable to use private key file", true); } - - socket_address.sin_family = AF_INET; - socket_address.sin_port = htons(1965); - socket_address.sin_addr.s_addr = htonl(INADDR_ANY); - - maple_socket = socket(AF_INET, SOCK_STREAM, 0); - if (maple_socket < 0) { exit_with("unable to create socket", false); } - - // Reuse address. Allows the use of the address instantly after a SIGINT - // without having to wait for the socket to die. - int reuse_addr = 1; - if (setsockopt( - maple_socket, - SOL_SOCKET, - SO_REUSEADDR, - &reuse_addr, - sizeof(int) - ) < 0) { exit_with("unable to set socket options (SO_LINGER)", false); } - - if (bind( - maple_socket, - reinterpret_cast<sockaddr *>(&socket_address), - sizeof(socket_address) - ) < 0) { exit_with("unable to bind", false); } - if (listen(maple_socket, 1) < 0) { exit_with("unable to listen", false); } + // Setup SSL + maple::setup_ssl(); // Listen and serve connections for (;;) { @@ -120,15 +83,15 @@ auto main() -> int { unsigned int socket_address_length = sizeof(socket_address_); SSL *ssl; int client = accept( - maple_socket, + maple::maple_socket, reinterpret_cast<sockaddr *>(&socket_address_), &socket_address_length ); - char request[1024]; - if (client < 0) { exit_with("unable to accept", false); } + if (client < 0) { maple::exit_with("unable to accept", false); } + + ssl = SSL_new(maple::ssl_context); - ssl = SSL_new(ssl_context); SSL_set_fd(ssl, client); if (SSL_accept(ssl) <= 0) { @@ -136,73 +99,87 @@ auto main() -> int { } else { std::stringstream response; size_t index_of_junk; - std::string protocol; + int request_scheme; // Gemini = 1, Titan = 2, Error = 0 + size_t bytes_read; + char request[1024]; - SSL_read(ssl, request, sizeof(request)); + SSL_read_ex(ssl, request, sizeof(request), &bytes_read); std::string path(request); - path = path.substr(0, path.size() - 2); // Remove "\r\n" - protocol = path.substr(0, 9); - path.erase(0, 9); // Remove "gemini://" - - // Try to remove the host, if you cannot; it must be a trailing slash-less - // hostname, so we will respond with the index. - size_t found_first = path.find_first_of('/'); - if (found_first != std::string::npos) { - path = path.substr(found_first, path.size() - 1); // Remove host + if (path.starts_with("gemini://")) { + request_scheme = 1; + } else if (path.starts_with("titan://")) { + request_scheme = 2; } else { - path = "/index.gmi"; + request_scheme = 0; } - // Remove junk, if any - index_of_junk = path.find_first_of('\n'); - if (index_of_junk != std::string::npos) { - path.erase(path.find_first_of('\n') - 1, path.size() - 1); - } + if (request_scheme != 0) { + path = path.substr(0, bytes_read); - // Check if the route is a file being served - if (std::find( - gemini_files.begin(), - gemini_files.end(), - ".maple/gmi" + path - ) != gemini_files.end()) { - // If the route is a file being served; get the file contents + // Remove "\r\n" if Gemini + if (request_scheme == 1) { + path = path.substr(0, path.size() - 2); + } - std::ifstream file(".maple/gmi" + path); - std::stringstream buffer; + if (request_scheme == 1) { + path.erase(0, 9); // Remove "gemini://" + } else { + path.erase(0, 8); // Remove "titan://" + } - buffer << file.rdbuf(); + // Try to remove the host, if you cannot; it must be a trailing + // slash-less hostname, so we will respond with the index. + size_t found_first = path.find_first_of('/'); - response << "20 text/gemini\r\n" << buffer.str(); - } else { - if (path.empty() || path.at(path.length() - 1) == '/') { - std::ifstream file(".maple/gmi" + path + "index.gmi"); - std::stringstream buffer; + if (found_first != std::string::npos) { + path = path.substr( + found_first, + path.size() - 1 + ); // Remove host + } else { + path = "/index.gmi"; + } - buffer << file.rdbuf(); + if (request_scheme == 1) { + // Remove junk, if any + index_of_junk = path.find_first_of('\n'); - response << "20 text/gemini\r\n" << buffer.str(); - } else { - if (std::equal( - protocol.begin(), - protocol.end(), - GEMINI_PROTOCOL.begin(), - GEMINI_PROTOCOL.end(), - [](char a, char b) -> bool { - return std::tolower(a) == std::tolower(b); - } - )) { - response << "51 The server (Maple) could not find the specified file.\r\n"; - } else { - response << "59 The server (Maple) received a bad request: Invalid protocol\r\n"; + if (index_of_junk != std::string::npos) { + path.erase( + path.find_first_of('\n') - 1, + path.size() - 1 + ); } } - } - std::cout << "requested " << path << std::endl; + // Gemini + if (request_scheme == 1) { + maple::gemini::handle_client(gemini_files, path, response); + } else { // Titan + if (!titan) { + response << "20 text/gemini\r\nThe server (Maple) does not have " + "Titan support enabled!"; + } else { + maple::titan::handle_client( + response, + path, + titan_token, + titan_max_size + ); + } + } - SSL_write(ssl, response.str().c_str(), static_cast<int>(response.str().size())); + SSL_write( + ssl, + response.str().c_str(), + static_cast<int>(response.str().size()) + ); + } else { + std::cout << "received a request with an unsupported url scheme" + << std::endl; + } } SSL_shutdown(ssl); @@ -211,8 +188,126 @@ auto main() -> int { } } -auto exit_with[[noreturn]](const char *message, bool ssl) -> void { - perror(message); - if (ssl) { ERR_print_errors_fp(stderr); } - std::exit(EXIT_FAILURE); +namespace maple { + auto exit_with[[noreturn]](const char *message, bool ssl) -> void { + perror(message); + + if (ssl) { ERR_print_errors_fp(stderr); } + + std::exit(EXIT_FAILURE); + } + + auto setup_environment( + bool &titan, + std::string &titan_token, + size_t &titan_max_size + ) -> void { + char *titan_environment = std::getenv("TITAN"); + + if (titan_environment == nullptr) { + titan = false; + } else { + std::string valid_titan_environment(titan_environment); + + std::transform( + valid_titan_environment.begin(), + valid_titan_environment.end(), + valid_titan_environment.begin(), + [](unsigned char c) -> int { return std::tolower(c); } + ); + + if (valid_titan_environment == "true" || valid_titan_environment == "1") { + char *unvalidated_titan_token = std::getenv("TITAN_TOKEN"); + char *unvalidated_titan_max_size = std::getenv("TITAN_MAX_SIZE"); + + if (unvalidated_titan_token == nullptr) { + titan_token = ""; + } else { + titan_token = std::string(unvalidated_titan_token); + } + + if (unvalidated_titan_max_size == nullptr) { + titan_max_size = 1024; + + std::cout << "no TITAN_MAX_SIZE set, defaulting to 1024" << std::endl; + } else { + try { + titan_max_size = static_cast<size_t>( + std::stoi(unvalidated_titan_max_size) + ); + } catch (...) { + maple::exit_with( + "TITAN_MAX_SIZE could not be interpreted as an integer", + false + ); + } + } + + titan = true; + } + } + } + + auto setup_ssl() -> void { + sockaddr_in socket_address {}; + + // Setup OpenSSL + SSL_library_init(); + SSL_load_error_strings(); + + maple::ssl_context = SSL_CTX_new(TLS_server_method()); + + if (!maple::ssl_context) { + maple::exit_with("unable to create ssl context", true); + } + + if (SSL_CTX_use_certificate_file( + maple::ssl_context, + ".maple/public.pem", + SSL_FILETYPE_PEM + ) <= 0) { + maple::exit_with("unable to use certificate file", true); + } + if (SSL_CTX_use_PrivateKey_file( + maple::ssl_context, + ".maple/private.pem", + SSL_FILETYPE_PEM + ) <= 0) { + maple::exit_with("unable to use private key file", true); + } + + socket_address.sin_family = AF_INET; + socket_address.sin_port = htons(1965); + socket_address.sin_addr.s_addr = htonl(INADDR_ANY); + + maple::maple_socket = socket(AF_INET, SOCK_STREAM, 0); + + if (maple::maple_socket < 0) { + maple::exit_with("unable to create socket", false); + } + + // Reuse address. Allows the use of the address instantly after a SIGINT + // without having to wait for the socket to die. + int reuse_addr = 1; + if (setsockopt( + maple::maple_socket, + SOL_SOCKET, + SO_REUSEADDR, + &reuse_addr, + sizeof(int) + ) < 0) { + maple::exit_with("unable to set socket options (SO_LINGER)", false); + } + + if (bind( + maple::maple_socket, + reinterpret_cast<sockaddr *>(&socket_address), + sizeof(socket_address) + ) < 0) { + maple::exit_with("unable to bind", false); + } + if (listen(maple::maple_socket, 1) < 0) { + maple::exit_with("unable to listen", false); + } + } } diff --git a/maple/maple.hh b/maple/maple.hh new file mode 100644 index 0000000..05e83fc --- /dev/null +++ b/maple/maple.hh @@ -0,0 +1,35 @@ +/* + * This file is part of Maple <https://github.com/gemrest/maple>. + * Copyright (C) 2022-2022 Fuwn <[email protected]> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright (C) 2022-2022 Fuwn <[email protected]> + * SPDX-License-Identifier: GPL-3.0-only + */ + +#ifndef MAPLE_HH +#define MAPLE_HH + +#include <openssl/ssl.h> + +namespace maple { + static int maple_socket; + static SSL_CTX *ssl_context; + + auto exit_with[[noreturn]](const char *, bool) -> void; + auto setup_environment(bool &, std::string &, size_t &) -> void; + auto setup_ssl() -> void; +} + +#endif // MAPLE_HH diff --git a/maple/titan.cc b/maple/titan.cc new file mode 100644 index 0000000..2ae32a4 --- /dev/null +++ b/maple/titan.cc @@ -0,0 +1,168 @@ +/* + * This file is part of Maple <https://github.com/gemrest/maple>. + * Copyright (C) 2022-2022 Fuwn <[email protected]> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright (C) 2022-2022 Fuwn <[email protected]> + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include <map> +#include <fstream> +#include <vector> + +#include "titan.hh" + +namespace maple::titan { + auto parameters_to_map( + const std::vector<std::string> ¶meters + ) -> std::map<std::string, std::string> { + std::map<std::string, std::string> parameters_map; + + for (auto parameter : parameters) { + // Find the key in `parameter` + size_t parameter_delimiter_position = parameter.find('='); + std::string key = parameter.substr( + 0, + parameter_delimiter_position + ); + + // Remove the key in `parameter` + parameter.erase(0, parameter_delimiter_position + 1); + + // Add the key and value to `parameters_map` + parameters_map[key] = parameter; + } + + return parameters_map; + } + + auto handle_client( + std::stringstream &response, + std::string path, + const std::string &titan_token, + size_t titan_max_size + ) -> void { + std::vector<std::string> parameters; + // Find path in `path` + size_t delimiter_position = path.find(';'); + std::string update_path = path.substr(0, delimiter_position); + std::string body = path.substr(path.find('\n') + 1, path.length() - 1); + + path.erase(path.find('\n') - 1, path.length() - 1); + // parameters.push_back(update_path); + path.erase(0, delimiter_position + 1); // Remove path from `path` + + // Find mime parameter in `path` + delimiter_position = path.find(';'); + + parameters.push_back(path.substr(0, delimiter_position)); + path.erase(0, delimiter_position + 1); // Remove mime parameter from `path` + + // Find size parameter in `path` + delimiter_position = path.find(';'); + + parameters.push_back(path.substr(0, delimiter_position)); + + // Find token parameter in `path` + delimiter_position = path.find(';'); + + // Since the token is optional, only get and assign the token + // parameters value if it exists. + if (delimiter_position != std::string::npos) { + parameters.push_back(path.substr( + delimiter_position + 1, + path.length() - 1 + )); + } + +/// Check if a parameter exists within a `std::vector` of Titan +/// parameters. + /* auto parameter_exists = []( + const std::vector<std::string> &_parameters, + const std::string ¶meter + ) -> bool { + return std::any_of( + _parameters.begin(), + _parameters.end(), + [&](const std::string &s) -> bool { + return s.find(parameter) != std::string::npos; + } + ); + }; */ + + std::map<std::string, std::string> parameters_map = + maple::titan::parameters_to_map(parameters); + + // Make sure all tokens have been supplied + for (;;) { + if (parameters_map.find("mime") == parameters_map.end()) { + response << "20 text/gemini\r\nThe serve (Maple) did not " + "receive a mime parameter!"; + break; + } + if (parameters_map.find("size") == parameters_map.end()) { + response << "20 text/gemini\r\nThe serve (Maple) did not " + "receive a size parameter!"; + + break; + } + if (!titan_token.empty() + && parameters_map.find("token") == parameters_map.end()) + { + response << "20 text/gemini\r\nThe serve (Maple) did not " + "receive a token parameter!"; + + break; + } + + try { + size_t body_size = static_cast<size_t>( + std::stoi(parameters_map["size"]) + ); + + if (body_size > titan_max_size) { + response << "20 text/gemini\r\nThe server (Maple) received a body " + << "which is larger than the maximum allowed body size (" + << titan_max_size << ")."; + + break; + } + } catch (...) { + response << "20 text/gemini\r\nThe server (Maple) could not interpret " + "the size parameter as an integer!"; + + break; + } + + if (update_path == "/") { + update_path = "/index.gmi"; + } + + if (parameters_map["token"] == titan_token) { + std::ofstream file(".maple/gmi" + update_path); + + file << body; + + response << "20 text/gemini\r\nSuccessfully wrote " + << body.length() << " bytes to " << update_path << '!'; + } else { + response << "20 text/gemini\r\nThe server (Maple) wrote to " + << update_path; + } + + break; + } + } +} diff --git a/maple/titan.hh b/maple/titan.hh new file mode 100644 index 0000000..8895551 --- /dev/null +++ b/maple/titan.hh @@ -0,0 +1,40 @@ +/* + * This file is part of Maple <https://github.com/gemrest/maple>. + * Copyright (C) 2022-2022 Fuwn <[email protected]> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Copyright (C) 2022-2022 Fuwn <[email protected]> + * SPDX-License-Identifier: GPL-3.0-only + */ + +#ifndef TITAN_HH +#define TITAN_HH + +#include <sstream> + +namespace maple::titan { + /// Convert a `std::vector` of Titan parameters into a key/ value `std::map` + auto parameters_to_map( + const std::vector<std::string> & + ) -> std::map<std::string, std::string>; + + auto handle_client( + std::stringstream &, + std::string, + const std::string &, + size_t + ) -> void; +} + +#endif // TITAN_HH |