diff options
| author | Fuwn <[email protected]> | 2022-05-09 08:53:36 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2022-05-09 08:53:36 +0000 |
| commit | 56fbf0ae1ef77ae69c7d2484d67bb664a4dee744 (patch) | |
| tree | 14cdccb13cd594b2f18210ce352847e7b85170ad | |
| parent | refactor: explicit auto (diff) | |
| download | maple-56fbf0ae1ef77ae69c7d2484d67bb664a4dee744.tar.xz maple-56fbf0ae1ef77ae69c7d2484d67bb664a4dee744.zip | |
feat: titan support
This commit is huge...
For the most part, this commit just adds Titan support. However, this
commit also refactors the `maple/` directory so that every complex
block lives in it's own namespace.
| -rw-r--r-- | build.ninja | 6 | ||||
| -rw-r--r-- | maple/gemini.cc | 64 | ||||
| -rw-r--r-- | maple/gemini.hh | 35 | ||||
| -rw-r--r-- | maple/maple.cc | 230 | ||||
| -rw-r--r-- | maple/maple.hh | 13 | ||||
| -rw-r--r-- | maple/titan.cc | 168 | ||||
| -rw-r--r-- | maple/titan.hh | 40 |
7 files changed, 476 insertions, 80 deletions
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..2a8bc0d --- /dev/null +++ b/maple/gemini.cc @@ -0,0 +1,64 @@ +/* + * 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 + + std::ifstream file(".maple/gmi" + path); + std::stringstream buffer; + + buffer << file.rdbuf(); + + file.close(); + + 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; + + buffer << file.rdbuf(); + + response << "20 text/gemini\r\n" << buffer.str(); + } 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 0bae0e9..6382fa1 100644 --- a/maple/maple.cc +++ b/maple/maple.cc @@ -21,29 +21,78 @@ #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; + bool titan = false; + std::string titan_token; + size_t titan_max_size = 0; + + // Check if the user is want to support Titan + { + 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; + } + } + } // Try a graceful shutdown when a SIGINT is detected 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 @@ -61,8 +110,7 @@ auto main() -> int { file_extension.end(), gemini_file_extension.begin(), gemini_file_extension.end(), - [](auto a, auto b) -> bool { - return std::tolower(a) == std::tolower(b); + [](auto a, auto b) -> bool {return std::tolower(a) == std::tolower(b); } )) { gemini_files.push_back(entry.path()); @@ -78,57 +126,58 @@ auto main() -> int { 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); + 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( - ssl_context, + maple::ssl_context, ".maple/public.pem", SSL_FILETYPE_PEM ) <= 0) { - exit_with("unable to use certificate file", true); + maple::exit_with("unable to use certificate file", true); } if (SSL_CTX_use_PrivateKey_file( - ssl_context, + maple::ssl_context, ".maple/private.pem", SSL_FILETYPE_PEM ) <= 0) { - exit_with("unable to use private key file", true); + 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_socket = socket(AF_INET, SOCK_STREAM, 0); - if (maple_socket < 0) { - exit_with("unable to create socket", false); + 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_socket, + maple::maple_socket, SOL_SOCKET, SO_REUSEADDR, &reuse_addr, sizeof(int) ) < 0) { - exit_with("unable to set socket options (SO_LINGER)", false); + maple::exit_with("unable to set socket options (SO_LINGER)", false); } if (bind( - maple_socket, + maple::maple_socket, reinterpret_cast<sockaddr *>(&socket_address), sizeof(socket_address) ) < 0) { - exit_with("unable to bind", false); + maple::exit_with("unable to bind", false); } - if (listen(maple_socket, 1) < 0) { - exit_with("unable to listen", false); + if (listen(maple::maple_socket, 1) < 0) { + maple::exit_with("unable to listen", false); } // Listen and serve connections @@ -137,15 +186,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(ssl_context); + ssl = SSL_new(maple::ssl_context); SSL_set_fd(ssl, client); if (SSL_accept(ssl) <= 0) { @@ -153,65 +202,88 @@ auto main() -> int { } else { std::stringstream response; size_t index_of_junk; + int request_scheme; // Gemini = 1, Titan = 2, Error = 0 + size_t bytes_read; - 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" - 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 - ); - } - - // 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 - - std::ifstream file(".maple/gmi" + path); - std::stringstream buffer; - - buffer << file.rdbuf(); + if (request_scheme != 0) { + path = path.substr(0, bytes_read); - 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; + // Remove "\r\n" if Gemini + if (request_scheme == 1) { + path = path.substr(0, path.size() - 2); + } - buffer << file.rdbuf(); + if (request_scheme == 1) { + path.erase(0, 9); // Remove "gemini://" + } else { + path.erase(0, 8); // Remove "titan://" + } - response << "20 text/gemini\r\n" << buffer.str(); + // 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 } else { - response << "51 The server (Maple) could not find the specified file.\r\n"; + path = "/index.gmi"; + } + +// std::cout << "1: \"" << path << "\"" << std::endl; + + if (request_scheme == 1) { + // 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 + ); + } } - } - std::cout << "requested " << path << std::endl; +// std::cout << "2: \"" << 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); @@ -220,8 +292,10 @@ 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); + } } diff --git a/maple/maple.hh b/maple/maple.hh new file mode 100644 index 0000000..bdf43c3 --- /dev/null +++ b/maple/maple.hh @@ -0,0 +1,13 @@ +#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; +} + +#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 |