๐Ÿ A very simple static Gemini server, now with Titan support!
cpp gemini titan gemini-protocol titan-protocol
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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.

Fuwn 56fbf0ae 047e83da

+475 -79
+4 -2
build.ninja
··· 11 11 rule link 12 12 command = $cc $ldflags $in -o $out 13 13 14 - build $out_dir/$name.o: compile $src_dir/$name.cc 14 + build $out_dir/$name.o: compile $src_dir/$name.cc 15 + build $out_dir/gemini.o: compile $src_dir/gemini.cc 16 + build $out_dir/titan.o: compile $src_dir/titan.cc 15 17 16 - build $out_dir/$name: link $out_dir/$name.o 18 + build $out_dir/$name: link $out_dir/$name.o $out_dir/gemini.o $out_dir/titan.o 17 19 18 20 default $out_dir/$name
+64
maple/gemini.cc
··· 1 + /* 2 + * This file is part of Maple <https://github.com/gemrest/maple>. 3 + * Copyright (C) 2022-2022 Fuwn <contact@fuwn.me> 4 + * 5 + * This program is free software: you can redistribute it and/or modify 6 + * it under the terms of the GNU General Public License as published by 7 + * the Free Software Foundation, version 3. 8 + * 9 + * This program is distributed in the hope that it will be useful, but 10 + * WITHOUT ANY WARRANTY; without even the implied warranty of 11 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 + * General Public License for more details. 13 + * 14 + * You should have received a copy of the GNU General Public License 15 + * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 + * 17 + * Copyright (C) 2022-2022 Fuwn <contact@fuwn.me> 18 + * SPDX-License-Identifier: GPL-3.0-only 19 + */ 20 + 21 + #include <fstream> 22 + #include <iostream> 23 + 24 + #include "gemini.hh" 25 + 26 + namespace maple::gemini { 27 + auto handle_client( 28 + std::vector<std::string> gemini_files, 29 + std::string path, 30 + std::stringstream &response 31 + ) -> void { 32 + // Check if the route is a file being served 33 + if (std::find( 34 + gemini_files.begin(), 35 + gemini_files.end(), 36 + ".maple/gmi" + path 37 + ) != gemini_files.end()) { 38 + // If the route is a file being served; get the file contents 39 + 40 + std::ifstream file(".maple/gmi" + path); 41 + std::stringstream buffer; 42 + 43 + buffer << file.rdbuf(); 44 + 45 + file.close(); 46 + 47 + response << "20 text/gemini\r\n" << buffer.str(); 48 + } else { 49 + if (path.empty() || path.at(path.length() - 1) == '/') { 50 + std::ifstream file(".maple/gmi" + path + "index.gmi"); 51 + std::stringstream buffer; 52 + 53 + buffer << file.rdbuf(); 54 + 55 + response << "20 text/gemini\r\n" << buffer.str(); 56 + } else { 57 + response 58 + << "51 The server (Maple) could not find the specified file.\r\n"; 59 + } 60 + } 61 + 62 + std::cout << "requested " << path << std::endl; 63 + } 64 + }
+35
maple/gemini.hh
··· 1 + /* 2 + * This file is part of Maple <https://github.com/gemrest/maple>. 3 + * Copyright (C) 2022-2022 Fuwn <contact@fuwn.me> 4 + * 5 + * This program is free software: you can redistribute it and/or modify 6 + * it under the terms of the GNU General Public License as published by 7 + * the Free Software Foundation, version 3. 8 + * 9 + * This program is distributed in the hope that it will be useful, but 10 + * WITHOUT ANY WARRANTY; without even the implied warranty of 11 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 + * General Public License for more details. 13 + * 14 + * You should have received a copy of the GNU General Public License 15 + * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 + * 17 + * Copyright (C) 2022-2022 Fuwn <contact@fuwn.me> 18 + * SPDX-License-Identifier: GPL-3.0-only 19 + */ 20 + 21 + #ifndef GEMINI_HH 22 + #define GEMINI_HH 23 + 24 + #include <sstream> 25 + #include <vector> 26 + 27 + namespace maple::gemini { 28 + auto handle_client( 29 + std::vector<std::string>, 30 + std::string, 31 + std::stringstream & 32 + ) -> void; 33 + } 34 + 35 + #endif // GEMINI_HH
+151 -77
maple/maple.cc
··· 21 21 #include <arpa/inet.h> 22 22 #include <csignal> 23 23 #include <filesystem> 24 - #include <fstream> 25 24 #include <iostream> 25 + #include <map> 26 26 #include <openssl/err.h> 27 - #include <openssl/ssl.h> 28 27 #include <sys/socket.h> 29 28 #include <unistd.h> 30 29 #include <vector> 31 30 32 - static int maple_socket; 33 - static SSL_CTX *ssl_context; 34 - 35 - auto exit_with[[noreturn]](const char *, bool) -> void; 31 + #include "gemini.hh" 32 + #include "maple.hh" 33 + #include "titan.hh" 36 34 37 35 auto main() -> int { 38 36 sockaddr_in socket_address {}; 39 37 std::vector<std::string> gemini_files; 38 + bool titan = false; 39 + std::string titan_token; 40 + size_t titan_max_size = 0; 41 + 42 + // Check if the user is want to support Titan 43 + { 44 + char *titan_environment = std::getenv("TITAN"); 45 + 46 + if (titan_environment == nullptr) { 47 + titan = false; 48 + } else { 49 + std::string valid_titan_environment(titan_environment); 50 + 51 + std::transform( 52 + valid_titan_environment.begin(), 53 + valid_titan_environment.end(), 54 + valid_titan_environment.begin(), 55 + [](unsigned char c) -> int { return std::tolower(c); } 56 + ); 57 + 58 + if (valid_titan_environment == "true" || valid_titan_environment == "1") { 59 + char *unvalidated_titan_token = std::getenv("TITAN_TOKEN"); 60 + char *unvalidated_titan_max_size = std::getenv("TITAN_MAX_SIZE"); 61 + 62 + if (unvalidated_titan_token == nullptr) { 63 + titan_token = ""; 64 + } else { 65 + titan_token = std::string(unvalidated_titan_token); 66 + } 67 + 68 + if (unvalidated_titan_max_size == nullptr) { 69 + titan_max_size = 1024; 70 + 71 + std::cout << "no TITAN_MAX_SIZE set, defaulting to 1024" << std::endl; 72 + } else { 73 + try { 74 + titan_max_size = static_cast<size_t>( 75 + std::stoi(unvalidated_titan_max_size) 76 + ); 77 + } catch (...) { 78 + maple::exit_with( 79 + "TITAN_MAX_SIZE could not be interpreted as an integer", 80 + false 81 + ); 82 + } 83 + } 84 + 85 + titan = true; 86 + } 87 + } 88 + } 40 89 41 90 // Try a graceful shutdown when a SIGINT is detected 42 91 signal(SIGINT, [](int signal_) -> void { 43 92 std::cout << "shutdown(" << signal_ << ")" << std::endl; 44 93 45 - close(maple_socket); 46 - SSL_CTX_free(ssl_context); 94 + close(maple::maple_socket); 95 + SSL_CTX_free(maple::ssl_context); 47 96 }); 48 97 49 98 // Find and keep track of all Gemini files to serve ··· 61 110 file_extension.end(), 62 111 gemini_file_extension.begin(), 63 112 gemini_file_extension.end(), 64 - [](auto a, auto b) -> bool { 65 - return std::tolower(a) == std::tolower(b); 113 + [](auto a, auto b) -> bool {return std::tolower(a) == std::tolower(b); 66 114 } 67 115 )) { 68 116 gemini_files.push_back(entry.path()); ··· 78 126 SSL_library_init(); 79 127 SSL_load_error_strings(); 80 128 81 - ssl_context = SSL_CTX_new(TLS_server_method()); 82 - if (!ssl_context) { 83 - exit_with("unable to create ssl context", true); 129 + maple::ssl_context = SSL_CTX_new(TLS_server_method()); 130 + if (!maple::ssl_context) { 131 + maple::exit_with("unable to create ssl context", true); 84 132 } 85 133 86 134 if (SSL_CTX_use_certificate_file( 87 - ssl_context, 135 + maple::ssl_context, 88 136 ".maple/public.pem", 89 137 SSL_FILETYPE_PEM 90 138 ) <= 0) { 91 - exit_with("unable to use certificate file", true); 139 + maple::exit_with("unable to use certificate file", true); 92 140 } 93 141 if (SSL_CTX_use_PrivateKey_file( 94 - ssl_context, 142 + maple::ssl_context, 95 143 ".maple/private.pem", 96 144 SSL_FILETYPE_PEM 97 145 ) <= 0) { 98 - exit_with("unable to use private key file", true); 146 + maple::exit_with("unable to use private key file", true); 99 147 } 100 148 101 149 socket_address.sin_family = AF_INET; 102 150 socket_address.sin_port = htons(1965); 103 151 socket_address.sin_addr.s_addr = htonl(INADDR_ANY); 104 152 105 - maple_socket = socket(AF_INET, SOCK_STREAM, 0); 106 - if (maple_socket < 0) { 107 - exit_with("unable to create socket", false); 153 + maple::maple_socket = socket(AF_INET, SOCK_STREAM, 0); 154 + 155 + if (maple::maple_socket < 0) { 156 + maple::exit_with("unable to create socket", false); 108 157 } 109 158 110 159 // Reuse address. Allows the use of the address instantly after a SIGINT 111 160 // without having to wait for the socket to die. 112 161 int reuse_addr = 1; 113 162 if (setsockopt( 114 - maple_socket, 163 + maple::maple_socket, 115 164 SOL_SOCKET, 116 165 SO_REUSEADDR, 117 166 &reuse_addr, 118 167 sizeof(int) 119 168 ) < 0) { 120 - exit_with("unable to set socket options (SO_LINGER)", false); 169 + maple::exit_with("unable to set socket options (SO_LINGER)", false); 121 170 } 122 171 123 172 if (bind( 124 - maple_socket, 173 + maple::maple_socket, 125 174 reinterpret_cast<sockaddr *>(&socket_address), 126 175 sizeof(socket_address) 127 176 ) < 0) { 128 - exit_with("unable to bind", false); 177 + maple::exit_with("unable to bind", false); 129 178 } 130 - if (listen(maple_socket, 1) < 0) { 131 - exit_with("unable to listen", false); 179 + if (listen(maple::maple_socket, 1) < 0) { 180 + maple::exit_with("unable to listen", false); 132 181 } 133 182 134 183 // Listen and serve connections ··· 137 186 unsigned int socket_address_length = sizeof(socket_address_); 138 187 SSL *ssl; 139 188 int client = accept( 140 - maple_socket, 189 + maple::maple_socket, 141 190 reinterpret_cast<sockaddr *>(&socket_address_), 142 191 &socket_address_length 143 192 ); 144 193 char request[1024]; 145 194 146 - if (client < 0) { exit_with("unable to accept", false); } 195 + if (client < 0) { maple::exit_with("unable to accept", false); } 147 196 148 - ssl = SSL_new(ssl_context); 197 + ssl = SSL_new(maple::ssl_context); 149 198 SSL_set_fd(ssl, client); 150 199 151 200 if (SSL_accept(ssl) <= 0) { ··· 153 202 } else { 154 203 std::stringstream response; 155 204 size_t index_of_junk; 205 + int request_scheme; // Gemini = 1, Titan = 2, Error = 0 206 + size_t bytes_read; 156 207 157 - SSL_read(ssl, request, sizeof(request)); 208 + SSL_read_ex(ssl, request, sizeof(request), &bytes_read); 158 209 159 210 std::string path(request); 160 211 161 - path = path.substr(0, path.size() - 2); // Remove "\r\n" 162 - path.erase(0, 9); // Remove "gemini://" 163 - 164 - // Try to remove the host, if you cannot; it must be a trailing slash-less 165 - // hostname, so we will respond with the index. 166 - size_t found_first = path.find_first_of('/'); 167 - if (found_first != std::string::npos) { 168 - path = path.substr( 169 - found_first, 170 - path.size() - 1 171 - ); // Remove host 212 + if (path.starts_with("gemini://")) { 213 + request_scheme = 1; 214 + } else if (path.starts_with("titan://")) { 215 + request_scheme = 2; 172 216 } else { 173 - path = "/index.gmi"; 217 + request_scheme = 0; 174 218 } 175 219 176 - // Remove junk, if any 177 - index_of_junk = path.find_first_of('\n'); 178 - if (index_of_junk != std::string::npos) { 179 - path.erase( 180 - path.find_first_of('\n') - 1, 181 - path.size() - 1 182 - ); 183 - } 220 + if (request_scheme != 0) { 221 + path = path.substr(0, bytes_read); 184 222 185 - // Check if the route is a file being served 186 - if (std::find( 187 - gemini_files.begin(), 188 - gemini_files.end(), 189 - ".maple/gmi" + path 190 - ) != gemini_files.end()) { 191 - // If the route is a file being served; get the file contents 223 + // Remove "\r\n" if Gemini 224 + if (request_scheme == 1) { 225 + path = path.substr(0, path.size() - 2); 226 + } 192 227 193 - std::ifstream file(".maple/gmi" + path); 194 - std::stringstream buffer; 195 - 196 - buffer << file.rdbuf(); 228 + if (request_scheme == 1) { 229 + path.erase(0, 9); // Remove "gemini://" 230 + } else { 231 + path.erase(0, 8); // Remove "titan://" 232 + } 197 233 198 - response << "20 text/gemini\r\n" << buffer.str(); 199 - } else { 200 - if (path.empty() || path.at(path.length() - 1) == '/') { 201 - std::ifstream file(".maple/gmi" + path + "index.gmi"); 202 - std::stringstream buffer; 234 + // Try to remove the host, if you cannot; it must be a trailing 235 + // slash-less hostname, so we will respond with the index. 236 + size_t found_first = path.find_first_of('/'); 237 + if (found_first != std::string::npos) { 238 + path = path.substr( 239 + found_first, 240 + path.size() - 1 241 + ); // Remove host 242 + } else { 243 + path = "/index.gmi"; 244 + } 203 245 204 - buffer << file.rdbuf(); 246 + // std::cout << "1: \"" << path << "\"" << std::endl; 205 247 206 - response << "20 text/gemini\r\n" << buffer.str(); 207 - } else { 208 - response << "51 The server (Maple) could not find the specified file.\r\n"; 248 + if (request_scheme == 1) { 249 + // Remove junk, if any 250 + index_of_junk = path.find_first_of('\n'); 251 + if (index_of_junk != std::string::npos) { 252 + path.erase( 253 + path.find_first_of('\n') - 1, 254 + path.size() - 1 255 + ); 256 + } 209 257 } 210 - } 258 + 259 + // std::cout << "2: \"" << path << "\"" << std::endl; 211 260 212 - std::cout << "requested " << path << std::endl; 261 + // Gemini 262 + if (request_scheme == 1) { 263 + maple::gemini::handle_client(gemini_files, path, response); 264 + } else { // Titan 265 + if (!titan) { 266 + response << "20 text/gemini\r\nThe server (Maple) does not have " 267 + "Titan support enabled!"; 268 + } else { 269 + maple::titan::handle_client( 270 + response, 271 + path, 272 + titan_token, 273 + titan_max_size 274 + ); 275 + } 276 + } 213 277 214 - SSL_write(ssl, response.str().c_str(), static_cast<int>(response.str().size())); 278 + SSL_write( 279 + ssl, 280 + response.str().c_str(), 281 + static_cast<int>(response.str().size()) 282 + ); 283 + } else { 284 + std::cout << "received a request with an unsupported url scheme" 285 + << std::endl; 286 + } 215 287 } 216 288 217 289 SSL_shutdown(ssl); ··· 220 292 } 221 293 } 222 294 223 - auto exit_with[[noreturn]](const char *message, bool ssl) -> void { 224 - perror(message); 225 - if (ssl) { ERR_print_errors_fp(stderr); } 226 - std::exit(EXIT_FAILURE); 295 + namespace maple { 296 + auto exit_with[[noreturn]](const char *message, bool ssl) -> void { 297 + perror(message); 298 + if (ssl) { ERR_print_errors_fp(stderr); } 299 + std::exit(EXIT_FAILURE); 300 + } 227 301 }
+13
maple/maple.hh
··· 1 + #ifndef MAPLE_HH 2 + #define MAPLE_HH 3 + 4 + #include <openssl/ssl.h> 5 + 6 + namespace maple { 7 + static int maple_socket; 8 + static SSL_CTX *ssl_context; 9 + 10 + auto exit_with[[noreturn]](const char *, bool) -> void; 11 + } 12 + 13 + #endif // MAPLE_HH
+168
maple/titan.cc
··· 1 + /* 2 + * This file is part of Maple <https://github.com/gemrest/maple>. 3 + * Copyright (C) 2022-2022 Fuwn <contact@fuwn.me> 4 + * 5 + * This program is free software: you can redistribute it and/or modify 6 + * it under the terms of the GNU General Public License as published by 7 + * the Free Software Foundation, version 3. 8 + * 9 + * This program is distributed in the hope that it will be useful, but 10 + * WITHOUT ANY WARRANTY; without even the implied warranty of 11 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 + * General Public License for more details. 13 + * 14 + * You should have received a copy of the GNU General Public License 15 + * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 + * 17 + * Copyright (C) 2022-2022 Fuwn <contact@fuwn.me> 18 + * SPDX-License-Identifier: GPL-3.0-only 19 + */ 20 + 21 + #include <map> 22 + #include <fstream> 23 + #include <vector> 24 + 25 + #include "titan.hh" 26 + 27 + namespace maple::titan { 28 + auto parameters_to_map( 29 + const std::vector<std::string> &parameters 30 + ) -> std::map<std::string, std::string> { 31 + std::map<std::string, std::string> parameters_map; 32 + 33 + for (auto parameter : parameters) { 34 + // Find the key in `parameter` 35 + size_t parameter_delimiter_position = parameter.find('='); 36 + std::string key = parameter.substr( 37 + 0, 38 + parameter_delimiter_position 39 + ); 40 + 41 + // Remove the key in `parameter` 42 + parameter.erase(0, parameter_delimiter_position + 1); 43 + 44 + // Add the key and value to `parameters_map` 45 + parameters_map[key] = parameter; 46 + } 47 + 48 + return parameters_map; 49 + } 50 + 51 + auto handle_client( 52 + std::stringstream &response, 53 + std::string path, 54 + const std::string &titan_token, 55 + size_t titan_max_size 56 + ) -> void { 57 + std::vector<std::string> parameters; 58 + // Find path in `path` 59 + size_t delimiter_position = path.find(';'); 60 + std::string update_path = path.substr(0, delimiter_position); 61 + std::string body = path.substr(path.find('\n') + 1, path.length() - 1); 62 + 63 + path.erase(path.find('\n') - 1, path.length() - 1); 64 + // parameters.push_back(update_path); 65 + path.erase(0, delimiter_position + 1); // Remove path from `path` 66 + 67 + // Find mime parameter in `path` 68 + delimiter_position = path.find(';'); 69 + 70 + parameters.push_back(path.substr(0, delimiter_position)); 71 + path.erase(0, delimiter_position + 1); // Remove mime parameter from `path` 72 + 73 + // Find size parameter in `path` 74 + delimiter_position = path.find(';'); 75 + 76 + parameters.push_back(path.substr(0, delimiter_position)); 77 + 78 + // Find token parameter in `path` 79 + delimiter_position = path.find(';'); 80 + 81 + // Since the token is optional, only get and assign the token 82 + // parameters value if it exists. 83 + if (delimiter_position != std::string::npos) { 84 + parameters.push_back(path.substr( 85 + delimiter_position + 1, 86 + path.length() - 1 87 + )); 88 + } 89 + 90 + /// Check if a parameter exists within a `std::vector` of Titan 91 + /// parameters. 92 + /* auto parameter_exists = []( 93 + const std::vector<std::string> &_parameters, 94 + const std::string &parameter 95 + ) -> bool { 96 + return std::any_of( 97 + _parameters.begin(), 98 + _parameters.end(), 99 + [&](const std::string &s) -> bool { 100 + return s.find(parameter) != std::string::npos; 101 + } 102 + ); 103 + }; */ 104 + 105 + std::map<std::string, std::string> parameters_map = 106 + maple::titan::parameters_to_map(parameters); 107 + 108 + // Make sure all tokens have been supplied 109 + for (;;) { 110 + if (parameters_map.find("mime") == parameters_map.end()) { 111 + response << "20 text/gemini\r\nThe serve (Maple) did not " 112 + "receive a mime parameter!"; 113 + break; 114 + } 115 + if (parameters_map.find("size") == parameters_map.end()) { 116 + response << "20 text/gemini\r\nThe serve (Maple) did not " 117 + "receive a size parameter!"; 118 + 119 + break; 120 + } 121 + if (!titan_token.empty() 122 + && parameters_map.find("token") == parameters_map.end()) 123 + { 124 + response << "20 text/gemini\r\nThe serve (Maple) did not " 125 + "receive a token parameter!"; 126 + 127 + break; 128 + } 129 + 130 + try { 131 + size_t body_size = static_cast<size_t>( 132 + std::stoi(parameters_map["size"]) 133 + ); 134 + 135 + if (body_size > titan_max_size) { 136 + response << "20 text/gemini\r\nThe server (Maple) received a body " 137 + << "which is larger than the maximum allowed body size (" 138 + << titan_max_size << ")."; 139 + 140 + break; 141 + } 142 + } catch (...) { 143 + response << "20 text/gemini\r\nThe server (Maple) could not interpret " 144 + "the size parameter as an integer!"; 145 + 146 + break; 147 + } 148 + 149 + if (update_path == "/") { 150 + update_path = "/index.gmi"; 151 + } 152 + 153 + if (parameters_map["token"] == titan_token) { 154 + std::ofstream file(".maple/gmi" + update_path); 155 + 156 + file << body; 157 + 158 + response << "20 text/gemini\r\nSuccessfully wrote " 159 + << body.length() << " bytes to " << update_path << '!'; 160 + } else { 161 + response << "20 text/gemini\r\nThe server (Maple) wrote to " 162 + << update_path; 163 + } 164 + 165 + break; 166 + } 167 + } 168 + }
+40
maple/titan.hh
··· 1 + /* 2 + * This file is part of Maple <https://github.com/gemrest/maple>. 3 + * Copyright (C) 2022-2022 Fuwn <contact@fuwn.me> 4 + * 5 + * This program is free software: you can redistribute it and/or modify 6 + * it under the terms of the GNU General Public License as published by 7 + * the Free Software Foundation, version 3. 8 + * 9 + * This program is distributed in the hope that it will be useful, but 10 + * WITHOUT ANY WARRANTY; without even the implied warranty of 11 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 + * General Public License for more details. 13 + * 14 + * You should have received a copy of the GNU General Public License 15 + * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 + * 17 + * Copyright (C) 2022-2022 Fuwn <contact@fuwn.me> 18 + * SPDX-License-Identifier: GPL-3.0-only 19 + */ 20 + 21 + #ifndef TITAN_HH 22 + #define TITAN_HH 23 + 24 + #include <sstream> 25 + 26 + namespace maple::titan { 27 + /// Convert a `std::vector` of Titan parameters into a key/ value `std::map` 28 + auto parameters_to_map( 29 + const std::vector<std::string> & 30 + ) -> std::map<std::string, std::string>; 31 + 32 + auto handle_client( 33 + std::stringstream &, 34 + std::string, 35 + const std::string &, 36 + size_t 37 + ) -> void; 38 + } 39 + 40 + #endif // TITAN_HH