commit - 01105a5401d8b606c9d8327aa2ec2e48af826e55
commit + 3304ae77cac645c4050490bd7ce339bf7f8afc77
blob - 6800f1b3e5a99f6cf38f3a3471d80983148f3e73
blob + 0965f79a9250e3dfdcf7255ecaa0bff9b857c355
--- Makefile
+++ Makefile
${MAKE} -C tests clean
run: server
- ./vostok/vostok -c cert/server.crt -k cert/server.key -f ./
+ ./vostok/vostok -c cert/server.crt -k cert/server.key -f ./ -g vgi -e ./vgi.sh
tests:
${MAKE} -C tests
blob - 416d3c3bfef21603014cd82dd6340bc2ac608e36
blob + 67bde05c01a4010c32975a6e41ad5199f300fd40
--- README.gmi
+++ README.gmi
```
=> https://got.any-key.press/?action=summary&path=vostok.git vostok repository web frontend
-Latest version (git tag): v0.1.4
+Latest version (git tag): v0.2.0
=> capsule/vostok.png What is "vostok"?
```
* [v] make install
* [v] mime.types
* [v] redirect (31) "[.../]dir" to "[.../]dir/" (correctness of relative link)
+* [v] dynamic content: VGI (CGI-like)
* [ ] syslog(3)
-* [ ] SNI-based routing (Server Name Indication)
-* [ ] clang-tydi (lightweight standalone alternatives?)
-* [ ] man pages
-* [ ] Gemini redirect 3x (symbolic link on the file system/config?)
+* [ ] man pages (alternatives?)
* [ ] O_NONBLOCK (processing with a fixed number of threads)
* [ ] pledge(2) / unveil(2)
blob - 353cef1cf625b5329877f12bc183fb59a1ccf013
blob + d4e2e36973fadbdcc97e6a2063f9c64aeb638825
--- tests/test_open_file.cc
+++ tests/test_open_file.cc
{
std::ostream dev_null{nullptr};
std::ostream &g_log = dev_null;
-} // namespace <unnamed>
+} // namespace error
TEST_START(test_open_file)
std::ostringstream ss;
blob - 13d42a89a2991acc499aef91057a92efaf9044db
blob + 0f8797281a217416a5342ab8e2ffe68d336ee0d6
--- tests/test_request.cc
+++ tests/test_request.cc
namespace vostok
{
+namespace error
+{
+std::ostream &g_log = std::cout;
+} // namespace error
+
namespace
{
template <std::size_t N>
} // namespace <unnamed>
TEST_START(test_request)
+ Span<const char> no_prefix;
Request request;
std::string path;
{
auto &buffer = fill_request_buffer(request, "gemini://host");
buffer[buffer.size() - 1] = '\0';
- IS_TRUE(request.parse(path) == Request::BAD_REQUEST);
+ IS_TRUE(request.parse(no_prefix, path) == Request::BAD_REQUEST);
}
#define CASE_ERROR(url_literal, expected_result) \
fill_request_buffer(request, url_literal); \
- IS_TRUE(request.parse(path) == expected_result)
+ IS_TRUE(request.parse(no_prefix, path) == expected_result)
CASE_ERROR("", Request::URL_TOO_SHORT);
CASE_ERROR("g", Request::URL_TOO_SHORT);
#define CASE_OK(url_literal, path_literal) \
fill_request_buffer(request, url_literal); \
- IS_TRUE(request.parse(path) == Request::URL_OK); \
+ IS_TRUE(request.parse(no_prefix, path) == Request::URL_OK); \
IS_TRUE(path == path_literal)
CASE_OK("gemini://host", "");
CASE_OK("gemini://host:1965/a/b", "a/b");
CASE_OK("gemini://host:1965/a/b/", "a/b/");
+ CASE_OK("gemini://host.org:1965", "");
+ CASE_OK("gemini://host.org:1965/", "/");
+ CASE_OK("gemini://host.org:1965/a", "a");
+ CASE_OK("gemini://host.org:1965/a/", "a/");
+ CASE_OK("gemini://host.org:1965/a/b", "a/b");
+ CASE_OK("gemini://host.org:1965/a/b/", "a/b/");
+
CASE_OK("gemini://host/a/b/../c/./d", "a/c/d");
// RFC 3986, 3.1. Scheme
// > should accept uppercase letters as equivalent to lowercase in scheme
// > names (e.g., allow "HTTP" as well as "http")
CASE_OK("GeMiNi://host", "");
+
+ const auto vgi_prefix = cut_null("vgi");
+#define CASE_PREFIX_MATCHED(url_literal) \
+ fill_request_buffer(request, url_literal); \
+ IS_TRUE(request.parse(vgi_prefix, path) == Request::URL_PATH_PREFIX_MATCHED); \
+ IS_TRUE(path == (std::string{url_literal} + "\r\n"))
+
+ CASE_PREFIX_MATCHED("gemini://host/vgi");
+ CASE_PREFIX_MATCHED("gemini://host/vgi/");
+ CASE_PREFIX_MATCHED("gemini://host/vgi2");
+ CASE_PREFIX_MATCHED("gemini://host/vgi?");
+
+ CASE_PREFIX_MATCHED("gemini://host:1965/vgi");
+ CASE_PREFIX_MATCHED("gemini://host:1965/vgi/");
+ CASE_PREFIX_MATCHED("gemini://host:1965/vgi2");
+ CASE_PREFIX_MATCHED("gemini://host:1965/vgi?");
+
+ CASE_PREFIX_MATCHED("gemini://host.org:1965/vgi");
+ CASE_PREFIX_MATCHED("gemini://host.org:1965/vgi/");
+ CASE_PREFIX_MATCHED("gemini://host.org:1965/vgi2");
+ CASE_PREFIX_MATCHED("gemini://host.org:1965/vgi?");
+
+#define CASE_PREFIX_NOT_MATCHED(url_literal) \
+ fill_request_buffer(request, url_literal); \
+ IS_TRUE(request.parse(vgi_prefix, path) == Request::URL_OK); \
+
+ CASE_PREFIX_NOT_MATCHED("gemini://host/Xgi");
+ CASE_PREFIX_NOT_MATCHED("gemini://host/vgX");
+ CASE_PREFIX_NOT_MATCHED("gemini://host/_vgi");
+ CASE_PREFIX_NOT_MATCHED("gemini://host/VGi");
+
+ // debatable, but so far
+ CASE_PREFIX_NOT_MATCHED("gemini://host/./vgi");
+
+ // debatable, but so far
+ CASE_PREFIX_NOT_MATCHED("gemini://host/abc/../vgi");
+
TEST_END()
} // namespace vostok
blob - 36642223663b3697adb5642f2be7ef895c432922
blob + ea04154c4340d36d28532d3551a993ad32703778
--- vostok/args.cc
+++ vostok/args.cc
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;
error::g_log << "\t-m FILE : Path to file mime.types" << std::endl;
- error::g_log << "\t-g PATH : VGI (CGI-like) path part. Must NOT contain a separator (slash: /)" << std::endl;
+ error::g_log << "\t-g PATH : VGI (CGI-like) path prefix (case sensitive, without normalization)" << std::endl;
+ error::g_log << "\t-e PATH : VGI (CGI-like) execution command" << std::endl;
return false;
}
{
int ch;
char *p = nullptr;
- while ((ch = getopt(argc, argv, "a:p:c:k:f:m:g:")) != -1) {
+ while ((ch = getopt(argc, argv, "a:p:c:k:f:m:g:e:")) != -1) {
switch (ch) {
case 'a':
args.addr = optarg;
return false;
break;
case 'g':
- args.vgi = optarg;
+ args.vgi_prefix = Span<const char>{optarg, strlen(optarg)};
break;
+ case 'e':
+ args.vgi_command = optarg;
+ break;
default:
return usage(argv[0]);
error::g_log << "Invalid command line: -d option required" << std::endl;
return usage(argv[0]);
}
+ if ( (args.vgi_prefix.size() && !args.vgi_command) ||
+ (!args.vgi_prefix.size() && args.vgi_command) )
+ {
+ error::g_log << "Invalid command line: options -g and -e can only be specified together" << std::endl;
+ return usage(argv[0]);
+ }
return true;
}
blob - 83e0e5c50e2ac5ce8db054983ad855cb281b9f34
blob + 2bf92b3e7031734df1739a5d94504757f3b5c877
--- vostok/args.h
+++ vostok/args.h
czstring key_file{nullptr};
UniqueFd directory;
Mime mime;
- czstring vgi{nullptr};
+ Span<const char> vgi_prefix;
+ czstring vgi_command{nullptr};
};
blob - 7f087caa76e95ee647bee77a1c0d230aad3f151a
blob + d25fb31f943c5036e49c94ca947d23cfc1c9b550
--- vostok/gemini.cc
+++ vostok/gemini.cc
const Status STATUS_20_SUCCESS{'2', '0'};
const Status STATUS_31_REDIRECT_PERMANENT{'3', '1'};
const Status STATUS_40_TEMPORARY_FAILURE{'4', '0'};
+const Status STATUS_42_CGI_ERROR{'4', '2'};
const Status STATUS_50_PERMANENT_FAILURE{'5', '0'};
const Status STATUS_51_NOT_FOUND{'5', '1'};
const Status STATUS_53_PROXY_REQUEST_REFUSED{'5', '3'};
blob - cc773ff8a5e870b10d30c9be82bc14f75c33ec24
blob + 4ed75c6b759a9db6f15638f3911a3a6942c3c206
--- vostok/gemini.h
+++ vostok/gemini.h
extern const Status STATUS_20_SUCCESS;
extern const Status STATUS_31_REDIRECT_PERMANENT;
extern const Status STATUS_40_TEMPORARY_FAILURE;
+extern const Status STATUS_42_CGI_ERROR;
extern const Status STATUS_50_PERMANENT_FAILURE;
extern const Status STATUS_51_NOT_FOUND;
extern const Status STATUS_53_PROXY_REQUEST_REFUSED;
blob - 12ac2a5cf55579b9ed761481c1a951dc0560f983
blob + b8f42bafd86a8ee7dfe9eda918ee320864350783
--- vostok/request.cc
+++ vostok/request.cc
{
const auto gemini_scheme = cut_null("gemini://");
-inline bool is_gemini_scheme(const std::string &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::string &buffer)
+Span<const char> cut_crlf(const std::string &buffer)
{
// > servers MUST ignore anything sent after the first occurrence of a <CR><LF>.
for (auto current = buffer.cbegin(); current != buffer.cend(); ++current)
break;
if (*current == gemini::CRLF[0] && *next == gemini::CRLF[1])
- {
- buffer.resize(current - buffer.cbegin());
- return true;
- }
+ return Span<const char>{buffer.data(), static_cast<std::size_t>(current - buffer.cbegin())};
}
- return false;
+ return Span<const char>{};
}
}
-Request::ParseResult Request::parse(/* out */ std::string &path)
+Request::ParseResult
+Request::parse(
+ /* in */ const Span<const char> &stop_if_prefix,
+ /* out */ std::string &path
+)
{
- 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)
+ std::string::size_type path_from;
+ std::string::size_type size_without_crlf;
{
- if (*current == '/')
+ const auto buffer = cut_crlf(m_buffer);
+ if (!buffer.size())
+ return BAD_REQUEST;
+
+ // check and skip scheme
+ if (buffer.size() < gemini_scheme.size())
+ return URL_TOO_SHORT;
+ const bool is_gemini_scheme =
+ std::equal(
+ gemini_scheme.begin(),
+ gemini_scheme.end(),
+ buffer.begin(),
+ [](char v1, char v2){return std::tolower(v1) == std::tolower(v2);}
+ );
+ if (!is_gemini_scheme)
+ return URL_NON_GEMINI;
+
+ auto current = buffer.begin() + gemini_scheme.size();
+ // skip domain[:port]
+ for (; current != buffer.end(); ++current)
{
- ++current;
- break;
+ if (*current == '/')
+ {
+ ++current;
+ break;
+ }
}
- }
+ if (stop_if_prefix.size() && current != buffer.end())
+ {
+ if (stop_if_prefix.size() <= static_cast<std::size_t>(buffer.end() - current))
+ {
+ if (std::equal(stop_if_prefix.begin(), stop_if_prefix.end(), current))
+ {
+ m_buffer.swap(path);
+ return URL_PATH_PREFIX_MATCHED;
+ }
+ }
+ }
+
+ path_from = current - buffer.begin();
+ size_without_crlf = buffer.size();
+ }
+ m_buffer.resize(size_without_crlf);
const bool is_slash_on_end = (*m_buffer.crbegin() == '/');
// normalize '.' and '..'
- const auto ret = PathNormalization{}(current, m_buffer);
+ const auto ret = PathNormalization{}(m_buffer.begin() + path_from, m_buffer);
if (ret == URL_OK)
{
m_buffer.swap(path);
blob - 1359056f3684043421faf449d2d868ff02af7caa
blob + 30078df115d1488dfe1a1aec8f7c4023b475e4e7
--- vostok/request.h
+++ vostok/request.h
/** Gemini request parser */
+#include "utils.h"
#include <string>
#pragma once
/* Get buffer for incoming Gemini request */
std::string &get_buffer();
- /* Parse incoming Gemini request
- and return normalized path as zero-terminated string (if URL_OK) */
+ /* Parse incoming Gemini request and return:
+ if URL_OK: normalized path as zero-terminated string
+ if URL_PATH_PREFIX_MATCHED: "raw" Gemini request string
+ */
enum ParseResult
{
URL_OK,
+ URL_PATH_PREFIX_MATCHED,
BAD_REQUEST,
URL_TOO_SHORT,
URL_NON_GEMINI,
URL_ROOT_TRAVERSE,
};
- ParseResult parse(/* out */ std::string &path);
+ ParseResult
+ parse(
+ /* in */ const Span<const char> &stop_if_prefix,
+ /* out */ std::string &path
+ );
private:
std::string m_buffer;
blob - 3ffe1270ee87afe1bc69f3991207856e1f05eaf4
blob + 37162dd8e110c679705509a62f7d62fc46447ccf
--- vostok/vostok.cc
+++ vostok/vostok.cc
#include "gemini.h"
#include <signal.h>
+#include <sys/wait.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
} // namespace meta
const std::string g_index_gmi{"index.gmi"};
+const auto ERROR42_ANSWER = cut_null("42 Temporary failure\r\n");
+struct ProcessRequestContext
+{
+ int directory_fd;
+ const Mime &mime;
+ const Span<const char> &vgi_prefix;
+ czstring vgi_command;
+
+ ProcessRequestContext(
+ int directory_fd,
+ const Mime &mime,
+ const Span<const char> &vgi_prefix,
+ czstring vgi_command
+ ) : directory_fd(directory_fd)
+ , mime(mime)
+ , vgi_prefix(vgi_prefix)
+ , vgi_command(vgi_command)
+ {
+ }
+};
+
+
bool send_response(NotNull<struct tls *> ctx, gemini::Status status, const std::string &meta)
{
// <STATUS><SPACE><META><CR><LF>
}
-void client_thread(const transport::AcceptedClient *accepted_client, int directory_fd, const Mime &mime)
+void
+process_gateway_request(
+ const transport::AcceptedClient &accepted_client,
+ const std::string &path,
+ const ProcessRequestContext &context
+)
{
+ int stdin_pair[2];
+ if (pipe(stdin_pair) != 0)
+ {
+ error::occurred("Create child stdin", error::Print{});
+ send_response(accepted_client.get_ctx(), gemini::STATUS_42_CGI_ERROR, meta::TEMPORARY_FAILURE);
+ return;
+ }
+ UniqueFd stdin_read{stdin_pair[0]};
+ UniqueFd stdin_write{stdin_pair[1]};
+
+
+ int stdout_pair[2];
+ if (pipe(stdout_pair) != 0)
+ {
+ error::occurred("Create child stdout", error::Print{});
+ send_response(accepted_client.get_ctx(), gemini::STATUS_42_CGI_ERROR, meta::TEMPORARY_FAILURE);
+ return;
+ }
+ UniqueFd stdout_read{stdout_pair[0]};
+ UniqueFd stdout_write{stdout_pair[1]};
+
+ const auto child_pid = fork();
+ if (child_pid == -1)
+ {
+ error::occurred("Fork VGI", error::Print{});
+ send_response(accepted_client.get_ctx(), gemini::STATUS_42_CGI_ERROR, meta::TEMPORARY_FAILURE);
+ return;
+ }
+
+ if (child_pid == 0)
+ {
+ // child process (VGI)
+ if (dup2(stdin_read.get(), STDIN_FILENO) == -1)
+ {
+ write(stdout_write.get(), ERROR42_ANSWER.data(), ERROR42_ANSWER.size());
+ return;
+ }
+ stdin_read.reset();
+ stdin_write.reset();
+
+ if (dup2(stdout_write.get(), STDOUT_FILENO) == -1)
+ {
+ write(stdout_write.get(), ERROR42_ANSWER.data(), ERROR42_ANSWER.size());
+ return;
+ }
+ stdout_read.reset();
+ stdout_write.reset();
+
+ execl(context.vgi_command, context.vgi_command, nullptr);
+
+ // if `execl` return, an error has occurred
+ write(STDOUT_FILENO, ERROR42_ANSWER.data(), ERROR42_ANSWER.size());
+ exit(1);
+ }
+ // parent process
+
+ if (write(stdin_write.get(), path.data(), path.size()) == -1)
+ {
+ error::occurred("Write to stdin of the child process", error::Print{});
+ send_response(accepted_client.get_ctx(), gemini::STATUS_42_CGI_ERROR, meta::TEMPORARY_FAILURE);
+ return;
+ }
+ stdin_read.reset();
+ stdin_write.reset();
+
+ stdout_write.reset(); // make sure entry is closed to get EOF
+ bool was_write = false;
+ std::vector<char> buffer;
+ buffer.resize(64 * 1024);
+ for (; ; )
+ {
+ const auto ret = read(stdout_read.get(), buffer.data(), buffer.size());
+ if (ret == -1)
+ {
+ error::occurred("Read from stdout of the child process", error::Print{});
+ if (!was_write)
+ send_response(accepted_client.get_ctx(), gemini::STATUS_42_CGI_ERROR, meta::TEMPORARY_FAILURE);
+ return;
+ }
+ const auto readed = static_cast<size_t>(ret);
+ if (readed == 0)
+ break; // EOF
+ if (!transport::send(accepted_client.get_ctx(), Span<const char>{buffer.data(), readed}))
+ return;
+ was_write = true;
+ }
+
+ int child_status{0};
+ for (; ; )
+ {
+ waitpid(child_pid, &child_status, 0);
+ if (WIFEXITED(child_status))
+ break;
+ }
+ error::g_log << "VGI command return " << std::dec << WEXITSTATUS(child_status) << std::endl;
+}
+
+
+void
+client_thread(
+ const transport::AcceptedClient *accepted_client,
+ const ProcessRequestContext &context
+)
+{
assert(accepted_client);
std::unique_ptr<const transport::AcceptedClient> accepted_client_deleter{accepted_client};
return;
std::string path;
- const auto parse_result = request.parse(path);
+ const auto parse_result = request.parse(context.vgi_prefix, path);
switch (parse_result)
{
case Request::BAD_REQUEST:
send_response(accepted_client->get_ctx(), gemini::STATUS_50_PERMANENT_FAILURE, meta::ROOT_TRAVERSE);
return;
+ case Request::URL_PATH_PREFIX_MATCHED:
+ process_gateway_request(*accepted_client, path, context);
+ return;
+
case Request::URL_OK:
break;
}
UniqueFd opened_dir;
UniqueFd opened_fd;
- auto open_file_result = open_file::open(directory_fd, path, opened_fd);
+ auto open_file_result = open_file::open(context.directory_fd, path, opened_fd);
const std::string *opened_path{nullptr};
switch (open_file_result)
{
}
// <META> from mime.types or "text/gemini" (default).
- const auto _mime_type = mime.lookup(*opened_path);
+ const auto _mime_type = context.mime.lookup(*opened_path);
const std::string &meta_string = _mime_type ? *_mime_type : meta::TEXT_GEMINI;
send_response(accepted_client->get_ctx(), gemini::STATUS_20_SUCCESS, meta_string);
}
-bool server_loop(int server_socket, NotNull<struct tls *>server_ctx, int directory_fd, const Mime &mime)
+bool
+server_loop(
+ int server_socket,
+ NotNull<struct tls *>server_ctx,
+ const ProcessRequestContext &context
+)
{
error::g_log << "🚀 Vostok server listening..." << std::endl;
for (; ; )
{
try
{
- std::thread{client_thread, accepted_client.get(), directory_fd, mime}.detach();
+ std::thread{client_thread, accepted_client.get(), context}.detach();
}
catch (const std::system_error &e)
{
return false;
}
- return server_loop(server_socket.get(), server_ctx.get(), args.directory.get(), args.mime);
+ ProcessRequestContext context{
+ args.directory.get(),
+ args.mime,
+ args.vgi_prefix,
+ args.vgi_command
+ };
+ return server_loop(server_socket.get(), server_ctx.get(), context);
}
blob - /dev/null
blob + 03b52a3c244ffd94f9470210870d3f33bd3887a0 (mode 755)
--- /dev/null
+++ vgi.sh
+#!/bin/sh
+
+# Answer header:
+echo "20 text/gemini\r"
+
+# Answer body:
+echo "VGI demo\r"
+echo "\r"
+echo "URL: \r"
+echo "=> $(cat -)\r"