aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.ninja6
-rw-r--r--maple/gemini.cc64
-rw-r--r--maple/gemini.hh35
-rw-r--r--maple/maple.cc230
-rw-r--r--maple/maple.hh13
-rw-r--r--maple/titan.cc168
-rw-r--r--maple/titan.hh40
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> &parameters
+ ) -> 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 &parameter
+ ) -> 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