Commit Diff


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 <std::size_t N>
-constexpr span<const char> cut_null(const char (&arr)[N])
-{
-    static_assert(N > 0, "!(N > 0)");
-    return span<const char>{arr, N - 1};
-}
-
-
-}   // vostok
blob - 4991b214435143b06a885cc1ce776bb84fdd5b9b (mode 644)
blob + /dev/null
--- shared/error.cc
+++ /dev/null
@@ -1,16 +0,0 @@
-/** Errors handling */
-
-#include <iostream>
-#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 <errno.h>
-#include <string.h>
-#include <ostream>
-
-#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<typename TPrintError>
-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<char, 2> CRLF{'\r', '\n'};
-
-const std::array<char, 1> 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 <array>
-
-#pragma once
-
-
-namespace vostok
-{
-namespace gemini
-{
-
-
-constexpr auto MAX_URL_LENGTH = 1024;
-constexpr auto MAX_META_LENGTH = 1024;
-
-extern const std::array<char, 2> CRLF;
-extern const std::array<char, 1> SPACE;
-
-using status_t = std::array<char, 2>;
-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 <cstddef>
-#include <cassert>
-
-
-#pragma once
-
-
-namespace vostok
-{
-
-template <class T>
-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 <cassert>
-#include <array>
-
-#pragma once
-
-
-namespace vostok
-{
-
-template<typename ElemT>
-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 <std::size_t N>
-    constexpr span(element_type (&arr)[N]) : m_p{arr}, m_count{N} {}
-    template <std::size_t N>
-    constexpr span(std::array<element_type, N> &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<element_type> first(std::size_t count) const
-    {
-        assert(count <= m_count);
-        return span<element_type>{m_p, count};
-    }
-    span<element_type> subspan(std::size_t offset) const
-    {
-        assert(offset <= m_count);
-        return (offset < m_count) ? span<element_type>{m_p + offset, m_count - offset} : span<element_type>{};
-    }
-
-    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<struct tls_config, decltype(&tls_config_free)>;
-constexpr auto protocols = TLS_PROTOCOL_TLSv1_2 | TLS_PROTOCOL_TLSv1_3;
-
-
-bool read(not_null<struct tls *> ctx, span<char> &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 <unnamed>
-
-
-bool init()
-{
-    if (tls_init() == -1)
-    {
-        error::occurred("TLS initialization", error::none{});
-        return false;
-    }
-
-    return true;
-}
-
-
-void create_server(not_null<czstring> cert_file, not_null<czstring> 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_null<struct tls *>server_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<char> read_request(not_null<struct tls *> ctx, std::array<char, gemini::MAX_REQUEST_LENGTH> &buffer)
-{
-    span<char> 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 <CR><LF>.
-            return request.first(current - request.begin());
-        }
-    }
-    error::occurred("Parse request", error::none{});
-    return {};
-}
-
-
-bool send_response(not_null<struct tls *> ctx, gemini::status_t status, span<const char> meta)
-{
-    // > <STATUS><SPACE><META><CR><LF>
-    std::array<char, status.size() + gemini::SPACE.size() + gemini::MAX_META_LENGTH + gemini::CRLF.size()> 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<char const>{&buff[0], static_cast<size_t>(current - buff.begin())});
-}
-
-
-bool send(not_null<struct tls *> ctx, span<const char> 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 <memory>
-#include <tls.h>
-
-#pragma once
-
-
-namespace vostok
-{
-namespace transport
-{
-
-
-/** `struct tls *` smart pointer */
-typedef std::unique_ptr<struct tls, decltype(&tls_free)> 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<czstring> cert_file, not_null<czstring> key_file, context_t &ctx);
-
-
-/** Accept new client. Socket ownership transfer */
-void accept(
-    not_null<struct tls *>server_ctx,
-    unique_fd &client_socket,           // Socket ownership transfer
-    accepted_context &ctx
-);
-
-
-/** Read genimi request and return url (empty url - error) */
-span<char> read_request(not_null<struct tls *> ctx, std::array<char, gemini::MAX_REQUEST_LENGTH> &buffer);
-
-
-/** Write gemini response */
-bool send_response(not_null<struct tls *> ctx, gemini::status_t status, span<const char> meta);
-
-
-/** Send raw bytes */
-bool send(not_null<struct tls *> ctx, span<const char> 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 <unistd.h>
-#include <non_copiable>
-
-#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 <unistd.h>
+#include <fcntl.h>
+
+
+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 <unnamed>
+
+
+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<czstring> 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 <iostream>
+#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 <errno.h>
+#include <string.h>
+#include <ostream>
+
+#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<typename TPrintError>
+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<char, 2> CRLF{'\r', '\n'};
+
+const std::array<char, 1> 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 <array>
+
+#pragma once
+
+
+namespace vostok
+{
+namespace gemini
+{
+
+
+constexpr auto MAX_URL_LENGTH = 1024;
+constexpr auto MAX_META_LENGTH = 1024;
+
+extern const std::array<char, 2> CRLF;
+extern const std::array<char, 1> SPACE;
+
+using status_t = std::array<char, 2>;
+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 <list>
+
+
+namespace vostok
+{
+
+namespace
+{
+
+const auto gemini_scheme = cut_null("gemini://");
+
+
+class path_normalization
+{
+public:
+    using path_components_t = std::list< span<const char> >;
+
+    url_normalization_result operator() (span<char> 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<char> 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 <unnamed>
+
+
+url_normalization_result parse_url(span<char> 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<char, gemini::MAX_URL_LENGTH + 1>;
+
+/** Extract normalized path from URL as list null-terminated string */
+url_normalization_result parse_url(span<char> 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<struct tls_config, decltype(&tls_config_free)>;
+constexpr auto protocols = TLS_PROTOCOL_TLSv1_2 | TLS_PROTOCOL_TLSv1_3;
+
+
+bool read(not_null<struct tls *> ctx, span<char> &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 <unnamed>
+
+
+bool init()
+{
+    if (tls_init() == -1)
+    {
+        error::occurred("TLS initialization", error::none{});
+        return false;
+    }
+
+    return true;
+}
+
+
+void create_server(not_null<czstring> cert_file, not_null<czstring> 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_null<struct tls *>server_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<char> read_request(not_null<struct tls *> ctx, std::array<char, gemini::MAX_REQUEST_LENGTH> &buffer)
+{
+    span<char> 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 <CR><LF>.
+            return request.first(current - request.begin());
+        }
+    }
+    error::occurred("Parse request", error::none{});
+    return {};
+}
+
+
+bool send_response(not_null<struct tls *> ctx, gemini::status_t status, span<const char> meta)
+{
+    // > <STATUS><SPACE><META><CR><LF>
+    std::array<char, status.size() + gemini::SPACE.size() + gemini::MAX_META_LENGTH + gemini::CRLF.size()> 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<char const>{&buff[0], static_cast<size_t>(current - buff.begin())});
+}
+
+
+bool send(not_null<struct tls *> ctx, span<const char> 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 <memory>
+#include <tls.h>
+
+#pragma once
+
+
+namespace vostok
+{
+namespace transport
+{
+
+
+/** `struct tls *` smart pointer */
+typedef std::unique_ptr<struct tls, decltype(&tls_free)> 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<czstring> cert_file, not_null<czstring> key_file, context_t &ctx);
+
+
+/** Accept new client. Socket ownership transfer */
+void accept(
+    not_null<struct tls *>server_ctx,
+    unique_fd &client_socket,           // Socket ownership transfer
+    accepted_context &ctx
+);
+
+
+/** Read genimi request and return url (empty url - error) */
+span<char> read_request(not_null<struct tls *> ctx, std::array<char, gemini::MAX_REQUEST_LENGTH> &buffer);
+
+
+/** Write gemini response */
+bool send_response(not_null<struct tls *> ctx, gemini::status_t status, span<const char> meta);
+
+
+/** Send raw bytes */
+bool send(not_null<struct tls *> ctx, span<const char> 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 <cstddef>
+#include <cassert>
+#include <array>
+
+#include <unistd.h>
+
+
+#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 T>
+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<typename ElemT>
+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 <std::size_t N>
+    constexpr span(element_type (&arr)[N]) : m_p{arr}, m_count{N} {}
+    template <std::size_t N>
+    constexpr span(std::array<element_type, N> &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<element_type> first(std::size_t count) const
+    {
+        assert(count <= m_count);
+        return span<element_type>{m_p, count};
+    }
+    span<element_type> subspan(std::size_t offset) const
+    {
+        assert(offset <= m_count);
+        return (offset < m_count) ? span<element_type>{m_p + offset, m_count - offset} : span<element_type>{};
+    }
+
+    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 <std::size_t N>
+constexpr span<const char> cut_null(const char (&arr)[N])
+{
+    static_assert(N > 0, "!(N > 0)");
+    return span<const char>{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 <fcntl.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include <thread>
+#include <vector>
+
+
+namespace vostok
+{
+namespace
+{
+
+
+/** Remove null terminating character and return as span */
+template <std::size_t N>
+constexpr span<const char> cut_null(const char (&arr)[N])
+{
+    static_assert(N > 0, "!(N > 0)");
+    return span<const char>{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<char, gemini::MAX_REQUEST_LENGTH> 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 <META> 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<size_t>(ret);
+        if (readed)
+        {
+            if (!transport::send(ctx.get_ctx(), span<const char>{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_null<struct tls *>ctx, 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 <unnamed>
+}   // 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 <unistd.h>
-#include <fcntl.h>
-
-
-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 <unnamed>
-
-
-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<czstring> 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 <list>
-
-
-namespace vostok
-{
-
-namespace
-{
-
-const auto gemini_scheme = cut_null("gemini://");
-
-
-class path_normalization
-{
-public:
-    using path_components_t = std::list< span<const char> >;
-
-    url_normalization_result operator() (span<char> 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<char> 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 <unnamed>
-
-
-url_normalization_result parse_url(span<char> 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<char, gemini::MAX_URL_LENGTH + 1>;
-
-/** Extract normalized path from URL as list null-terminated string */
-url_normalization_result parse_url(span<char> 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 <fcntl.h>
-#include <sys/stat.h>
-#include <sys/socket.h>
-#include <arpa/inet.h>
-#include <netinet/in.h>
-
-#include <thread>
-#include <vector>
-
-
-namespace vostok
-{
-namespace
-{
-
-
-/** Remove null terminating character and return as span */
-template <std::size_t N>
-constexpr span<const char> cut_null(const char (&arr)[N])
-{
-    static_assert(N > 0, "!(N > 0)");
-    return span<const char>{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<char, gemini::MAX_REQUEST_LENGTH> 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 <META> 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<size_t>(ret);
-        if (readed)
-        {
-            if (!transport::send(ctx.get_ctx(), span<const char>{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_null<struct tls *>ctx, 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 <unnamed>
-}   // 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;
-}