Commit Diff


commit - 2f4289de81ec2f59955947bad61a6e9037183b63
commit + 75d552576b36e6005329689689467468eb3bebca
blob - 218df0d6e740a53293ed9e6279ada706563aa26d
blob + 28e9c68f37b47c27910971722aaf6032a9f213d6
--- .gitignore
+++ .gitignore
@@ -1,7 +1,7 @@
 syntax: glob
 cert/
 vostok/vostok
-tests/test_parse_url
+tests/test_request
 tests/test_open_file
 **/*.swp
 **/*.o
blob - d209b7c61c4cebf765b97b2a42a5b6ffd81b706b
blob + 0f3019466b5fcf8c9cc286c34515fa9ff67f0576
--- tests/Makefile
+++ tests/Makefile
@@ -1,16 +1,16 @@
 .PHONY: tests clean
 
-CXXFLAGS	+= -I../vostok
+CXXFLAGS	= -D_GLIBCXX_DEBUG -D_LIBCPP_DEBUG -DDEBUG -D_DEBUG -I../vostok
 
-tests: test_parse_url test_open_file
-	./test_parse_url
+tests: test_request test_open_file
+	./test_request
 	./test_open_file
 
-test_parse_url: test_parse_url.cc ../vostok/parse_url.cc ../vostok/parse_url.h
-	${CXX} ${CXXFLAGS} test_parse_url.cc ../vostok/parse_url.cc -o test_parse_url
+test_request: test_request.cc ../vostok/request.cc ../vostok/request.h ../vostok/gemini.cc
+	${CXX} ${CXXFLAGS} test_request.cc ../vostok/request.cc ../vostok/gemini.cc -o test_request
 
 test_open_file: test_open_file.cc ../vostok/open_file.cc ../vostok/open_file.h
 	${CXX} ${CXXFLAGS} test_open_file.cc ../vostok/open_file.cc -o test_open_file
 
 clean:
-	rm -f test_parse_url test_open_file
+	rm -f test_request test_open_file
blob - 3ae18a4e563aa1dbdc476b126a710a87fdc15ad2 (mode 644)
blob + /dev/null
--- tests/test_parse_url.cc
+++ /dev/null
@@ -1,76 +0,0 @@
-#include "parse_url.h"
-#include <string.h>
-#include <iostream>
-
-#include "tests.h"
-
-
-namespace vostok
-{
-namespace
-{
-template <std::size_t N>
-void init(std::vector<char> &v, const char (&arr)[N], bool cut_null=false)
-{
-    static_assert(N > 0, "N");
-    const auto n = cut_null ? (N - 1) : N;
-    v.resize(n);
-    std::copy(&arr[0], &arr[n], v.begin());
-}
-}   // namespace <unnamed>
-
-TEST_START(test_extract_url_path)
-    std::vector<char> url, expected;
-
-#define CASE_OK(url_literal, path_literal) \
-    init(url, url_literal, true); \
-    init(expected, path_literal); \
-    IS_TRUE(extract_url_path(url) == url_ok && (url.size() == expected.size() && std::equal(url.cbegin(), url.cend(), expected.cbegin())))
-
-    CASE_OK("gemini://host", "");
-    CASE_OK("gemini://host/", "");
-    CASE_OK("gemini://host/a", "a");
-    CASE_OK("gemini://host/a/", "a");
-    CASE_OK("gemini://host/a/b", "a/b");
-    CASE_OK("gemini://host/a/b/", "a/b");
-
-    CASE_OK("gemini://host:1965", "");
-    CASE_OK("gemini://host:1965/", "");
-    CASE_OK("gemini://host:1965/a", "a");
-    CASE_OK("gemini://host:1965/a/", "a");
-    CASE_OK("gemini://host:1965/a/b", "a/b");
-    CASE_OK("gemini://host:1965/a/b/", "a/b");
-
-    CASE_OK("gemini://host/a/b/../c/./d", "a/c/d");
-
-    // RFC 3986, 3.1. Scheme
-    // > ... An implementation
-    // > should accept uppercase letters as equivalent to lowercase in scheme
-    // > names (e.g., allow "HTTP" as well as "http")
-    CASE_OK("GeMiNi://host", "");
-
-#define CASE_ERROR(url_literal, expected_result) \
-    init(url, url_literal, true); \
-    IS_TRUE(extract_url_path(url) == expected_result)
-
-    CASE_ERROR("", url_too_short);
-    CASE_ERROR("g", url_too_short);
-    CASE_ERROR("gemini:/", url_too_short);
-
-    CASE_ERROR("gemini1://", url_non_gemini);
-    CASE_ERROR("semini://", url_non_gemini);
-
-    CASE_ERROR("gemini://host/../secret.txt", url_root_traverse);
-    CASE_ERROR("gemini://host/dir/../../secret.txt", url_root_traverse);
-
-TEST_END()
-
-}   // namespace vostok
-
-
-extern "C"
-int
-main(int, char **)
-{
-   return vostok::test_extract_url_path();
-}
blob - /dev/null
blob + 7a61653e524b22ec02a4d5d321f6ade1014aeb3e (mode 644)
--- /dev/null
+++ tests/test_request.cc
@@ -0,0 +1,85 @@
+#include "request.h"
+#include <string.h>
+#include <iostream>
+
+#include "tests.h"
+
+
+namespace vostok
+{
+namespace
+{
+template <std::size_t N>
+void fill_request_buffer(std::vector<char> &v, const char (&arr)[N])
+{
+    static_assert(N > 0, "N");
+    v.resize(N + 1);
+    auto p = std::copy(&arr[0], &arr[N - 1], v.begin());
+    *p = '\r';
+    ++p;
+    *p = '\n';
+    ++p;
+}
+}   // namespace <unnamed>
+
+TEST_START(test_request)
+    Request request;
+
+    auto &buffer = request.get_buffer();
+    IS_TRUE(buffer.size() == 1026);
+
+    fill_request_buffer(buffer, "gemini://host");
+    buffer[buffer.size() - 1] = '\0';
+    IS_TRUE(request.parse() == Request::BAD_REQUEST);
+
+#define CASE_ERROR(url_literal, expected_result) \
+    fill_request_buffer(request.get_buffer(), url_literal); \
+    IS_TRUE(request.parse() == expected_result)
+
+    CASE_ERROR("", Request::URL_TOO_SHORT);
+    CASE_ERROR("g", Request::URL_TOO_SHORT);
+    CASE_ERROR("gemini:/", Request::URL_TOO_SHORT);
+
+    CASE_ERROR("gemini1://", Request::URL_NON_GEMINI);
+    CASE_ERROR("semini://", Request::URL_NON_GEMINI);
+
+    CASE_ERROR("gemini://host/../secret.txt", Request::URL_ROOT_TRAVERSE);
+    CASE_ERROR("gemini://host/dir/../../secret.txt", Request::URL_ROOT_TRAVERSE);
+
+#define CASE_OK(url_literal, path_literal) \
+    fill_request_buffer(request.get_buffer(), url_literal); \
+    IS_TRUE(request.parse() == Request::URL_OK && !strcmp(request.get_path().data(), path_literal))
+
+    CASE_OK("gemini://host", "");
+    CASE_OK("gemini://host/", "");
+    CASE_OK("gemini://host/a", "a");
+    CASE_OK("gemini://host/a/", "a");
+    CASE_OK("gemini://host/a/b", "a/b");
+    CASE_OK("gemini://host/a/b/", "a/b");
+
+    CASE_OK("gemini://host:1965", "");
+    CASE_OK("gemini://host:1965/", "");
+    CASE_OK("gemini://host:1965/a", "a");
+    CASE_OK("gemini://host:1965/a/", "a");
+    CASE_OK("gemini://host:1965/a/b", "a/b");
+    CASE_OK("gemini://host:1965/a/b/", "a/b");
+
+    CASE_OK("gemini://host/a/b/../c/./d", "a/c/d");
+
+    // RFC 3986, 3.1. Scheme
+    // > ... An implementation
+    // > should accept uppercase letters as equivalent to lowercase in scheme
+    // > names (e.g., allow "HTTP" as well as "http")
+    CASE_OK("GeMiNi://host", "");
+
+TEST_END()
+
+}   // namespace vostok
+
+
+extern "C"
+int
+main(int, char **)
+{
+   return vostok::test_request();
+}
blob - 83054350c7bcd01ca738e4810455fc399ff75232
blob + 65f8d32addaca2f9201a5b5e06104c92c6b90769
--- vostok/Makefile
+++ vostok/Makefile
@@ -5,7 +5,7 @@ CXXFILES	= transport.cc
 CXXFILES	+= error.cc
 CXXFILES	+= gemini.cc
 CXXFILES	+= args.cc
-CXXFILES	+= parse_url.cc
+CXXFILES	+= request.cc
 CXXFILES	+= open_file.cc
 CXXFILES	+= vostok.cc
 OFILES		= ${CXXFILES:.cc=.o}
blob - d81f5fd62b340269f56acd085a42c8a107fe12e0 (mode 644)
blob + /dev/null
--- vostok/parse_url.cc
+++ /dev/null
@@ -1,156 +0,0 @@
-/** URL normalization */
-
-#include "parse_url.h"
-#include "error.h"
-
-#include <list>
-#include <utility>
-#include <cctype>
-
-
-namespace vostok
-{
-
-namespace
-{
-
-const auto gemini_scheme = cut_null("gemini://");
-inline bool is_gemini_scheme(const std::vector<char> &url)
-{
-    return std::equal(
-        gemini_scheme.begin(),
-        gemini_scheme.end(),
-        url.begin(),
-        [](char v1, char v2){return std::tolower(v1) == std::tolower(v2);}
-    );
-}
-
-class PathNormalization
-{
-public:
-    using Components = std::list< Span<const char> >;
-
-    url_normalization_result
-    operator() (
-        std::vector<char>::const_iterator p,
-        std::vector<char> &url
-    );
-
-protected:
-    url_normalization_result on_component();
-    url_normalization_result on_component_ok()
-    {
-        m_inprogress = decltype(m_inprogress){};
-        return url_ok;
-    }
-
-    void fill(std::vector<char> &url) const;
-
-private:
-    Components::value_type m_inprogress;
-    Components m_result;
-};
-
-
-url_normalization_result
-PathNormalization::operator() (
-    std::vector<char>::const_iterator p,
-    std::vector<char> &url
-)
-{
-    m_inprogress = Components::value_type{nullptr, 0};
-    m_result.clear();
-
-    for (; p != url.cend(); ++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(url);
-    return url_ok;
-}
-
-
-url_normalization_result PathNormalization::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 PathNormalization::fill(std::vector<char> &url) const
-{
-    auto current = url.begin();
-    for (auto it = m_result.cbegin(); it != m_result.cend(); ++it)
-    {
-        if (current != url.begin())
-        {
-            // non-first path component: insert separator
-            *current = '/';
-            ++current;
-        }
-        current = std::copy(it->begin(), it->end(), current);
-    }
-    *current = '\0';
-    ++current;
-
-    url.resize(current - url.begin());
-}
-
-}   // namespace <unnamed>
-
-
-url_normalization_result
-extract_url_path(
-    /* in/out */ std::vector<char> &url
-)
-{
-    // check and skip scheme
-    if (url.size() < gemini_scheme.size())
-        return url_too_short;
-    if (!is_gemini_scheme(url))
-        return url_non_gemini;
-    auto current = url.cbegin() + gemini_scheme.size();
-
-    // skip domain[:port]
-    for (; current != url.cend(); ++current)
-    {
-        if (*current == '/')
-        {
-            ++current;
-            break;
-        }
-    }
-
-    // normalize '.' and '..'
-    return PathNormalization{}(current, url);
-}
-
-
-}   // namespace vostok
blob - /dev/null
blob + 8a5780ade2c7ca8084f2837d2d5a24189be0f2d0 (mode 644)
--- /dev/null
+++ vostok/request.cc
@@ -0,0 +1,193 @@
+/** URL normalization */
+
+#include "request.h"
+#include "gemini.h"
+#include "error.h"
+#include "utils.h"
+
+#include <list>
+#include <cctype>
+
+
+namespace vostok
+{
+
+namespace
+{
+
+const auto gemini_scheme = cut_null("gemini://");
+inline bool is_gemini_scheme(const std::vector<char> &url)
+{
+    return std::equal(
+        gemini_scheme.begin(),
+        gemini_scheme.end(),
+        url.begin(),
+        [](char v1, char v2){return std::tolower(v1) == std::tolower(v2);}
+    );
+}
+
+
+bool cut_crlf(std::vector<char> &buffer)
+{
+    // > servers MUST ignore anything sent after the first occurrence of a <CR><LF>.
+    for (auto current = buffer.cbegin(); current != buffer.cend(); ++current)
+    {
+        const auto next = current + 1;
+        if (next == buffer.cend())
+            break;
+
+        if (*current == gemini::CRLF[0] && *next == gemini::CRLF[1])
+        {
+            buffer.resize(current - buffer.cbegin());
+            return true;
+        }
+    }
+    return false;
+}
+
+
+class PathNormalization
+{
+public:
+    using Components = std::list< Span<const char> >;
+
+    Request::ParseResult
+    operator() (
+        std::vector<char>::const_iterator p,
+        std::vector<char> &url
+    );
+
+protected:
+    Request::ParseResult on_component();
+    Request::ParseResult on_component_ok()
+    {
+        m_inprogress = decltype(m_inprogress){};
+        return Request::URL_OK;
+    }
+
+    void fill(std::vector<char> &url) const;
+
+private:
+    Components::value_type m_inprogress;
+    Components m_result;
+};
+
+
+Request::ParseResult
+PathNormalization::operator() (
+    std::vector<char>::const_iterator p,
+    std::vector<char> &url
+)
+{
+    m_inprogress = Components::value_type{nullptr, 0};
+    m_result.clear();
+
+    for (; p != url.cend(); ++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 != Request::URL_OK)
+            return parse_result;
+    }
+    const auto parse_result = on_component();
+    if (parse_result != Request::URL_OK)
+        return parse_result;
+
+    fill(url);
+    return Request::URL_OK;
+}
+
+
+Request::ParseResult PathNormalization::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 Request::URL_ROOT_TRAVERSE;
+
+        m_result.pop_back();
+        return on_component_ok();
+    }
+
+    m_result.push_back(std::move(m_inprogress));
+    return on_component_ok();
+}
+
+void PathNormalization::fill(std::vector<char> &url) const
+{
+    auto current = url.begin();
+    for (auto it = m_result.cbegin(); it != m_result.cend(); ++it)
+    {
+        if (current != url.begin())
+        {
+            // non-first path component: insert separator
+            *current = '/';
+            ++current;
+        }
+        current = std::copy(it->begin(), it->end(), current);
+    }
+    *current = '\0';
+    ++current;
+
+    url.resize(current - url.begin());
+}
+
+}   // namespace <unnamed>
+
+
+std::vector<char> &Request::get_buffer()
+{
+    m_buffer.resize(gemini::MAX_REQUEST_LENGTH);
+    return m_buffer;
+}
+
+
+Request::ParseResult Request::parse()
+{
+    if (!cut_crlf(m_buffer))
+        return BAD_REQUEST;
+
+    // check and skip scheme
+    if (m_buffer.size() < gemini_scheme.size())
+        return URL_TOO_SHORT;
+    if (!is_gemini_scheme(m_buffer))
+        return URL_NON_GEMINI;
+    auto current = m_buffer.cbegin() + gemini_scheme.size();
+
+    // skip domain[:port]
+    for (; current != m_buffer.cend(); ++current)
+    {
+        if (*current == '/')
+        {
+            ++current;
+            break;
+        }
+    }
+
+    // normalize '.' and '..'
+    return PathNormalization{}(current, m_buffer);
+}
+
+
+const std::vector<char> &Request::get_path() const
+{
+    assert(m_buffer.size() > 0);
+    assert(m_buffer.size() < gemini::MAX_REQUEST_LENGTH);
+    assert(m_buffer[m_buffer.size() - 1] == '\0');
+    return m_buffer;
+}
+
+
+}   // namespace vostok
blob - 4fece0d30541a02b52be7cfc58bc84d700aaaab7 (mode 644)
blob + /dev/null
--- vostok/parse_url.h
+++ /dev/null
@@ -1,32 +0,0 @@
-/** URL normalization */
-
-#include "utils.h"
-#include "gemini.h"
-
-#include <vector>
-
-
-#pragma once
-
-
-namespace vostok
-{
-
-
-enum url_normalization_result
-{
-    url_ok,
-
-    url_too_short,
-    url_non_gemini,
-    url_root_traverse,
-};
-
-/** Extract normalized path from URL as null-terminated string (inplace) */
-url_normalization_result
-extract_url_path(
-    /* in/out */ std::vector<char> &url
-);
-
-
-}   // namespace vostok
blob - /dev/null
blob + 980e20c255154fbbd82759f47d31da52140da1e2 (mode 644)
--- /dev/null
+++ vostok/request.h
@@ -0,0 +1,54 @@
+/** Gemini request parser */
+
+#include <vector>
+
+#pragma once
+
+
+namespace vostok
+{
+
+
+class Request
+{
+public:
+    /* Get buffer for incoming Gemini request */
+    std::vector<char> &get_buffer();
+
+    /* Parse incoming Gemini request */
+    enum ParseResult
+    {
+        URL_OK,
+
+        BAD_REQUEST,
+        URL_TOO_SHORT,
+        URL_NON_GEMINI,
+        URL_ROOT_TRAVERSE,
+    };
+    ParseResult parse();
+
+
+    /* Get normalized path (if parse() return URL_OK) as zero-terminated string */
+    const std::vector<char> &get_path() const;
+
+private:
+    std::vector<char> m_buffer;
+};
+
+enum url_normalization_result
+{
+    url_ok,
+
+    url_too_short,
+    url_non_gemini,
+    url_root_traverse,
+};
+
+/** Extract normalized path from URL as null-terminated string (inplace) */
+url_normalization_result
+extract_url_path(
+    /* in/out */ std::vector<char> &url
+);
+
+
+}   // namespace vostok
blob - 88f9a8d0e00e67af58036b1876e9f289344e7fcd
blob + ce1c426013b7734b3cb83baba2ea6915037fc386
--- vostok/transport.cc
+++ vostok/transport.cc
@@ -14,6 +14,7 @@ namespace transport
 namespace
 {
 
+
 constexpr auto protocols = TLS_PROTOCOL_TLSv1_2 | TLS_PROTOCOL_TLSv1_3;
 
 
@@ -40,26 +41,6 @@ class PrintIfError (private)
 };
 
 
-bool read(NotNull<struct tls *> ctx, std::vector<char> &buff)
-{
-    ssize_t ret{};
-    for (; ; )
-    {
-        ret = tls_read(ctx, buff.data(), buff.size());
-        if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT)
-            continue;
-        break;
-    }
-    if (ret == -1)
-    {
-        error::occurred("TLS read", PrintIfError{tls_error(ctx)});
-        return false;
-    }
-    buff.resize(ret);
-    return true;
-}
-
-
 }   // namespace <unnamed>
 
 
@@ -75,7 +56,7 @@ bool init()
 }
 
 
-void create_server(NotNull<czstring> cert_file, NotNull<czstring> key_file, ContextPtr &ret_ctx)
+Server::Server(NotNull<czstring> cert_file, NotNull<czstring> key_file)
 {
     ConfigPtr cfg{tls_config_new()};
     if (!cfg)
@@ -125,7 +106,7 @@ void create_server(NotNull<czstring> cert_file, NotNul
         return;
     }
 
-    ctx.swap(ret_ctx);
+    ctx.swap(*this);
 }
 
 
@@ -153,49 +134,34 @@ AcceptedClient::AcceptedClient(int server_socket, stru
 }
 
 
-bool read_request(NotNull<struct tls *> ctx, std::vector<char> &url)
+bool recv(
+    /* in */ NotNull<struct tls *> ctx,
+    /* in/out */ std::vector<char> &buff
+)
 {
-    // <URL><CR><LF>
-    url.resize(gemini::MAX_REQUEST_LENGTH);
-    if (!read(ctx, url))
-        return false;
-
-    for (auto current = url.begin(); current < url.end(); ++current)
+    ssize_t ret{};
+    for (; ; )
     {
-        const auto next = (current + 1);
-        if (next == url.end())
-            break;
-
-        if (*current == gemini::CRLF[0] && *next == gemini::CRLF[1])
-        {
-            // > servers MUST ignore anything sent after the first occurrence of a <CR><LF>.
-            url.resize(current - url.begin());
-            return true;
-        }
+        ret = tls_read(ctx, buff.data(), buff.size());
+        if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT)
+            continue;
+        break;
     }
-
-    error::occurred("Parse request", error::None{});
-    return false;
-}
-
-
-bool send_response(NotNull<struct tls *> ctx, gemini::Status status, Span<const char> meta)
-{
-    // <STATUS><SPACE><META><CR><LF>
-    if (!send(ctx, status))
+    if (ret == -1)
+    {
+        error::occurred("TLS read", PrintIfError{tls_error(ctx)});
         return false;
-    if (!send(ctx, gemini::SPACE))
-        return false;
-    if (meta.size())
-        if (!send(ctx, meta))
-            return false;
-    if (!send(ctx, gemini::CRLF))
-        return false;
+    }
+    buff.resize(ret);
     return true;
 }
 
 
-bool send(NotNull<struct tls *> ctx, Span<const char> buff)
+bool
+send(
+    /* in */ NotNull<struct tls *> ctx,
+    /* in */ Span<const char> buff
+)
 {
     ssize_t ret{0};
     while (buff.size() > 0)
blob - 102e54212f998f9aaa530cede0eef43433d2b84b
blob + e4e5c5bc74e604ca45c322c02d57bed199bdacdf
--- vostok/transport.h
+++ vostok/transport.h
@@ -1,7 +1,6 @@
 /** Wrap libtls for gemini protocol */
 
 #include "utils.h"
-#include "gemini.h"
 
 #include <vector>
 #include <tls.h>
@@ -29,7 +28,11 @@ bool init();
 
 
 /* Create new TLS context for gemini server */
-void create_server(NotNull<czstring> cert_file, NotNull<czstring> key_file, ContextPtr &server_ctx);
+class Server : public ContextPtr
+{
+public:
+    Server(NotNull<czstring> cert_file, NotNull<czstring> key_file);
+};
 
 
 /** Accept new client */
@@ -49,17 +52,20 @@ class AcceptedClient (private)
 };
 
 
-/** Read genimi request and return url */
-bool read_request(NotNull<struct tls *> ctx, std::vector<char> &url);
+/** Receive raw bytes */
+bool
+recv(
+    /* in */ NotNull<struct tls *> ctx,
+    /* in/out */ std::vector<char> &buff
+);
 
 
-/** Write gemini response */
-bool send_response(NotNull<struct tls *> ctx, gemini::Status status, Span<const char> meta);
-
-
 /** Send raw bytes */
-bool send(NotNull<struct tls *> ctx, Span<const char> buff);
+bool
+send(
+    /* in */ NotNull<struct tls *> ctx,
+    /* in */ Span<const char> buff
+);
 
-
 }   // namespace transport
 }   // namespace vostok<C-F12>
blob - c1151107c5b33b391e6681df5f6523245e2d71ed
blob + 95ed0e83b4699dcba4526fc6e971df47fb692a6c
--- vostok/vostok.cc
+++ vostok/vostok.cc
@@ -3,8 +3,9 @@
 #include "error.h"
 #include "transport.h"
 #include "args.h"
-#include "parse_url.h"
+#include "request.h"
 #include "open_file.h"
+#include "gemini.h"
 
 #include <sys/socket.h>
 #include <arpa/inet.h>
@@ -40,49 +41,67 @@ const char sz_temporary_failure[] = "Temporary failure
 const auto temporary_failure = cut_null(sz_temporary_failure);
 }   // namespace meta
 
+bool send_response(NotNull<struct tls *> ctx, gemini::Status status, Span<const char> meta)
+{
+    // <STATUS><SPACE><META><CR><LF>
+    if (!transport::send(ctx, status))
+        return false;
+    if (!transport::send(ctx, gemini::SPACE))
+        return false;
+    if (meta.size())
+    {
+        if (!transport::send(ctx, meta))
+            return false;
+    }
+    if (!transport::send(ctx, gemini::CRLF))
+        return false;
+    return true;
+}
 
+
+
 void client_thread(const transport::AcceptedClient *accepted_client, int directory_fd)
 {
     assert(accepted_client);
     std::unique_ptr<const transport::AcceptedClient> accepted_client_deleter{accepted_client};
 
-    std::vector<char> url;
-    if (!transport::read_request(accepted_client->get_ctx(), url))
-    {
-        transport::send_response(accepted_client->get_ctx(), gemini::STATUS_59_BAD_REQUEST, meta::bad_request);
+    Request request;
+    if (!transport::recv(accepted_client->get_ctx(), request.get_buffer()))
         return;
-    }
 
-    
-    const auto parse_result = extract_url_path(url);
+    const auto parse_result = request.parse();
     switch (parse_result)
     {
-    case url_too_short:
-        error::occurred("parse URL", []{error::g_log << meta::sz_url_too_short;});
-        transport::send_response(accepted_client->get_ctx(), gemini::STATUS_59_BAD_REQUEST, meta::url_too_short);
+    case Request::BAD_REQUEST:
+        error::occurred("Parse request", []{error::g_log << meta::sz_bad_request;});
+        send_response(accepted_client->get_ctx(), gemini::STATUS_59_BAD_REQUEST, meta::bad_request);
         return;
-    case url_non_gemini:
-        error::occurred("parse URL", []{error::g_log << meta::sz_non_gemini;});
-        transport::send_response(accepted_client->get_ctx(), gemini::STATUS_53_PROXY_REQUEST_REFUSED, meta::non_gemini);
+    case Request::URL_TOO_SHORT:
+        error::occurred("Parse request", []{error::g_log << meta::sz_url_too_short;});
+        send_response(accepted_client->get_ctx(), gemini::STATUS_59_BAD_REQUEST, meta::url_too_short);
         return;
-    case url_root_traverse:
-        error::occurred("parse URL", []{error::g_log << meta::sz_root_traverse;});
-        transport::send_response(accepted_client->get_ctx(), gemini::STATUS_50_PERMANENT_FAILURE, meta::root_traverse);
+    case Request::URL_NON_GEMINI:
+        error::occurred("Parse request", []{error::g_log << meta::sz_non_gemini;});
+        send_response(accepted_client->get_ctx(), gemini::STATUS_53_PROXY_REQUEST_REFUSED, meta::non_gemini);
         return;
+    case Request::URL_ROOT_TRAVERSE:
+        error::occurred("Parse request", []{error::g_log << meta::sz_root_traverse;});
+        send_response(accepted_client->get_ctx(), gemini::STATUS_50_PERMANENT_FAILURE, meta::root_traverse);
+        return;
 
-    case url_ok:
+    case Request::URL_OK:
         break;
     }
 
     UniqueFd opened_file{};
-    const auto open_file_result = open_file(directory_fd, url.data(), opened_file);
+    const auto open_file_result = open_file(directory_fd, request.get_path().data(), opened_file);
     switch (open_file_result)
     {
     case file_not_found:
-        transport::send_response(accepted_client->get_ctx(), gemini::STATUS_51_NOT_FOUND, meta::not_found);
+        send_response(accepted_client->get_ctx(), gemini::STATUS_51_NOT_FOUND, meta::not_found);
         return;
     case file_opening_error:
-        transport::send_response(accepted_client->get_ctx(), gemini::STATUS_40_TEMPORARY_FAILURE, meta::temporary_failure);
+        send_response(accepted_client->get_ctx(), gemini::STATUS_40_TEMPORARY_FAILURE, meta::temporary_failure);
         return;
 
     case file_opened:
@@ -91,7 +110,7 @@ void client_thread(const transport::AcceptedClient *ac
     }
 
     // > If <META> is an empty string, the MIME type MUST default to "text/gemini; charset=utf-8".
-    transport::send_response(accepted_client->get_ctx(), gemini::STATUS_20_SUCCESS, {});
+    send_response(accepted_client->get_ctx(), gemini::STATUS_20_SUCCESS, {});
 
     std::vector<char> buffer;
     buffer.resize(64 * 1024);
@@ -101,9 +120,9 @@ void client_thread(const transport::AcceptedClient *ac
         if (ret == -1)
         {
             error::occurred(
-                [&url]
+                [&request]
                 {
-                    error::g_log << "Read file \"" << url.data() << "\"";
+                    error::g_log << "Read file \"" << request.get_path().data() << "\"";
                 },
                 error::Print{}
             );
@@ -118,7 +137,7 @@ void client_thread(const transport::AcceptedClient *ac
         if (readed < buffer.size())
             break;
     }
-    error::g_log << "20 " << "\"" << url.data() << "\"" << std::endl;
+    error::g_log << "20 " << "\"" << request.get_path().data() << "\"" << std::endl;
 }
 
 
@@ -166,8 +185,7 @@ bool main(const CommandLineArguments &args)
     if (!transport::init())
         return false;
 
-    transport::ContextPtr server_ctx;
-    transport::create_server(args.cert_file, args.key_file, server_ctx);
+    transport::Server server_ctx{args.cert_file, args.key_file};
     if (!server_ctx)
         return false;