commit bcf3fb81ce90f24602b958223d2aa1494ff05f23 from: Aleksey Ryndin date: Wed Aug 16 16:32:12 2023 UTC Big refactoring: client is not need, entire project is a server commit - bce8f2740bc3c255439257893c9c6f724feb4689 commit + bcf3fb81ce90f24602b958223d2aa1494ff05f23 blob - d3fd63b7230c38592410979565486532bae78ab1 blob + 659a2fa04d7960c078871d6a706cadc183397df8 --- .gitignore +++ .gitignore @@ -1,4 +1,4 @@ syntax: glob cert/ -vostokd/vostokd +vostok/vostok **/*.swp blob - acdaaf481179d4a060644166f0198c5d07b1e9d2 blob + ae7abe5252f36df871ce94207abed46800156dd8 --- Makefile +++ Makefile @@ -3,10 +3,10 @@ all: server clean: - rm -rf vostokd/vostokd + rm -rf vostok/vostok server: - ${MAKE} -C vostokd + ${MAKE} -C vostok run_server: server - ./vostokd/vostokd -c cert/server.crt -k cert/server.key -f ./ + ./vostok/vostok -c cert/server.crt -k cert/server.key -f ./ blob - cdb10cb45e220ca09bf2a04fb0a76a8425c2333b blob + 57c1e0a9dea1959aac83600c9dbb919260cb7173 --- reports/0.0.1.gmi +++ reports/0.0.1.gmi @@ -1,9 +1,30 @@ -# vostokd: gemini-сервер статического контента +# vostok: простой сервер Gemini на C++11 +Столкнувшись в очередной раз с протоколом Gemini я перед сном решил почитать его спецификацию: +=> gemini://gemini.circumlunar.space/docs/specification.gmi Project Gemini (Speculative specification) + +В целом мне очень понравилось то, что автор называет протокол простым и на самом деле протокол оказался несложным. Хотя и тут стоит отметить, что использование прослойки TLS в качестве транспортного уровня делает эту самую простоту весьма условной. В противовес этому, например, протокол Spartan работает поверх обычного TCP. Наверное, если бы я сейчас сел писать реализацию заново, то выбрал бы spartan. +=> spartan://spartan.mozz.us The Spartan Protocol Homepage + + ...work-in-progress... ``` -$ mkdir cert && cd cert cert$ openssl req -newkey rsa:4096 -nodes -keyout server.key -x509 -days 36500 -out server.crt ``` + + +``` +openssl s_client -crlf -quiet -connect -no_ssl3 -no_tls1 -no_tls1_1 -connect localhost:1965 <<< "gemini://gemini.any-key.press/reports/0.0.1.gmi" +``` + +вместо создания thread'ов на каждое соединение хорошо бы перейти на неблокирующий ввод-вывод и фиксированное количество воркеров (thread'ов) + +задавать редиректы, что позволит открывать index.gmi при открытии корня + +/etc/mime.types + +(?) std::err заменить на syslog(3) + +тесты blob - 2b7e140a4af8327c021ece421a7e4c429784072e (mode 644) blob + /dev/null --- shared/cut_null +++ /dev/null @@ -1,21 +0,0 @@ -/** Remove null terminating character */ - -#include "span" - -#pragma once - - -namespace vostok -{ - - -/** Remove null terminating character and return as span */ -template -constexpr span cut_null(const char (&arr)[N]) -{ - static_assert(N > 0, "!(N > 0)"); - return span{arr, N - 1}; -} - - -} // vostok blob - 4991b214435143b06a885cc1ce776bb84fdd5b9b (mode 644) blob + /dev/null --- shared/error.cc +++ /dev/null @@ -1,16 +0,0 @@ -/** Errors handling */ - -#include -#include "error.h" - - -namespace vostok -{ -namespace error -{ - -std::ostream &g_log = std::cerr; - - -} // namespace error -} // namespace vostok blob - a1791a32da6f24b2d3a3060d95fb971e744eaecd (mode 644) blob + /dev/null --- shared/error.h +++ /dev/null @@ -1,73 +0,0 @@ -/** Errors handling */ - -#include -#include -#include - -#pragma once - - -namespace vostok -{ -namespace error -{ - - -/** Current output log stream (`std::err` on starup) */ -extern std::ostream &g_log; - - -/** Default error code (errno) printer */ -struct print -{ - const int m_error; - explicit print(int error=errno) : m_error{error} {} - void operator() () const - { - g_log << "Error code: " << std::dec << m_error << ". " << strerror(m_error) << std::endl; - } -}; - - -/** Empty error code printer */ -struct none -{ - void operator() () const {} -}; - - -/** Error handler: print action, print error */ -template< - typename TPrintAction, - typename TPrintError -> -void -occurred( - TPrintAction print_action, - TPrintError print_error -) -{ - print_action(); - g_log << " failed." << std::endl; - print_error(); -} - -template -void -occurred( - const char *action, - TPrintError print_error -) -{ - occurred( - [action] - { - g_log << action; - }, - print_error - ); -} - - -} // namespace error -} // namespace vostok blob - a99927eecbfa09bd8a7e7ed7903d4d3205a28c69 (mode 644) blob + /dev/null --- shared/gemini.cc +++ /dev/null @@ -1,24 +0,0 @@ -/** Gemini protocol specifications */ - -#include "gemini.h" - -namespace vostok -{ -namespace gemini -{ - - -const std::array CRLF{'\r', '\n'}; - -const std::array SPACE{' '}; - -const status_t STATUS_20_SUCCESS{'2', '0'}; -const status_t STATUS_40_TEMPORARY_FAILURE{'4', '0'}; -const status_t STATUS_50_PERMANENT_FAILURE{'5', '0'}; -const status_t STATUS_51_NOT_FOUND{'5', '1'}; -const status_t STATUS_53_PROXY_REQUEST_REFUSED{'5', '3'}; -const status_t STATUS_59_BAD_REQUEST{'5', '9'}; - - -} // namespace gemini -} // namespace vostok blob - 3b9b02c570f7adc25dcbcbb98661afa9c95160af (mode 644) blob + /dev/null --- shared/gemini.h +++ /dev/null @@ -1,32 +0,0 @@ -/** Gemini protocol specifications */ - -#include - -#pragma once - - -namespace vostok -{ -namespace gemini -{ - - -constexpr auto MAX_URL_LENGTH = 1024; -constexpr auto MAX_META_LENGTH = 1024; - -extern const std::array CRLF; -extern const std::array SPACE; - -using status_t = std::array; -extern const status_t STATUS_20_SUCCESS; -extern const status_t STATUS_40_TEMPORARY_FAILURE; -extern const status_t STATUS_50_PERMANENT_FAILURE; -extern const status_t STATUS_51_NOT_FOUND; -extern const status_t STATUS_53_PROXY_REQUEST_REFUSED; -extern const status_t STATUS_59_BAD_REQUEST; - -constexpr auto MAX_REQUEST_LENGTH = MAX_URL_LENGTH + CRLF.size(); - - -} // namespace gemini -} // namespace vostok blob - 91f18102c1d60cd52c7e585c92c8329e80f99ca7 (mode 644) blob + /dev/null --- shared/non_copiable +++ /dev/null @@ -1,17 +0,0 @@ -/** Non-copiable objects */ - - -#pragma once - - -namespace vostok -{ - -struct non_copiable -{ - non_copiable() = default; - non_copiable(const non_copiable &) = delete; - non_copiable &operator =(const non_copiable &) = delete; -}; - -} // namespace vostok blob - 27e97471f0107832ede9ffbcb9d826feb13c9601 (mode 644) blob + /dev/null --- shared/not_null +++ /dev/null @@ -1,43 +0,0 @@ -/** Restricts a pointer or smart pointer to only hold non-null values. */ - - -#include -#include - - -#pragma once - - -namespace vostok -{ - -template -class not_null -{ -public: - not_null(T p) :m_p{p} { assert(m_p != nullptr); } - - T get() const {return m_p;} - operator T() const {return m_p;} - T operator->() const {return m_p;} - - - // prevents compilation when someone attempts to assign a null pointer constant - not_null(std::nullptr_t) = delete; - not_null& operator=(std::nullptr_t) = delete; - - // unwanted operators...pointers only point to single objects! - not_null& operator++() = delete; - not_null& operator--() = delete; - not_null operator++(int) = delete; - not_null operator--(int) = delete; - not_null& operator+=(std::ptrdiff_t) = delete; - not_null& operator-=(std::ptrdiff_t) = delete; - void operator[](std::ptrdiff_t) const = delete; - -private: - T m_p; -}; - - -} // namespace vostok blob - 5ce2750cbb1b144ccd61629648a261139e086a47 (mode 644) blob + /dev/null --- shared/span +++ /dev/null @@ -1,54 +0,0 @@ -/** Simply std::span implementation for C++11 */ - -#include -#include - -#pragma once - - -namespace vostok -{ - -template -class span -{ -public: - using element_type = ElemT; - using iterator = ElemT *; - - - constexpr span() : m_p{nullptr}, m_count{0} {} - constexpr span(element_type *p, std::size_t count) : m_p{count ? p : nullptr}, m_count{count} {} - template - constexpr span(element_type (&arr)[N]) : m_p{arr}, m_count{N} {} - template - constexpr span(std::array &arr) : m_p{arr.data()}, m_count{N} {} - - constexpr std::size_t size() const {return m_count;} - - constexpr iterator begin() const noexcept { return m_p; } - constexpr iterator end() const noexcept { return m_p + size(); } - - span first(std::size_t count) const - { - assert(count <= m_count); - return span{m_p, count}; - } - span subspan(std::size_t offset) const - { - assert(offset <= m_count); - return (offset < m_count) ? span{m_p + offset, m_count - offset} : span{}; - } - - element_type &operator[](std::size_t idx) const - { - assert(idx < m_count); - return m_p[idx]; - } - -private: - element_type *m_p; - std::size_t m_count; -}; - -} // namespace vostok blob - d80d60dae0f1e151f0e6f93adba30d597190355f (mode 644) blob + /dev/null --- shared/transport.cc +++ /dev/null @@ -1,207 +0,0 @@ -/** Wrap libtls for gemini protocol */ - -#include "error.h" -#include "transport.h" - - -namespace vostok -{ -namespace transport -{ -namespace -{ - -using config_t = std::unique_ptr; -constexpr auto protocols = TLS_PROTOCOL_TLSv1_2 | TLS_PROTOCOL_TLSv1_3; - - -bool read(not_null ctx, span &buff) -{ - ssize_t ret{}; - for (; ; ) - { - ret = tls_read(ctx, buff.begin(), buff.size()); - if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) - continue; - break; - } - if (ret == -1) - { - const auto last_error = tls_error(ctx); - error::occurred( - "TLS read", - [last_error] - { - if (last_error) - error::g_log << "Error: " << last_error << std::endl; - } - ); - return false; - } - buff = buff.first(ret); - return true; -} - - -} // namespace - - -bool init() -{ - if (tls_init() == -1) - { - error::occurred("TLS initialization", error::none{}); - return false; - } - - return true; -} - - -void create_server(not_null cert_file, not_null key_file, context_t &ret_ctx) -{ - config_t cfg{tls_config_new(), tls_config_free}; - if (!cfg) - { - error::occurred("Create TLS configuration", error::none{}); - return; - } - - if (tls_config_set_protocols(cfg.get(), protocols) == -1) - { - error::occurred("Allow TLS 1.2 and TLS 1.3 protocols", error::none{}); - return; - } - if (tls_config_set_cert_file(cfg.get(), cert_file) == -1) - { - error::occurred( - [cert_file] - { - error::g_log << "Load certificate file " << cert_file; - }, - error::none{} - ); - return; - } - if (tls_config_set_key_file(cfg.get(), key_file) == -1) - { - error::occurred( - [key_file] - { - error::g_log << "Load key file " << key_file; - }, - error::none{} - ); - return; - } - - context_t ctx{tls_server(), tls_free}; - if (!ctx) - { - error::occurred("Create TLS server context", error::none{}); - return; - } - - if (tls_configure(ctx.get(), cfg.get()) == -1) - { - auto config_error = tls_config_error(cfg.get()); - error::occurred( - "Configure TLS context", - [config_error] - { - if (config_error) - error::g_log << "Error: " << config_error << std::endl; - } - ); - return; - } - - ctx.swap(ret_ctx); -} - - -void accept( - not_nullserver_ctx, - unique_fd &client_socket, // Socket ownership transfer - accepted_context &ctx -) -{ - struct tls *client_ctx = nullptr; - if (tls_accept_socket(server_ctx, &client_ctx, client_socket.get()) == -1) - { - error::occurred("TLS accept", error::none{}); - ctx.reset(); - return; - } - ctx.reset(accepted_context::value{client_socket.release(), client_ctx}); -} - - -span read_request(not_null ctx, std::array &buffer) -{ - span request{buffer}; - if (!read(ctx, request)) - return {}; - - for (auto current = request.begin(); current < request.end(); ++current) - { - const auto next = (current + 1); - if (next == request.end()) - break; - - if (*current == gemini::CRLF[0] && *next == gemini::CRLF[1]) - { - // > servers MUST ignore anything sent after the first occurrence of a . - return request.first(current - request.begin()); - } - } - error::occurred("Parse request", error::none{}); - return {}; -} - - -bool send_response(not_null ctx, gemini::status_t status, span meta) -{ - // > - std::array buff; - auto current = buff.begin(); - current = std::copy(status.cbegin(), status.cend(), current); - current = std::copy(gemini::SPACE.cbegin(), gemini::SPACE.cend(), current); - assert(meta.size() <= gemini::MAX_META_LENGTH); - current = std::copy(meta.begin(), meta.end(), current); - current = std::copy(gemini::CRLF.cbegin(), gemini::CRLF.cend(), current); - - return send(ctx, span{&buff[0], static_cast(current - buff.begin())}); -} - - -bool send(not_null ctx, span buff) -{ - ssize_t ret{0}; - while (buff.size() > 0) - { - ret = tls_write(ctx, buff.begin(), buff.size()); - if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) - continue; - break; - buff = buff.subspan(buff.size() - ret); - } - if (ret == -1) - { - const auto last_error = tls_error(ctx); - error::occurred( - "TLS write", - [last_error] - { - if (last_error) - error::g_log << "Error: " << last_error << std::endl; - } - ); - return false; - } - return true; -} - - -} // namespace transport -} // namespace vostok blob - f235e457c38976fd348742488c27f7759fb7dc42 (mode 644) blob + /dev/null --- shared/transport.h +++ /dev/null @@ -1,88 +0,0 @@ -/** Wrap libtls for gemini protocol */ - -#include "zstring" -#include "not_null" -#include "non_copiable" -#include "unique_fd" -#include "span" -#include "gemini.h" - -#include -#include - -#pragma once - - -namespace vostok -{ -namespace transport -{ - - -/** `struct tls *` smart pointer */ -typedef std::unique_ptr context_t; - - -/** Pair: `struct tls *` pointer and file descriptor as smart pointer */ -class accepted_context : non_copiable -{ -public: - struct value - { - int m_fd; - struct tls *m_ctx; - value(int fd = -1, struct tls *ctx = nullptr) : m_fd{fd}, m_ctx{ctx} {} - }; - - accepted_context(int fd = -1, struct tls *ctx = nullptr) { reset(value{fd, ctx}); } - accepted_context(value v) { reset(v); } - - struct tls *get_ctx() const { return m_ctx.get(); } - int get_fd() const { return m_fd.get(); } - - value get() const { return value{m_fd.get(), m_ctx.get()}; } - value release() { return value{m_fd.release(), m_ctx.release()}; } - void reset(value v=value{-1, nullptr}) - { - m_fd.reset(v.m_fd); - m_ctx.reset(v.m_ctx); - assert((m_fd && m_ctx) || (!m_fd && !m_ctx)); - } - operator bool () const { return m_fd && m_ctx; } - -private: - unique_fd m_fd; - context_t m_ctx{nullptr, tls_free}; -}; - - -/** Per-process initialization */ -bool init(); - - -/* Create new TLS context for gemini server */ -void create_server(not_null cert_file, not_null key_file, context_t &ctx); - - -/** Accept new client. Socket ownership transfer */ -void accept( - not_nullserver_ctx, - unique_fd &client_socket, // Socket ownership transfer - accepted_context &ctx -); - - -/** Read genimi request and return url (empty url - error) */ -span read_request(not_null ctx, std::array &buffer); - - -/** Write gemini response */ -bool send_response(not_null ctx, gemini::status_t status, span meta); - - -/** Send raw bytes */ -bool send(not_null ctx, span buff); - - -} // namespace transport -} // namespace vostok blob - e791df4a9bb41ae6998d77d98b0702074eda9ecc (mode 644) blob + /dev/null --- shared/unique_fd +++ /dev/null @@ -1,40 +0,0 @@ -/** Smart pointer for file descriptor */ - -#include -#include - -#pragma once - - -namespace vostok -{ - - -class unique_fd : non_copiable -{ -public: - explicit unique_fd(int fd = -1) : m_fd(fd) {} - ~unique_fd(){ reset();} - - operator bool() const {return m_fd != -1;} - - int get() const { return m_fd; } - void reset(int fd = -1) - { - if (*this) - close(m_fd); - m_fd = fd; - } - int release() - { - const auto fd = m_fd; - m_fd = -1; - return fd; - } - -private: - int m_fd; -}; - - -} // namespace vostok blob - 72f350aae70828f2d30d40217c557c58f297dba5 (mode 644) blob + /dev/null --- shared/zstring +++ /dev/null @@ -1,14 +0,0 @@ -/** C-style (null-terminated) string */ - - -#pragma once - - -namespace vostok -{ - - -using czstring=const char *; - - -} // namespace vostok blob - /dev/null blob + 9ac28c40d073d08304d261ac84db6659466f4c1f (mode 644) --- /dev/null +++ vostok/Makefile @@ -0,0 +1,18 @@ +CXXFLAGS = -Wall -Wextra -std=c++11 -I../shared +LIBS = -ltls + +CXXFILES = transport.cc +HXXFILES = transport.h +CXXFILES += error.cc +HXXFILES += error.h +CXXFILES += gemini.cc +HXXFILES += gemini.h +CXXFILES += args.cc +HXXFILES += args.h +CXXFILES += parse_url.cc +HXXFILES += parse_url.h +CXXFILES += vostok.cc +HXXFILES += utils.h + +vostok: ${CXXFILES} ${HXXFILES} + ${CXX} ${CXXFLAGS} ${CXXFILES} ${LIBS} -o vostok blob - /dev/null blob + 94b5019fb6f0bd231cc26ba512811ea27082155b (mode 644) --- /dev/null +++ vostok/args.cc @@ -0,0 +1,100 @@ +/** Parse command line arguments */ + +#include "args.h" +#include "error.h" + +#include +#include + + +namespace vostok +{ +namespace +{ + +/** Print usage and return false */ +bool usage(const char *program) +{ + const char *file_name = strrchr(program, '/'); + if (!file_name) + file_name = program; + else + ++file_name; + + error::g_log << "Usage: " << file_name << " [OPTS]" << std::endl << std::endl; + error::g_log << "OPTS may be: " << std::endl; + error::g_log << "\t-a ADDR : listening network address (127.0.0.1 by default)" << std::endl; + error::g_log << "\t-p PORT : TCP/IP port number (1965 by default)" << std::endl; + error::g_log << "\t-c FILE : Server certificate file [REQUIRED]" << std::endl; + error::g_log << "\t-k FILE : Server key file [REQUIRED]" << std::endl; + error::g_log << "\t-f PATH : Path to file system data [REQUIRED]" << std::endl; + + return false; +} + +} // namespace + + +bool command_line_arguments::parse(int argc, char *argv[]) +{ + int ch; + char *p = nullptr; + while ((ch = getopt(argc, argv, "a:p:c:k:f:")) != -1) { + switch (ch) { + case 'a': + m_addr = optarg; + break; + case 'p': + p = nullptr; + m_port = std::strtoul(optarg, &p, 10); + if (!m_port) + { + error::g_log << "Invalid PORT value: " << optarg << std::endl; + return usage(argv[0]); + } + break; + case 'c': + m_cert_file = optarg; + break; + case 'k': + m_key_file = optarg; + break; + case 'f': + m_directory.reset(open(optarg, O_RDONLY)); + if (!m_directory) + { + error::occurred( + [] + { + error::g_log << "Open directory \"" << optarg << "\""; + }, + error::print{} + ); + return false; + } + break; + + default: + return usage(argv[0]); + } + } + if (!m_cert_file) + { + error::g_log << "Invalid command line: -c option required" << std::endl; + return usage(argv[0]); + } + if (!m_key_file) + { + error::g_log << "Invalid command line: -k option required" << std::endl; + return usage(argv[0]); + } + if (!m_directory) + { + error::g_log << "Invalid command line: -d option required" << std::endl; + return usage(argv[0]); + } + return true; +} + + +} // namespace vostok blob - /dev/null blob + 7f7b8682627a3b5ff65838d3724972184ead7e07 (mode 644) --- /dev/null +++ vostok/args.h @@ -0,0 +1,25 @@ +/** Parse command line arguments */ + + +#pragma once + +#include "utils.h" + + +namespace vostok +{ + + +struct command_line_arguments +{ + not_null m_addr{"127.0.0.1"}; + int m_port{1965}; + czstring m_cert_file{nullptr}; + czstring m_key_file{nullptr}; + unique_fd m_directory; + + bool parse(int argc, char *argv[]); +}; + + +} // namespace vostok blob - /dev/null blob + 4991b214435143b06a885cc1ce776bb84fdd5b9b (mode 644) --- /dev/null +++ vostok/error.cc @@ -0,0 +1,16 @@ +/** Errors handling */ + +#include +#include "error.h" + + +namespace vostok +{ +namespace error +{ + +std::ostream &g_log = std::cerr; + + +} // namespace error +} // namespace vostok blob - /dev/null blob + a1791a32da6f24b2d3a3060d95fb971e744eaecd (mode 644) --- /dev/null +++ vostok/error.h @@ -0,0 +1,73 @@ +/** Errors handling */ + +#include +#include +#include + +#pragma once + + +namespace vostok +{ +namespace error +{ + + +/** Current output log stream (`std::err` on starup) */ +extern std::ostream &g_log; + + +/** Default error code (errno) printer */ +struct print +{ + const int m_error; + explicit print(int error=errno) : m_error{error} {} + void operator() () const + { + g_log << "Error code: " << std::dec << m_error << ". " << strerror(m_error) << std::endl; + } +}; + + +/** Empty error code printer */ +struct none +{ + void operator() () const {} +}; + + +/** Error handler: print action, print error */ +template< + typename TPrintAction, + typename TPrintError +> +void +occurred( + TPrintAction print_action, + TPrintError print_error +) +{ + print_action(); + g_log << " failed." << std::endl; + print_error(); +} + +template +void +occurred( + const char *action, + TPrintError print_error +) +{ + occurred( + [action] + { + g_log << action; + }, + print_error + ); +} + + +} // namespace error +} // namespace vostok blob - /dev/null blob + a99927eecbfa09bd8a7e7ed7903d4d3205a28c69 (mode 644) --- /dev/null +++ vostok/gemini.cc @@ -0,0 +1,24 @@ +/** Gemini protocol specifications */ + +#include "gemini.h" + +namespace vostok +{ +namespace gemini +{ + + +const std::array CRLF{'\r', '\n'}; + +const std::array SPACE{' '}; + +const status_t STATUS_20_SUCCESS{'2', '0'}; +const status_t STATUS_40_TEMPORARY_FAILURE{'4', '0'}; +const status_t STATUS_50_PERMANENT_FAILURE{'5', '0'}; +const status_t STATUS_51_NOT_FOUND{'5', '1'}; +const status_t STATUS_53_PROXY_REQUEST_REFUSED{'5', '3'}; +const status_t STATUS_59_BAD_REQUEST{'5', '9'}; + + +} // namespace gemini +} // namespace vostok blob - /dev/null blob + 3b9b02c570f7adc25dcbcbb98661afa9c95160af (mode 644) --- /dev/null +++ vostok/gemini.h @@ -0,0 +1,32 @@ +/** Gemini protocol specifications */ + +#include + +#pragma once + + +namespace vostok +{ +namespace gemini +{ + + +constexpr auto MAX_URL_LENGTH = 1024; +constexpr auto MAX_META_LENGTH = 1024; + +extern const std::array CRLF; +extern const std::array SPACE; + +using status_t = std::array; +extern const status_t STATUS_20_SUCCESS; +extern const status_t STATUS_40_TEMPORARY_FAILURE; +extern const status_t STATUS_50_PERMANENT_FAILURE; +extern const status_t STATUS_51_NOT_FOUND; +extern const status_t STATUS_53_PROXY_REQUEST_REFUSED; +extern const status_t STATUS_59_BAD_REQUEST; + +constexpr auto MAX_REQUEST_LENGTH = MAX_URL_LENGTH + CRLF.size(); + + +} // namespace gemini +} // namespace vostok blob - /dev/null blob + afe7698e1348e587ce929c920f9b917f807c6a81 (mode 644) --- /dev/null +++ vostok/parse_url.cc @@ -0,0 +1,137 @@ +/** URL normalization */ + +#include "parse_url.h" +#include "error.h" + +#include + + +namespace vostok +{ + +namespace +{ + +const auto gemini_scheme = cut_null("gemini://"); + + +class path_normalization +{ +public: + using path_components_t = std::list< span >; + + url_normalization_result operator() (span url_path, zs_url_path_t &zs_url_path); + +protected: + url_normalization_result on_component(); + url_normalization_result on_component_ok() + { + m_inprogress = decltype(m_inprogress){}; + return url_ok; + } + + void fill(zs_url_path_t &zs_url_path) const; + +private: + path_components_t::value_type m_inprogress; + path_components_t m_result; +}; + + +url_normalization_result path_normalization::operator() (span url_path, zs_url_path_t &zs_url_path) +{ + m_inprogress = path_components_t::value_type{nullptr, 0}; + m_result.clear(); + + for (auto p = url_path.begin(); p != url_path.end(); ++p) + { + if (*p != '/') + { + if (m_inprogress.size()) + m_inprogress = decltype(m_inprogress){m_inprogress.begin(), m_inprogress.size() + 1}; + else + m_inprogress = decltype(m_inprogress){p, 1}; + continue; + } + + const auto parse_result = on_component(); + if (parse_result != url_ok) + return parse_result; + } + const auto parse_result = on_component(); + if (parse_result != url_ok) + return parse_result; + + fill(zs_url_path); + return url_ok; +} + + +url_normalization_result path_normalization::on_component() +{ + if ((m_inprogress.size() == 0) || (m_inprogress.size() == 1 && m_inprogress[0] == '.')) + { + return on_component_ok(); + } + if (m_inprogress.size() == 2 && m_inprogress[0] == '.' && m_inprogress[1] == '.') + { + if (m_result.empty()) + return url_root_traverse; + + m_result.pop_back(); + return on_component_ok(); + } + + m_result.push_back(std::move(m_inprogress)); + return on_component_ok(); +} + +void path_normalization::fill(zs_url_path_t &zs_url_path) const +{ + auto current = zs_url_path.begin(); + for (auto it = m_result.cbegin(); it != m_result.cend(); ++it) + { + if (current != zs_url_path.begin()) + { + // non-first path component: insert separator + *current = '/'; + ++current; + } + current = std::copy(it->begin(), it->end(), current); + } + *current = '\0'; +} + +} // namespace + + +url_normalization_result parse_url(span url, zs_url_path_t &zs_url_path) +{ + // check and skip scheme + if (url.size() < gemini_scheme.size()) + return url_too_short; + if (!std::equal(gemini_scheme.begin(), gemini_scheme.end(), url.begin())) + return url_non_gemini; + url = url.subspan(gemini_scheme.size()); + + // skip domain[:port] + const char *current = url.begin(); + for (; current != url.end(); ++current) + { + if (*current == '/') + { + const auto skip_len = (current + 1) - url.begin(); + url = url.subspan(skip_len); + break; + } + } + if (current == url.end()) + url = decltype(url){}; + + // normalize '.' and '..' + path_normalization normalizer; + return normalizer(url, zs_url_path); +} + + +} // namespace vostok blob - /dev/null blob + d79fc3e17d319c79462467e62c21625b3a7ba0a8 (mode 644) --- /dev/null +++ vostok/parse_url.h @@ -0,0 +1,30 @@ +/** URL normalization */ + +#include "utils.h" +#include "gemini.h" + + +#pragma once + + +namespace vostok +{ + + +enum url_normalization_result +{ + url_ok, + + url_too_short, + url_non_gemini, + url_root_traverse, +}; + +/** Zero-terminated path from gemini URL */ +using zs_url_path_t = std::array; + +/** Extract normalized path from URL as list null-terminated string */ +url_normalization_result parse_url(span url, zs_url_path_t &zs_url_path); + + +} // namespace vostok blob - /dev/null blob + d80d60dae0f1e151f0e6f93adba30d597190355f (mode 644) --- /dev/null +++ vostok/transport.cc @@ -0,0 +1,207 @@ +/** Wrap libtls for gemini protocol */ + +#include "error.h" +#include "transport.h" + + +namespace vostok +{ +namespace transport +{ +namespace +{ + +using config_t = std::unique_ptr; +constexpr auto protocols = TLS_PROTOCOL_TLSv1_2 | TLS_PROTOCOL_TLSv1_3; + + +bool read(not_null ctx, span &buff) +{ + ssize_t ret{}; + for (; ; ) + { + ret = tls_read(ctx, buff.begin(), buff.size()); + if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) + continue; + break; + } + if (ret == -1) + { + const auto last_error = tls_error(ctx); + error::occurred( + "TLS read", + [last_error] + { + if (last_error) + error::g_log << "Error: " << last_error << std::endl; + } + ); + return false; + } + buff = buff.first(ret); + return true; +} + + +} // namespace + + +bool init() +{ + if (tls_init() == -1) + { + error::occurred("TLS initialization", error::none{}); + return false; + } + + return true; +} + + +void create_server(not_null cert_file, not_null key_file, context_t &ret_ctx) +{ + config_t cfg{tls_config_new(), tls_config_free}; + if (!cfg) + { + error::occurred("Create TLS configuration", error::none{}); + return; + } + + if (tls_config_set_protocols(cfg.get(), protocols) == -1) + { + error::occurred("Allow TLS 1.2 and TLS 1.3 protocols", error::none{}); + return; + } + if (tls_config_set_cert_file(cfg.get(), cert_file) == -1) + { + error::occurred( + [cert_file] + { + error::g_log << "Load certificate file " << cert_file; + }, + error::none{} + ); + return; + } + if (tls_config_set_key_file(cfg.get(), key_file) == -1) + { + error::occurred( + [key_file] + { + error::g_log << "Load key file " << key_file; + }, + error::none{} + ); + return; + } + + context_t ctx{tls_server(), tls_free}; + if (!ctx) + { + error::occurred("Create TLS server context", error::none{}); + return; + } + + if (tls_configure(ctx.get(), cfg.get()) == -1) + { + auto config_error = tls_config_error(cfg.get()); + error::occurred( + "Configure TLS context", + [config_error] + { + if (config_error) + error::g_log << "Error: " << config_error << std::endl; + } + ); + return; + } + + ctx.swap(ret_ctx); +} + + +void accept( + not_nullserver_ctx, + unique_fd &client_socket, // Socket ownership transfer + accepted_context &ctx +) +{ + struct tls *client_ctx = nullptr; + if (tls_accept_socket(server_ctx, &client_ctx, client_socket.get()) == -1) + { + error::occurred("TLS accept", error::none{}); + ctx.reset(); + return; + } + ctx.reset(accepted_context::value{client_socket.release(), client_ctx}); +} + + +span read_request(not_null ctx, std::array &buffer) +{ + span request{buffer}; + if (!read(ctx, request)) + return {}; + + for (auto current = request.begin(); current < request.end(); ++current) + { + const auto next = (current + 1); + if (next == request.end()) + break; + + if (*current == gemini::CRLF[0] && *next == gemini::CRLF[1]) + { + // > servers MUST ignore anything sent after the first occurrence of a . + return request.first(current - request.begin()); + } + } + error::occurred("Parse request", error::none{}); + return {}; +} + + +bool send_response(not_null ctx, gemini::status_t status, span meta) +{ + // > + std::array buff; + auto current = buff.begin(); + current = std::copy(status.cbegin(), status.cend(), current); + current = std::copy(gemini::SPACE.cbegin(), gemini::SPACE.cend(), current); + assert(meta.size() <= gemini::MAX_META_LENGTH); + current = std::copy(meta.begin(), meta.end(), current); + current = std::copy(gemini::CRLF.cbegin(), gemini::CRLF.cend(), current); + + return send(ctx, span{&buff[0], static_cast(current - buff.begin())}); +} + + +bool send(not_null ctx, span buff) +{ + ssize_t ret{0}; + while (buff.size() > 0) + { + ret = tls_write(ctx, buff.begin(), buff.size()); + if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) + continue; + break; + buff = buff.subspan(buff.size() - ret); + } + if (ret == -1) + { + const auto last_error = tls_error(ctx); + error::occurred( + "TLS write", + [last_error] + { + if (last_error) + error::g_log << "Error: " << last_error << std::endl; + } + ); + return false; + } + return true; +} + + +} // namespace transport +} // namespace vostok blob - /dev/null blob + a193575fd5ec3e4fac5f36aa02d94ca060a2b444 (mode 644) --- /dev/null +++ vostok/transport.h @@ -0,0 +1,84 @@ +/** Wrap libtls for gemini protocol */ + +#include "utils.h" +#include "gemini.h" + +#include +#include + +#pragma once + + +namespace vostok +{ +namespace transport +{ + + +/** `struct tls *` smart pointer */ +typedef std::unique_ptr context_t; + + +/** Pair: `struct tls *` pointer and file descriptor as smart pointer */ +class accepted_context : non_copiable +{ +public: + struct value + { + int m_fd; + struct tls *m_ctx; + value(int fd = -1, struct tls *ctx = nullptr) : m_fd{fd}, m_ctx{ctx} {} + }; + + accepted_context(int fd = -1, struct tls *ctx = nullptr) { reset(value{fd, ctx}); } + accepted_context(value v) { reset(v); } + + struct tls *get_ctx() const { return m_ctx.get(); } + int get_fd() const { return m_fd.get(); } + + value get() const { return value{m_fd.get(), m_ctx.get()}; } + value release() { return value{m_fd.release(), m_ctx.release()}; } + void reset(value v=value{-1, nullptr}) + { + m_fd.reset(v.m_fd); + m_ctx.reset(v.m_ctx); + assert((m_fd && m_ctx) || (!m_fd && !m_ctx)); + } + operator bool () const { return m_fd && m_ctx; } + +private: + unique_fd m_fd; + context_t m_ctx{nullptr, tls_free}; +}; + + +/** Per-process initialization */ +bool init(); + + +/* Create new TLS context for gemini server */ +void create_server(not_null cert_file, not_null key_file, context_t &ctx); + + +/** Accept new client. Socket ownership transfer */ +void accept( + not_nullserver_ctx, + unique_fd &client_socket, // Socket ownership transfer + accepted_context &ctx +); + + +/** Read genimi request and return url (empty url - error) */ +span read_request(not_null ctx, std::array &buffer); + + +/** Write gemini response */ +bool send_response(not_null ctx, gemini::status_t status, span meta); + + +/** Send raw bytes */ +bool send(not_null ctx, span buff); + + +} // namespace transport +} // namespace vostok blob - /dev/null blob + 987e0eb0db0d3606adc002559eeaca9834890165 (mode 644) --- /dev/null +++ vostok/utils.h @@ -0,0 +1,141 @@ +/** C++ header-only utilites */ + +#include +#include +#include + +#include + + +#pragma once + + +namespace vostok +{ + + +/** C-style (null-terminated) string */ +using czstring=const char *; + + +/** Non-copiable objects */ +struct non_copiable +{ + non_copiable() = default; + non_copiable(const non_copiable &) = delete; + non_copiable &operator =(const non_copiable &) = delete; +}; + + +/** Restricts a pointer to only hold non-null values. */ +template +class not_null +{ +public: + not_null(T p) :m_p{p} { assert(m_p != nullptr); } + + T get() const {return m_p;} + operator T() const {return m_p;} + T operator->() const {return m_p;} + + + // prevents compilation when someone attempts to assign a null pointer constant + not_null(std::nullptr_t) = delete; + not_null& operator=(std::nullptr_t) = delete; + + // unwanted operators...pointers only point to single objects! + not_null& operator++() = delete; + not_null& operator--() = delete; + not_null operator++(int) = delete; + not_null operator--(int) = delete; + not_null& operator+=(std::ptrdiff_t) = delete; + not_null& operator-=(std::ptrdiff_t) = delete; + void operator[](std::ptrdiff_t) const = delete; + +private: + T m_p; +}; + + +/** Simply std::span implementation for C++11 */ +template +class span +{ +public: + using element_type = ElemT; + using iterator = ElemT *; + + + constexpr span() : m_p{nullptr}, m_count{0} {} + constexpr span(element_type *p, std::size_t count) : m_p{count ? p : nullptr}, m_count{count} {} + template + constexpr span(element_type (&arr)[N]) : m_p{arr}, m_count{N} {} + template + constexpr span(std::array &arr) : m_p{arr.data()}, m_count{N} {} + + constexpr std::size_t size() const {return m_count;} + + constexpr iterator begin() const noexcept { return m_p; } + constexpr iterator end() const noexcept { return m_p + size(); } + + span first(std::size_t count) const + { + assert(count <= m_count); + return span{m_p, count}; + } + span subspan(std::size_t offset) const + { + assert(offset <= m_count); + return (offset < m_count) ? span{m_p + offset, m_count - offset} : span{}; + } + + element_type &operator[](std::size_t idx) const + { + assert(idx < m_count); + return m_p[idx]; + } + +private: + element_type *m_p; + std::size_t m_count; +}; + + +/** Remove null terminating character and return as span */ +template +constexpr span cut_null(const char (&arr)[N]) +{ + static_assert(N > 0, "!(N > 0)"); + return span{arr, N - 1}; +} + + +/** Smart pointer for file descriptor */ +class unique_fd : non_copiable +{ +public: + explicit unique_fd(int fd = -1) : m_fd(fd) {} + ~unique_fd(){ reset();} + + operator bool() const {return m_fd != -1;} + + int get() const { return m_fd; } + void reset(int fd = -1) + { + if (*this) + close(m_fd); + m_fd = fd; + } + int release() + { + const auto fd = m_fd; + m_fd = -1; + return fd; + } + +private: + int m_fd; +}; + + +} // namespace vostok blob - /dev/null blob + f9a5c944c04a3461b69cbc83ccedfe9a3ff7c366 (mode 644) --- /dev/null +++ vostok/vostok.cc @@ -0,0 +1,270 @@ +/* Gemini server */ + +#include "error.h" +#include "transport.h" +#include "args.h" +#include "parse_url.h" + +#include +#include +#include +#include +#include + +#include +#include + + +namespace vostok +{ +namespace +{ + + +/** Remove null terminating character and return as span */ +template +constexpr span cut_null(const char (&arr)[N]) +{ + static_assert(N > 0, "!(N > 0)"); + return span{arr, N - 1}; +} + + +namespace meta +{ +const char sz_bad_request[] = "Bad request"; +const auto bad_request = cut_null(sz_bad_request); + +const char sz_url_too_short[] = "URL is too short"; +const auto url_too_short = cut_null(sz_url_too_short); + +const char sz_non_gemini[] = "No proxying to non-Gemini content"; +const auto non_gemini = cut_null(sz_non_gemini); + +const char sz_root_traverse[] = "Wrong traverse"; +const auto root_traverse = cut_null(sz_root_traverse); + +const char sz_not_found[] = "Not found"; +const auto not_found = cut_null(sz_not_found); + +const char sz_temporary_failure[] = "Temporary failure"; +const auto temporary_failure = cut_null(sz_temporary_failure); +} // namespace meta + + +void client_thread(transport::accepted_context::value raw_value, int directory_fd) +{ + const transport::accepted_context ctx{raw_value}; + assert(ctx); + + std::array buffer; + auto url = transport::read_request(ctx.get_ctx(), buffer); + if (!url.size()) + { + transport::send_response(ctx.get_ctx(), gemini::STATUS_59_BAD_REQUEST, meta::bad_request); + return; + } + + zs_url_path_t zs_url_path; + const auto parse_result = parse_url(url, zs_url_path); + switch (parse_result) + { + case url_too_short: + error::occurred("parse URL", []{error::g_log << meta::sz_url_too_short << "." << std::endl;}); + transport::send_response(ctx.get_ctx(), gemini::STATUS_59_BAD_REQUEST, meta::url_too_short); + return; + case url_non_gemini: + error::occurred("parse URL", []{error::g_log << meta::sz_non_gemini << "." << std::endl;}); + transport::send_response(ctx.get_ctx(), gemini::STATUS_53_PROXY_REQUEST_REFUSED, meta::non_gemini); + return; + case url_root_traverse: + error::occurred("parse URL", []{error::g_log << meta::sz_root_traverse << "." << std::endl;}); + transport::send_response(ctx.get_ctx(), gemini::STATUS_50_PERMANENT_FAILURE, meta::root_traverse); + return; + + case url_ok: + break; + } + + const unique_fd opened_file{openat(directory_fd, zs_url_path.data(), O_RDONLY)}; + if (!opened_file) + { + const auto error_code = errno; + error::occurred( + [&zs_url_path] + { + error::g_log << "Open file \"" << zs_url_path.data() << "\""; + }, + error::print{error_code} + ); + switch (error_code) + { + case ENOTDIR: + case ENOENT: + case EACCES: + case ELOOP: + case EISDIR: + case ENXIO: + transport::send_response(ctx.get_ctx(), gemini::STATUS_51_NOT_FOUND, meta::not_found); + break; + + default: + transport::send_response(ctx.get_ctx(), gemini::STATUS_40_TEMPORARY_FAILURE, meta::temporary_failure); + break; + } + return; + } + struct stat sb{}; + if (fstat(opened_file.get(), &sb) == -1) + { + error::occurred( + [&zs_url_path] + { + error::g_log << "Stat file \"" << zs_url_path.data() << "\""; + }, + error::print{} + ); + transport::send_response(ctx.get_ctx(), gemini::STATUS_40_TEMPORARY_FAILURE, meta::temporary_failure); + return; + } + if (S_ISDIR(sb.st_mode)) + { + error::occurred( + [&zs_url_path] + { + error::g_log << "Open file \"" << zs_url_path.data() << "\""; + }, + error::print{EISDIR} + ); + transport::send_response(ctx.get_ctx(), gemini::STATUS_51_NOT_FOUND, meta::not_found); + return; + } + + // > If is an empty string, the MIME type MUST default to "text/gemini; charset=utf-8". + transport::send_response(ctx.get_ctx(), gemini::STATUS_20_SUCCESS, {}); + for (; ; ) + { + const auto ret = read(opened_file.get(), buffer.data(), buffer.size()); + if (ret == -1) + { + error::occurred( + [&zs_url_path] + { + error::g_log << "Read file \"" << zs_url_path.data() << "\""; + }, + error::print{} + ); + return; + } + const auto readed = static_cast(ret); + if (readed) + { + if (!transport::send(ctx.get_ctx(), span{buffer.data(), readed})) + return; + } + if (readed < buffer.size()) + break; + } + error::g_log << "20 " << "\"" << zs_url_path.data() << "\"" << std::endl; +} + + +bool server_loop(int server_socket, not_nullctx, int directory_fd) +{ + error::g_log << "🚀 Vostok server listening..." << std::endl; + for (; ; ) + { + if (listen(server_socket, 1024) == -1) + { + error::occurred("Listen socket", error::print{}); + return false; + } + + struct sockaddr_in addr{}; + socklen_t addrlen = sizeof(addr); + unique_fd client_socket{accept(server_socket, (struct sockaddr *)&addr, &addrlen)}; + if (!client_socket) + { + error::occurred("Accept socket", error::print{}); + continue; + } + + transport::accepted_context client_ctx; + transport::accept(ctx, client_socket, client_ctx); + if (!client_ctx) + continue; + + try + { + std::thread{client_thread, client_ctx.get(), directory_fd}.detach(); + client_ctx.release(); // move ownership to thread + } + catch (const std::system_error &e) + { + error::occurred( + "Create client thread", + [&e] + { + error::g_log << "Error: " << std::dec << e.code() + << ". " << e.what() << std::endl; + } + ); + } + } + return true; +} + + +/** C++ entry point */ +bool main(const command_line_arguments &args) +{ + if (!transport::init()) + return false; + + transport::context_t ctx{nullptr, tls_free}; + transport::create_server(args.m_cert_file, args.m_key_file, ctx); + if (!ctx) + return false; + + const unique_fd server_socket{socket(AF_INET, SOCK_STREAM, 0)}; + if (!server_socket) + { + error::occurred("Create socket", error::print{}); + return false; + } + + struct sockaddr_in sockaddr{}; + sockaddr.sin_family = AF_INET; + sockaddr.sin_addr.s_addr = inet_addr(args.m_addr); + sockaddr.sin_port = htons(args.m_port); + + if (bind(server_socket.get(), (struct sockaddr *)&sockaddr, sizeof(sockaddr)) == -1) + { + error::occurred( + [&args] + { + error::g_log << "Bind to " << args.m_addr << ":" << std::dec << args.m_port; + }, + error::print{} + ); + return false; + } + + return server_loop(server_socket.get(), ctx.get(), args.m_directory.get()); +} + + +} // namespace +} // namespace vostok + + +extern "C" +int +main(int argc, char *argv[]) +{ + vostok::command_line_arguments args; + if (!args.parse(argc, argv)) + return 1; + + return vostok::main(args) ? 0 : 1; +} blob - 057a3719306c47d8e9a791269e7d309159e70709 (mode 644) blob + /dev/null --- vostokd/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -CXXFLAGS = -Wall -Wextra -std=c++11 -I../shared -LIBS = -ltls - -CXXFILES = vostokd.cc -HXXFILES = ../shared/not_null -HXXFILES += ../shared/zstring -HXXFILES += ../shared/unique_fd -HXXFILES += ../shared/non_copiable -HXXFILES += ../shared/unique_fd -HXXFILES += ../shared/span -HXXFILES += ../shared/cut_null -CXXFILES += ../shared/transport.cc -HXXFILES += ../shared/transport.h -CXXFILES += ../shared/error.cc -HXXFILES += ../shared/error.h -CXXFILES += ../shared/gemini.cc -HXXFILES += ../shared/gemini.h -CXXFILES += command_line_arguments.cc -HXXFILES += command_line_arguments.h -CXXFILES += url_normalization.cc -HXXFILES += url_normalization.h - -vostokd: ${CXXFILES} ${HXXFILES} - ${CXX} ${CXXFLAGS} ${CXXFILES} ${LIBS} -o vostokd blob - a07cc088e96481e3b281957882e83d37e0e73818 (mode 644) blob + /dev/null --- vostokd/command_line_arguments.cc +++ /dev/null @@ -1,100 +0,0 @@ -/** Parse command line arguments */ - -#include "command_line_arguments.h" -#include "error.h" - -#include -#include - - -namespace vostok -{ -namespace -{ - -/** Print usage and return false */ -bool usage(const char *program) -{ - const char *file_name = strrchr(program, '/'); - if (!file_name) - file_name = program; - else - ++file_name; - - error::g_log << "Usage: " << file_name << " [OPTS]" << std::endl << std::endl; - error::g_log << "OPTS may be: " << std::endl; - error::g_log << "\t-a ADDR : listening network address (127.0.0.1 by default)" << std::endl; - error::g_log << "\t-p PORT : TCP/IP port number (1965 by default)" << std::endl; - error::g_log << "\t-c FILE : Server certificate file [REQUIRED]" << std::endl; - error::g_log << "\t-k FILE : Server key file [REQUIRED]" << std::endl; - error::g_log << "\t-f PATH : Path to file system data [REQUIRED]" << std::endl; - - return false; -} - -} // namespace - - -bool command_line_arguments::parse_command_line(int argc, char *argv[]) -{ - int ch; - char *p = nullptr; - while ((ch = getopt(argc, argv, "a:p:c:k:f:")) != -1) { - switch (ch) { - case 'a': - m_addr = optarg; - break; - case 'p': - p = nullptr; - m_port = std::strtoul(optarg, &p, 10); - if (!m_port) - { - error::g_log << "Invalid PORT value: " << optarg << std::endl; - return usage(argv[0]); - } - break; - case 'c': - m_cert_file = optarg; - break; - case 'k': - m_key_file = optarg; - break; - case 'f': - m_directory.reset(open(optarg, O_RDONLY)); - if (!m_directory) - { - error::occurred( - [] - { - error::g_log << "Open directory \"" << optarg << "\""; - }, - error::print{} - ); - return false; - } - break; - - default: - return usage(argv[0]); - } - } - if (!m_cert_file) - { - error::g_log << "Invalid command line: -c option required" << std::endl; - return usage(argv[0]); - } - if (!m_key_file) - { - error::g_log << "Invalid command line: -k option required" << std::endl; - return usage(argv[0]); - } - if (!m_directory) - { - error::g_log << "Invalid command line: -d option required" << std::endl; - return usage(argv[0]); - } - return true; -} - - -} // namespace vostok blob - 8d96bc9af9bfdbfece9be42bba38e35a07bd9ee1 (mode 644) blob + /dev/null --- vostokd/command_line_arguments.h +++ /dev/null @@ -1,27 +0,0 @@ -/** Parse command line arguments */ - - -#pragma once - -#include "not_null" -#include "zstring" -#include "unique_fd" - - -namespace vostok -{ - - -struct command_line_arguments -{ - not_null m_addr{"127.0.0.1"}; - int m_port{1965}; - czstring m_cert_file{nullptr}; - czstring m_key_file{nullptr}; - unique_fd m_directory; - - bool parse_command_line(int argc, char *argv[]); -}; - - -} // namespace vostok blob - 786dcf7a7c39c38a3e601a0c8a7d966355dfc76a (mode 644) blob + /dev/null --- vostokd/url_normalization.cc +++ /dev/null @@ -1,138 +0,0 @@ -/** URL normalization */ - -#include "url_normalization.h" -#include "cut_null" -#include "error.h" - -#include - - -namespace vostok -{ - -namespace -{ - -const auto gemini_scheme = cut_null("gemini://"); - - -class path_normalization -{ -public: - using path_components_t = std::list< span >; - - url_normalization_result operator() (span url_path, zs_url_path_t &zs_url_path); - -protected: - url_normalization_result on_component(); - url_normalization_result on_component_ok() - { - m_inprogress = decltype(m_inprogress){}; - return url_ok; - } - - void fill(zs_url_path_t &zs_url_path) const; - -private: - path_components_t::value_type m_inprogress; - path_components_t m_result; -}; - - -url_normalization_result path_normalization::operator() (span url_path, zs_url_path_t &zs_url_path) -{ - m_inprogress = path_components_t::value_type{nullptr, 0}; - m_result.clear(); - - for (auto p = url_path.begin(); p != url_path.end(); ++p) - { - if (*p != '/') - { - if (m_inprogress.size()) - m_inprogress = decltype(m_inprogress){m_inprogress.begin(), m_inprogress.size() + 1}; - else - m_inprogress = decltype(m_inprogress){p, 1}; - continue; - } - - const auto parse_result = on_component(); - if (parse_result != url_ok) - return parse_result; - } - const auto parse_result = on_component(); - if (parse_result != url_ok) - return parse_result; - - fill(zs_url_path); - return url_ok; -} - - -url_normalization_result path_normalization::on_component() -{ - if ((m_inprogress.size() == 0) || (m_inprogress.size() == 1 && m_inprogress[0] == '.')) - { - return on_component_ok(); - } - if (m_inprogress.size() == 2 && m_inprogress[0] == '.' && m_inprogress[1] == '.') - { - if (m_result.empty()) - return url_root_traverse; - - m_result.pop_back(); - return on_component_ok(); - } - - m_result.push_back(std::move(m_inprogress)); - return on_component_ok(); -} - -void path_normalization::fill(zs_url_path_t &zs_url_path) const -{ - auto current = zs_url_path.begin(); - for (auto it = m_result.cbegin(); it != m_result.cend(); ++it) - { - if (current != zs_url_path.begin()) - { - // non-first path component: insert separator - *current = '/'; - ++current; - } - current = std::copy(it->begin(), it->end(), current); - } - *current = '\0'; -} - -} // namespace - - -url_normalization_result parse_url(span url, zs_url_path_t &zs_url_path) -{ - // check and skip scheme - if (url.size() < gemini_scheme.size()) - return url_too_short; - if (!std::equal(gemini_scheme.begin(), gemini_scheme.end(), url.begin())) - return url_non_gemini; - url = url.subspan(gemini_scheme.size()); - - // skip domain[:port] - const char *current = url.begin(); - for (; current != url.end(); ++current) - { - if (*current == '/') - { - const auto skip_len = (current + 1) - url.begin(); - url = url.subspan(skip_len); - break; - } - } - if (current == url.end()) - url = decltype(url){}; - - // normalize '.' and '..' - path_normalization normalizer; - return normalizer(url, zs_url_path); -} - - -} // namespace vostok blob - 7cda673779438908d7cb51350167a1a8c9d82c4c (mode 644) blob + /dev/null --- vostokd/url_normalization.h +++ /dev/null @@ -1,30 +0,0 @@ -/** URL normalization */ - -#include "span" -#include "gemini.h" - - -#pragma once - - -namespace vostok -{ - - -enum url_normalization_result -{ - url_ok, - - url_too_short, - url_non_gemini, - url_root_traverse, -}; - -/** Zero-terminated path from gemini URL */ -using zs_url_path_t = std::array; - -/** Extract normalized path from URL as list null-terminated string */ -url_normalization_result parse_url(span url, zs_url_path_t &zs_url_path); - - -} // namespace vostok blob - 72390f99d42cc57e7c7fba2e9f80a4437334c892 (mode 644) blob + /dev/null --- vostokd/vostokd.cc +++ /dev/null @@ -1,272 +0,0 @@ -/* Gemini server */ - -#include "error.h" -#include "transport.h" -#include "command_line_arguments.h" -#include "url_normalization.h" -#include "cut_null" -#include "unique_fd" - -#include -#include -#include -#include -#include - -#include -#include - - -namespace vostok -{ -namespace -{ - - -/** Remove null terminating character and return as span */ -template -constexpr span cut_null(const char (&arr)[N]) -{ - static_assert(N > 0, "!(N > 0)"); - return span{arr, N - 1}; -} - - -namespace meta -{ -const char sz_bad_request[] = "Bad request"; -const auto bad_request = cut_null(sz_bad_request); - -const char sz_url_too_short[] = "URL is too short"; -const auto url_too_short = cut_null(sz_url_too_short); - -const char sz_non_gemini[] = "No proxying to non-Gemini content"; -const auto non_gemini = cut_null(sz_non_gemini); - -const char sz_root_traverse[] = "Wrong traverse"; -const auto root_traverse = cut_null(sz_root_traverse); - -const char sz_not_found[] = "Not found"; -const auto not_found = cut_null(sz_not_found); - -const char sz_temporary_failure[] = "Temporary failure"; -const auto temporary_failure = cut_null(sz_temporary_failure); -} // namespace meta - - -void client_thread(transport::accepted_context::value raw_value, int directory_fd) -{ - const transport::accepted_context ctx{raw_value}; - assert(ctx); - - std::array buffer; - auto url = transport::read_request(ctx.get_ctx(), buffer); - if (!url.size()) - { - transport::send_response(ctx.get_ctx(), gemini::STATUS_59_BAD_REQUEST, meta::bad_request); - return; - } - - zs_url_path_t zs_url_path; - const auto parse_result = parse_url(url, zs_url_path); - switch (parse_result) - { - case url_too_short: - error::occurred("parse URL", []{error::g_log << meta::sz_url_too_short << "." << std::endl;}); - transport::send_response(ctx.get_ctx(), gemini::STATUS_59_BAD_REQUEST, meta::url_too_short); - return; - case url_non_gemini: - error::occurred("parse URL", []{error::g_log << meta::sz_non_gemini << "." << std::endl;}); - transport::send_response(ctx.get_ctx(), gemini::STATUS_53_PROXY_REQUEST_REFUSED, meta::non_gemini); - return; - case url_root_traverse: - error::occurred("parse URL", []{error::g_log << meta::sz_root_traverse << "." << std::endl;}); - transport::send_response(ctx.get_ctx(), gemini::STATUS_50_PERMANENT_FAILURE, meta::root_traverse); - return; - - case url_ok: - break; - } - - const unique_fd opened_file{openat(directory_fd, zs_url_path.data(), O_RDONLY)}; - if (!opened_file) - { - const auto error_code = errno; - error::occurred( - [&zs_url_path] - { - error::g_log << "Open file \"" << zs_url_path.data() << "\""; - }, - error::print{error_code} - ); - switch (error_code) - { - case ENOTDIR: - case ENOENT: - case EACCES: - case ELOOP: - case EISDIR: - case ENXIO: - transport::send_response(ctx.get_ctx(), gemini::STATUS_51_NOT_FOUND, meta::not_found); - break; - - default: - transport::send_response(ctx.get_ctx(), gemini::STATUS_40_TEMPORARY_FAILURE, meta::temporary_failure); - break; - } - return; - } - struct stat sb{}; - if (fstat(opened_file.get(), &sb) == -1) - { - error::occurred( - [&zs_url_path] - { - error::g_log << "Stat file \"" << zs_url_path.data() << "\""; - }, - error::print{} - ); - transport::send_response(ctx.get_ctx(), gemini::STATUS_40_TEMPORARY_FAILURE, meta::temporary_failure); - return; - } - if (S_ISDIR(sb.st_mode)) - { - error::occurred( - [&zs_url_path] - { - error::g_log << "Open file \"" << zs_url_path.data() << "\""; - }, - error::print{EISDIR} - ); - transport::send_response(ctx.get_ctx(), gemini::STATUS_51_NOT_FOUND, meta::not_found); - return; - } - - // > If is an empty string, the MIME type MUST default to "text/gemini; charset=utf-8". - transport::send_response(ctx.get_ctx(), gemini::STATUS_20_SUCCESS, {}); - for (; ; ) - { - const auto ret = read(opened_file.get(), buffer.data(), buffer.size()); - if (ret == -1) - { - error::occurred( - [&zs_url_path] - { - error::g_log << "Read file \"" << zs_url_path.data() << "\""; - }, - error::print{} - ); - return; - } - const auto readed = static_cast(ret); - if (readed) - { - if (!transport::send(ctx.get_ctx(), span{buffer.data(), readed})) - return; - } - if (readed < buffer.size()) - break; - } - error::g_log << "20 " << "\"" << zs_url_path.data() << "\"" << std::endl; -} - - -bool server_loop(int server_socket, not_nullctx, int directory_fd) -{ - error::g_log << "🚀 Vostok server listening..." << std::endl; - for (; ; ) - { - if (listen(server_socket, 1024) == -1) - { - error::occurred("Listen socket", error::print{}); - return false; - } - - struct sockaddr_in addr{}; - socklen_t addrlen = sizeof(addr); - unique_fd client_socket{accept(server_socket, (struct sockaddr *)&addr, &addrlen)}; - if (!client_socket) - { - error::occurred("Accept socket", error::print{}); - continue; - } - - transport::accepted_context client_ctx; - transport::accept(ctx, client_socket, client_ctx); - if (!client_ctx) - continue; - - try - { - std::thread{client_thread, client_ctx.get(), directory_fd}.detach(); - client_ctx.release(); // move ownership to thread - } - catch (const std::system_error &e) - { - error::occurred( - "Create client thread", - [&e] - { - error::g_log << "Error: " << std::dec << e.code() - << ". " << e.what() << std::endl; - } - ); - } - } - return true; -} - - -/** C++ entry point */ -bool main(const command_line_arguments &args) -{ - if (!transport::init()) - return false; - - transport::context_t ctx{nullptr, tls_free}; - transport::create_server(args.m_cert_file, args.m_key_file, ctx); - if (!ctx) - return false; - - const unique_fd server_socket{socket(AF_INET, SOCK_STREAM, 0)}; - if (!server_socket) - { - error::occurred("Create socket", error::print{}); - return false; - } - - struct sockaddr_in sockaddr{}; - sockaddr.sin_family = AF_INET; - sockaddr.sin_addr.s_addr = inet_addr(args.m_addr); - sockaddr.sin_port = htons(args.m_port); - - if (bind(server_socket.get(), (struct sockaddr *)&sockaddr, sizeof(sockaddr)) == -1) - { - error::occurred( - [&args] - { - error::g_log << "Bind to " << args.m_addr << ":" << std::dec << args.m_port; - }, - error::print{} - ); - return false; - } - - return server_loop(server_socket.get(), ctx.get(), args.m_directory.get()); -} - - -} // namespace -} // namespace vostok - - -extern "C" -int -main(int argc, char *argv[]) -{ - vostok::command_line_arguments args; - if (!args.parse_command_line(argc, argv)) - return 1; - - return vostok::main(args) ? 0 : 1; -}