commit - bce8f2740bc3c255439257893c9c6f724feb4689
commit + bcf3fb81ce90f24602b958223d2aa1494ff05f23
blob - d3fd63b7230c38592410979565486532bae78ab1
blob + 659a2fa04d7960c078871d6a706cadc183397df8
--- .gitignore
+++ .gitignore
syntax: glob
cert/
-vostokd/vostokd
+vostok/vostok
**/*.swp
blob - acdaaf481179d4a060644166f0198c5d07b1e9d2
blob + ae7abe5252f36df871ce94207abed46800156dd8
--- Makefile
+++ Makefile
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
-# 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
-/** 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
-/** 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
-/** 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
-/** 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
-/** 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
-/** 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
-/** 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
-/** 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
-/** 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
-/** 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
-/** 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
-/** 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
+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
+/** 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
+/** 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
+/** 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
+/** 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
+/** 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
+/** 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
+/** 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
+/** 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
+/** 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
+/** 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
+/** 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
+/* 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
-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
-/** 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
-/** 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
-/** 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
-/** 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
-/* 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;
-}