commit e0af76a7865fe55de80790d4129969c206394b58 from: Aleksey Ryndin date: Tue Sep 05 17:39:39 2023 UTC Add MIME types commit - 19ed715d035acf481eb350478d486262d99314b2 commit + e0af76a7865fe55de80790d4129969c206394b58 blob - 28e9c68f37b47c27910971722aaf6032a9f213d6 blob + 5fc78a21d19735dd5d9eaa422635b667b3f1f2d7 --- .gitignore +++ .gitignore @@ -1,7 +1,6 @@ syntax: glob cert/ vostok/vostok -tests/test_request -tests/test_open_file +tests/*.test **/*.swp **/*.o blob - 69bd50cfc1aecd54f40ab90568eea2c82144fb52 blob + 6800f1b3e5a99f6cf38f3a3471d80983148f3e73 --- Makefile +++ Makefile @@ -1,4 +1,4 @@ -.PHONY: server clean run_server tests install +.PHONY: server clean run tests install server: ${MAKE} -C vostok @@ -7,7 +7,7 @@ clean: ${MAKE} -C vostok clean ${MAKE} -C tests clean -run_server: server +run: server ./vostok/vostok -c cert/server.crt -k cert/server.key -f ./ tests: blob - 0f3019466b5fcf8c9cc286c34515fa9ff67f0576 blob + 8fdc131294e9c9385f9a5f0e5a3691200b834325 --- tests/Makefile +++ tests/Makefile @@ -1,16 +1,20 @@ .PHONY: tests clean -CXXFLAGS = -D_GLIBCXX_DEBUG -D_LIBCPP_DEBUG -DDEBUG -D_DEBUG -I../vostok +CXXFLAGS = -DDEBUG -D_DEBUG -D_LIBCPP_DEBUG=1 -D_GLIBCXX_DEBUG -I../vostok -tests: test_request test_open_file - ./test_request - ./test_open_file +tests: request.test open_file.test mime.test + ./request.test + ./open_file.test + ./mime.test -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 +request.test: test_request.cc ../vostok/request.cc ../vostok/request.h ../vostok/gemini.cc + ${CXX} ${CXXFLAGS} test_request.cc ../vostok/request.cc ../vostok/gemini.cc -o request.test -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 +open_file.test: test_open_file.cc ../vostok/open_file.cc ../vostok/open_file.h + ${CXX} ${CXXFLAGS} test_open_file.cc ../vostok/open_file.cc -o open_file.test +mime.test: test_mime.cc ../vostok/mime.cc ../vostok/mime.h + ${CXX} ${CXXFLAGS} test_mime.cc ../vostok/mime.cc -o mime.test + clean: - rm -f test_request test_open_file + rm -f *.test blob - 490a7207b488c0f90940fda93e95a002abeb7a66 blob + 9417da2e6457b779766d062cf311ab409ea719fd --- tests/test_open_file.cc +++ tests/test_open_file.cc @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -17,27 +16,6 @@ namespace error { std::ostream dev_null{nullptr}; std::ostream &g_log = dev_null; -} -namespace -{ -template -void fill(std::vector &v, const char (&arr)[N]) -{ - v.resize(N); - std::copy(&arr[0], &arr[N], v.begin()); -} - -template -bool is_tail(Span opened_path, const char (&arr)[N]) -{ - if (opened_path.size() >= (N - 1)) - { - opened_path = opened_path.subspan(opened_path.size() - (N - 1)); - return std::equal(opened_path.begin(), opened_path.end(), &arr[0]); - } - return false; -} - } // namespace TEST_START(test_open_file) @@ -49,19 +27,20 @@ TEST_START(test_open_file) IS_TRUE_ERRNO(dir, "Open directory " << ss.str().c_str()); - std::vector sz_file_name; - Span opened_path; + std::string file_name; + { UniqueFd opened_file; + const std::string *opened_path = nullptr; - fill(sz_file_name, ""); - IS_TRUE(open_file(dir.get(), sz_file_name , opened_file, opened_path) == FILE_NOT_FOUND and !opened_file); + file_name = ""; + IS_TRUE(open_file(dir.get(), file_name , opened_file, opened_path) == FILE_NOT_FOUND and !opened_file); - fill(sz_file_name, "non-existent-file"); - IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file); + file_name = "non-existent-file"; + IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file); - fill(sz_file_name, "non-existent-dir/file"); - IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file); + file_name = "non-existent-dir/file"; + IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file); } const UniqueFd index_gmi{openat(dir.get(), "index.gmi", O_RDONLY | O_CREAT | O_EXCL, S_IRWXU)}; @@ -71,10 +50,12 @@ TEST_START(test_open_file) { UniqueFd opened_file; - fill(sz_file_name, ""); - IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_OPENED and opened_file); - IS_TRUE(is_tail(opened_path, "index.gmi")); + const std::string *opened_path = nullptr; + file_name = ""; + IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_OPENED and opened_file); + IS_TRUE(*opened_path == "index.gmi"); + struct stat sb2{}; IS_TRUE_ERRNO(fstat(opened_file.get(), &sb2) != -1, "Stat file " << ss.str().c_str() << "/index.gmi"); IS_TRUE((sb1.st_dev == sb2.st_dev) && (sb1.st_ino == sb2.st_ino)); @@ -82,9 +63,10 @@ TEST_START(test_open_file) { UniqueFd opened_file; - fill(sz_file_name, "index.gmi"); - IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_OPENED and opened_file); - IS_TRUE(is_tail(opened_path, "index.gmi")); + const std::string *opened_path = nullptr; + file_name = "index.gmi"; + IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_OPENED and opened_file); + IS_TRUE(*opened_path == "index.gmi"); struct stat sb2{}; IS_TRUE_ERRNO(fstat(opened_file.get(), &sb2) != -1, "Stat file " << ss.str().c_str() << "/index.gmi"); IS_TRUE((sb1.st_dev == sb2.st_dev) && (sb1.st_ino == sb2.st_ino)); @@ -93,10 +75,13 @@ TEST_START(test_open_file) IS_TRUE_ERRNO(mkdirat(dir.get(), "subdir", S_IRWXU) != -1, "Create directory " << ss.str().c_str() << "/subdir"); { UniqueFd opened_file; - fill(sz_file_name, "subdir"); - IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file); - fill(sz_file_name, "subdir/"); - IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file); + const std::string *opened_path = nullptr; + + file_name = "subdir"; + IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file); + + file_name = "subdir/"; + IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_NOT_FOUND and !opened_file); } const UniqueFd subdir{openat(dir.get(), "subdir", O_RDONLY | O_DIRECTORY)}; IS_TRUE_ERRNO(subdir, "Open directory " << ss.str().c_str() << "/subdir"); @@ -107,9 +92,11 @@ TEST_START(test_open_file) { UniqueFd opened_file; - fill(sz_file_name, "subdir"); - IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_OPENED and opened_file); - IS_TRUE(is_tail(opened_path, "index.gmi")); + const std::string *opened_path = nullptr; + + file_name = "subdir"; + IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_OPENED and opened_file); + IS_TRUE(*opened_path == "index.gmi"); struct stat sb2{}; IS_TRUE_ERRNO(fstat(opened_file.get(), &sb2) != -1, "Stat file " << ss.str().c_str() << "/subdir/index.gmi"); IS_TRUE((sb1.st_dev == sb2.st_dev) && (sb1.st_ino == sb2.st_ino)); @@ -117,9 +104,11 @@ TEST_START(test_open_file) { UniqueFd opened_file; - fill(sz_file_name, "subdir/"); - IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_OPENED and opened_file); - IS_TRUE(is_tail(opened_path, "index.gmi")); + const std::string *opened_path = nullptr; + + file_name = "subdir/"; + IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_OPENED and opened_file); + IS_TRUE(*opened_path == "index.gmi"); struct stat sb2{}; IS_TRUE_ERRNO(fstat(opened_file.get(), &sb2) != -1, "Stat file " << ss.str().c_str() << "/subdir/index.gmi"); IS_TRUE((sb1.st_dev == sb2.st_dev) && (sb1.st_ino == sb2.st_ino)); @@ -127,9 +116,11 @@ TEST_START(test_open_file) { UniqueFd opened_file; - fill(sz_file_name, "subdir/index.gmi"); - IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_OPENED and opened_file); - IS_TRUE(is_tail(opened_path, "index.gmi")); + const std::string *opened_path = nullptr; + + file_name = "subdir/index.gmi"; + IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_OPENED and opened_file); + IS_TRUE(*opened_path == "subdir/index.gmi"); struct stat sb2{}; IS_TRUE_ERRNO(fstat(opened_file.get(), &sb2) != -1, "Stat file " << ss.str().c_str() << "/subdir/index.gmi"); IS_TRUE((sb1.st_dev == sb2.st_dev) && (sb1.st_ino == sb2.st_ino)); @@ -140,8 +131,10 @@ TEST_START(test_open_file) { UniqueFd opened_file; - fill(sz_file_name, "subdir/EPERM.gmi"); - IS_TRUE(open_file(dir.get(), sz_file_name, opened_file, opened_path) == FILE_OPENING_ERROR and !opened_file); + const std::string *opened_path = nullptr; + + file_name = "subdir/EPERM.gmi"; + IS_TRUE(open_file(dir.get(), file_name, opened_file, opened_path) == FILE_OPENING_ERROR and !opened_file); } IS_TRUE_ERRNO(unlinkat(subdir.get(), "EPERM.gmi", 0) != -1, "Remove file " << ss.str().c_str() << "/subdir/EPERM.gmi"); blob - /dev/null blob + 77c955e6b386a8abd0811a768c9b61eaaf01d43e (mode 644) --- /dev/null +++ tests/test_mime.cc @@ -0,0 +1,136 @@ +#include "mime.h" +#include + +#include "tests.h" + + +namespace vostok +{ + + +TEST_START(test_mime_get_ext) + std::string path{""}; + auto it = Mime::get_ext(path); + IS_TRUE(it == path.end()); + + path = "a"; + it = Mime::get_ext(path); + IS_TRUE(it == path.end()); + + path = "ab"; + it = Mime::get_ext(path); + IS_TRUE(it == path.end()); + + path = "."; + it = Mime::get_ext(path); + IS_TRUE(it == path.end()); + + path = "/"; + it = Mime::get_ext(path); + IS_TRUE(it == path.end()); + + path = "/."; + it = Mime::get_ext(path); + IS_TRUE(it == path.end()); + + path = "/a"; + it = Mime::get_ext(path); + IS_TRUE(it == path.end()); + + path = "/ab"; + it = Mime::get_ext(path); + IS_TRUE(it == path.end()); + + path = ".ext"; + it = Mime::get_ext(path); + IS_TRUE(it != path.end() && std::string(&*it) == "ext"); + + path = "a.ext"; + it = Mime::get_ext(path); + IS_TRUE(it != path.end() && std::string(&*it) == "ext"); + + path = "ab.ext"; + it = Mime::get_ext(path); + IS_TRUE(it != path.end() && std::string(&*it) == "ext"); + + path = ".ext1.ext2"; + it = Mime::get_ext(path); + IS_TRUE(it != path.end() && std::string(&*it) == "ext2"); + + path = "a.ext1.ext2"; + it = Mime::get_ext(path); + IS_TRUE(it != path.end() && std::string(&*it) == "ext2"); + + path = "ab.ext1.ext2"; + it = Mime::get_ext(path); + IS_TRUE(it != path.end() && std::string(&*it) == "ext2"); + + path = "xyz/.ext"; + it = Mime::get_ext(path); + IS_TRUE(it != path.end() && std::string(&*it) == "ext"); + + path = "xyz/a.ext"; + it = Mime::get_ext(path); + IS_TRUE(it != path.end() && std::string(&*it) == "ext"); + + path = "xyz/ab.ext"; + it = Mime::get_ext(path); + IS_TRUE(it != path.end() && std::string(&*it) == "ext"); + + path = "xyz/.ext1.ext2"; + it = Mime::get_ext(path); + IS_TRUE(it != path.end() && std::string(&*it) == "ext2"); + + path = "xyz/a.ext1.ext2"; + it = Mime::get_ext(path); + IS_TRUE(it != path.end() && std::string(&*it) == "ext2"); + + path = "xyz/ab.ext1.ext2"; + it = Mime::get_ext(path); + IS_TRUE(it != path.end() && std::string(&*it) == "ext2"); +TEST_END() + + +TEST_START(test_mime_db) + Mime mime; + + IS_TRUE(mime.parse_db("test_mime_types")); + + auto ret = mime.lookup(".unk"); + IS_TRUE(ret == nullptr); + + ret = mime.lookup(".rss"); + IS_TRUE(ret == nullptr); + + ret = mime.lookup(".pd"); + IS_TRUE(ret == nullptr); + ret = mime.lookup(".df"); + IS_TRUE(ret == nullptr); + ret = mime.lookup(".pf"); + IS_TRUE(ret == nullptr); + + ret = mime.lookup(".pdf"); + IS_TRUE(ret != nullptr && *ret == "application/pdf"); + + ret = mime.lookup(".ps"); + IS_TRUE(ret != nullptr && *ret == "application/postscript"); + ret = mime.lookup(".eps"); + IS_TRUE(ret != nullptr && *ret == "application/postscript"); + ret = mime.lookup(".ai"); + IS_TRUE(ret != nullptr && *ret == "application/postscript"); + + ret = mime.lookup(".rtf"); + IS_TRUE(ret != nullptr && *ret == "application/rtf"); +TEST_END() + + +} // namespace vostok + + +extern "C" +int +main(int, char **) +{ + const auto ret = vostok::test_mime_get_ext(); + return ret ? ret : vostok::test_mime_db(); +} blob - 7a61653e524b22ec02a4d5d321f6ade1014aeb3e blob + 8b266eeb9ade6abe27c9aba7f8dd16369f028e4f --- tests/test_request.cc +++ tests/test_request.cc @@ -1,6 +1,5 @@ #include "request.h" -#include -#include +#include #include "tests.h" @@ -10,31 +9,39 @@ namespace vostok namespace { template -void fill_request_buffer(std::vector &v, const char (&arr)[N]) +std::string &fill_request_buffer(Request &r, const char (&arr)[N]) { static_assert(N > 0, "N"); - v.resize(N + 1); - auto p = std::copy(&arr[0], &arr[N - 1], v.begin()); + auto &s = r.get_buffer(); + assert(s.size() >= (N + 1)); + auto p = std::copy(&arr[0], &arr[N - 1], s.begin()); *p = '\r'; ++p; *p = '\n'; ++p; + s.resize(N + 1); + return s; } } // namespace TEST_START(test_request) Request request; + std::string path; - auto &buffer = request.get_buffer(); - IS_TRUE(buffer.size() == 1026); + { + 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); + { + auto &buffer = fill_request_buffer(request, "gemini://host"); + buffer[buffer.size() - 1] = '\0'; + IS_TRUE(request.parse(path) == Request::BAD_REQUEST); + } #define CASE_ERROR(url_literal, expected_result) \ - fill_request_buffer(request.get_buffer(), url_literal); \ - IS_TRUE(request.parse() == expected_result) + fill_request_buffer(request, url_literal); \ + IS_TRUE(request.parse(path) == expected_result) CASE_ERROR("", Request::URL_TOO_SHORT); CASE_ERROR("g", Request::URL_TOO_SHORT); @@ -47,8 +54,9 @@ TEST_START(test_request) 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)) + fill_request_buffer(request, url_literal); \ + IS_TRUE(request.parse(path) == Request::URL_OK); \ + IS_TRUE(path == path_literal) CASE_OK("gemini://host", ""); CASE_OK("gemini://host/", ""); blob - /dev/null blob + 2437b1e2043e837bb56255a59b6de6e9011d3e06 (mode 644) --- /dev/null +++ tests/test_mime_types @@ -0,0 +1,8 @@ +#application/rss+xml rss + +application/pdf pdf + + +application/postscript ps eps ai + +application/rtf rtf blob - f63e7b699fe5770773ae17075b21503eec03a33c blob + e90e0c9567f320b98dda8251cc83251d8be4963a --- tests/tests.h +++ tests/tests.h @@ -1,3 +1,5 @@ +#include + #pragma once #define TEST_START(name) int name() { blob - 65f8d32addaca2f9201a5b5e06104c92c6b90769 blob + 9033a5cc11774e198b77ab33da9e8b3c988130ab --- vostok/Makefile +++ vostok/Makefile @@ -1,16 +1,11 @@ CXXFLAGS += -Wall -Wextra -std=c++11 LIBS = -ltls -CXXFILES = transport.cc -CXXFILES += error.cc -CXXFILES += gemini.cc -CXXFILES += args.cc -CXXFILES += request.cc -CXXFILES += open_file.cc -CXXFILES += vostok.cc +CXXFILES = transport.cc error.cc gemini.cc args.cc request.cc open_file.cc mime.cc vostok.cc +HXXFILES = args.h error.h gemini.h mime.h open_file.h request.h transport.h utils.h OFILES = ${CXXFILES:.cc=.o} -.cc.o: +.cc.o: ${HXXFILES} ${CXX} ${CXXFLAGS} -c -o $@ $< vostok: ${OFILES} blob - 19144e4a70be55173bbd017c1697f77ebf2669bc blob + 1f1e2b7aecd1bb638f36b78758a34a75065213d9 --- vostok/args.cc +++ vostok/args.cc @@ -28,6 +28,7 @@ bool usage(const char *program) 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; + error::g_log << "\t-m FILE : Path to file mime.types" << std::endl; return false; } @@ -44,7 +45,7 @@ parse_command_line_arguments( { int ch; char *p = nullptr; - while ((ch = getopt(argc, argv, "a:p:c:k:f:")) != -1) { + while ((ch = getopt(argc, argv, "a:p:c:k:f:m:")) != -1) { switch (ch) { case 'a': args.addr = optarg; @@ -78,6 +79,10 @@ parse_command_line_arguments( return false; } break; + case 'm': + if (!args.mime.parse_db(optarg)) + return false; + break; default: return usage(argv[0]); blob - 0d5d970247a1b2947a8fb3f7fb034b373f784656 blob + a9a2b6a44858304b3e76f8adca2f76bcc73105fb --- vostok/args.h +++ vostok/args.h @@ -1,6 +1,7 @@ /** Parse command line arguments */ #include "utils.h" +#include "mime.h" #pragma once @@ -16,6 +17,7 @@ struct CommandLineArguments czstring cert_file{nullptr}; czstring key_file{nullptr}; UniqueFd directory; + Mime mime; }; blob - bf7fc548d06c4122ff18d59004d8010ca6c8636c blob + 7e53ac30ac83be704bad5b3eb71ab4cf0ab4c44e --- vostok/open_file.cc +++ vostok/open_file.cc @@ -9,39 +9,26 @@ namespace vostok { -namespace -{ -const char index_gmi[] = "index.gmi"; -} // namespace +static const std::string index_gmi{"index.gmi"}; OpenFileResult open_file( /* in */ int directory_fd, - /* in */ const std::vector &sz_file_name, + /* in */ const std::string &request_path, /* out */ UniqueFd &opened_file, - /* out */ Span &opened_path + /* out */ const std::string* &opened_path ) { - assert(sz_file_name.size()); - - NotNull file_name = sz_file_name.data(); - opened_path = cut_null(sz_file_name); - - if (!*file_name) - { - file_name = index_gmi; - opened_path = cut_null(index_gmi); - } - - opened_file.reset(openat(directory_fd, file_name, O_RDONLY)); + opened_path = request_path.empty() ? &index_gmi : &request_path; + opened_file.reset(openat(directory_fd, opened_path->c_str(), O_RDONLY)); if (!opened_file) { const auto error_code = errno; error::occurred( - [file_name] + [&opened_path] { - error::g_log << "Open file \"" << file_name << "\""; + error::g_log << "Open file \"" << opened_path->c_str() << "\""; }, error::Print{error_code} ); @@ -51,9 +38,9 @@ open_file( if (fstat(opened_file.get(), &sb) == -1) { error::occurred( - [file_name] + [&opened_path] { - error::g_log << "Stat file \"" << file_name << "\""; + error::g_log << "Stat file \"" << opened_path->c_str() << "\""; }, error::Print{} ); @@ -62,15 +49,15 @@ open_file( if (S_ISDIR(sb.st_mode)) { const UniqueFd new_parent{opened_file.release()}; - opened_path = cut_null(index_gmi); - opened_file.reset(openat(new_parent.get(), index_gmi, O_RDONLY)); + opened_path = &index_gmi; + opened_file.reset(openat(new_parent.get(), opened_path->c_str(), O_RDONLY)); if (!opened_file) { const auto error_code = errno; error::occurred( - [file_name] + [&opened_path] { - error::g_log << "Open file \"" << file_name << "\" /" << index_gmi; + error::g_log << "Open file \"" << opened_path->c_str() << "\" /" << index_gmi; }, error::Print{error_code} ); blob - /dev/null blob + 80a02c2b1fc22087cef8d3252cc8cb4c67b82f37 (mode 644) --- /dev/null +++ vostok/mime.cc @@ -0,0 +1,94 @@ +/* Detect MIME file type */ + +#include "mime.h" + +#include +#include + + +namespace vostok +{ + + +bool Mime::parse_db(NotNull sz_path) +{ + std::ifstream ifstream; + ifstream.open(sz_path); + + for (std::string line; std::getline(ifstream, line); ) + { + if (line.empty()) + continue; + if (line[0] == '#') + continue; + + std::istringstream line_stream(line); + std::string type; + for (std::string token; std::getline(line_stream, token, '\t'); ) + { + if (token.empty()) + continue; + if (type.empty()) + { + type = token; + continue; + } + + std::istringstream ext_stream(token); + for (std::string ext; std::getline(ext_stream, ext, ' '); ) + { + if (!ext.empty()) + { + Key key{ext}; + m_map.emplace(std::move(key), type); + } + } + } + } + return true; +} + + +const std::string *Mime::lookup(const std::string &path) const +{ + const auto it = get_ext(path); + if (it == path.end()) + return nullptr; + + auto found = m_map.find(Key{&*it}); + return found == m_map.end() ? nullptr : &(found->second); +} + + +std::string::const_iterator Mime::get_ext(const std::string &path) +{ + if (!path.empty()) + { + auto current = (path.end() - 1); + for (; ; ) + { + if (*current == '/') + break; + + if (*current != '.') + { + if (current == path.begin()) + break; + + --current; + continue; + } + return current + 1; + } + } + return path.end(); +} + + +bool Mime::Less::operator() (const Mime::Key &key1, const Mime::Key &key2) const +{ + return strcasecmp(key1.c_str(), key2.c_str()) < 0; +} + + +} // namespace vostok blob - a69a53ff70dbd260bb80d14e0faa7e9017c6584a blob + 3b9c2eb7ae75aafc8380763b767c4323b8f75ee5 --- vostok/open_file.h +++ vostok/open_file.h @@ -1,6 +1,7 @@ /** Open file for Gemini response */ #include "utils.h" +#include #pragma once @@ -8,7 +9,6 @@ namespace vostok { - enum OpenFileResult { FILE_OPENED, @@ -20,9 +20,9 @@ enum OpenFileResult OpenFileResult open_file( /* in */ int directory_fd, - /* in */ const std::vector &sz_file_name, + /* in */ const std::string &request_path, /* out */ UniqueFd &opened_file, - /* out */ Span &opened_path + /* out */ const std::string* &opened_path ); blob - /dev/null blob + d090a2aee0616e7839bf45500d48bda520a7d7da (mode 644) --- /dev/null +++ vostok/mime.h @@ -0,0 +1,50 @@ +/* Detect MIME file type */ + +#include "utils.h" + +#include +#include + +#pragma once + + +namespace vostok +{ + + +class Mime +{ +public: + /* Read and parse "mime.types" file content */ + bool parse_db(NotNull sz_path); + + /* Lookup MIME type by FS path */ + const std::string *lookup(const std::string &path) const; + + /* Get file extension from path. Return path.end() if no extension. */ + static std::string::const_iterator get_ext(const std::string &path); + +protected: + class Key + { + public: + explicit Key(/* in/out */ std::string &string) {m_string.swap(string);} + explicit Key(const char *p) : m_p(p) {} + const char *c_str() const { return m_p ? m_p : m_string.c_str(); } + + private: + std::string m_string; + const char *m_p{nullptr}; + }; + +private: + class Less + { + public: + bool operator ()(const Key &key1, const Key &key2) const; + }; + std::map m_map; +}; + + +} // namespace vostok blob - 7bc242fbd334c6b9df1ffaa6c3a54ad249ca3f5c blob + e0d4eb17cf214b9012e7bfc4b6191d91444626cd --- vostok/request.cc +++ vostok/request.cc @@ -16,7 +16,7 @@ namespace { const auto gemini_scheme = cut_null("gemini://"); -inline bool is_gemini_scheme(const std::vector &url) +inline bool is_gemini_scheme(const std::string &url) { return std::equal( gemini_scheme.begin(), @@ -27,7 +27,7 @@ inline bool is_gemini_scheme(const std::vector & } -bool cut_crlf(std::vector &buffer) +bool cut_crlf(std::string &buffer) { // > servers MUST ignore anything sent after the first occurrence of a . for (auto current = buffer.cbegin(); current != buffer.cend(); ++current) @@ -53,8 +53,8 @@ class PathNormalization (public) Request::ParseResult operator() ( - std::vector::const_iterator p, - std::vector &url + std::string::const_iterator p, + std::string &url ); protected: @@ -65,7 +65,7 @@ class PathNormalization (public) return Request::URL_OK; } - void fill(std::vector &url) const; + void fill(std::string &url) const; private: Components::value_type m_inprogress; @@ -75,8 +75,8 @@ class PathNormalization (public) Request::ParseResult PathNormalization::operator() ( - std::vector::const_iterator p, - std::vector &url + std::string::const_iterator p, + std::string &url ) { m_inprogress = Components::value_type{nullptr, 0}; @@ -125,7 +125,7 @@ Request::ParseResult PathNormalization::on_component() return on_component_ok(); } -void PathNormalization::fill(std::vector &url) const +void PathNormalization::fill(std::string &url) const { auto current = url.begin(); for (auto it = m_result.cbegin(); it != m_result.cend(); ++it) @@ -138,23 +138,20 @@ void PathNormalization::fill(std::vector &url) c } current = std::copy(it->begin(), it->end(), current); } - *current = '\0'; - ++current; - url.resize(current - url.begin()); } } // namespace -std::vector &Request::get_buffer() +std::string &Request::get_buffer() { m_buffer.resize(gemini::MAX_REQUEST_LENGTH); return m_buffer; } -Request::ParseResult Request::parse() +Request::ParseResult Request::parse(/* out */ std::string &path) { if (!cut_crlf(m_buffer)) return BAD_REQUEST; @@ -177,17 +174,11 @@ Request::ParseResult Request::parse() } // normalize '.' and '..' - return PathNormalization{}(current, m_buffer); + const auto ret = PathNormalization{}(current, m_buffer); + if (ret == URL_OK) + m_buffer.swap(path); + return ret; } -const std::vector &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 - 9beac489676e75d5c1a7deee44be8c08bd0475e9 blob + 1359056f3684043421faf449d2d868ff02af7caa --- vostok/request.h +++ vostok/request.h @@ -1,6 +1,6 @@ /** Gemini request parser */ -#include +#include #pragma once @@ -13,9 +13,10 @@ class Request { public: /* Get buffer for incoming Gemini request */ - std::vector &get_buffer(); + std::string &get_buffer(); - /* Parse incoming Gemini request */ + /* Parse incoming Gemini request + and return normalized path as zero-terminated string (if URL_OK) */ enum ParseResult { URL_OK, @@ -25,14 +26,10 @@ class Request (public) URL_NON_GEMINI, URL_ROOT_TRAVERSE, }; - ParseResult parse(); + ParseResult parse(/* out */ std::string &path); - - /* Get normalized path (if parse() return URL_OK) as zero-terminated string */ - const std::vector &get_path() const; - private: - std::vector m_buffer; + std::string m_buffer; }; blob - ce1c426013b7734b3cb83baba2ea6915037fc386 blob + c7684b15aa5d1d31442d925abd0729a7f80e1e78 --- vostok/transport.cc +++ vostok/transport.cc @@ -136,7 +136,7 @@ AcceptedClient::AcceptedClient(int server_socket, stru bool recv( /* in */ NotNull ctx, - /* in/out */ std::vector &buff + /* in/out */ Span &buff ) { ssize_t ret{}; @@ -152,7 +152,7 @@ bool recv( error::occurred("TLS read", PrintIfError{tls_error(ctx)}); return false; } - buff.resize(ret); + buff = buff.first(ret); return true; } @@ -167,10 +167,10 @@ send( while (buff.size() > 0) { ret = tls_write(ctx, buff.begin(), buff.size()); + error::g_log << "|" << std::dec << ret << " (0x" << std::hex << ret << ")|" << std::endl; if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue; - break; - buff = buff.subspan(buff.size() - ret); + buff = buff.subspan(ret); } if (ret == -1) { blob - 08c3def08a75168b755be2465e2b8845f5f517c3 blob + 5533e0ea87859946aed4bfb8a63870bd800eb07a --- vostok/transport.h +++ vostok/transport.h @@ -54,10 +54,26 @@ class AcceptedClient (private) bool recv( /* in */ NotNull ctx, - /* in/out */ std::vector &buff + /* in/out */ Span &buff ); +template +bool +recv( + /* in */ NotNull ctx, + /* in/out */ Container &buff +) +{ + Span span{buff}; + const auto ret = recv(ctx, span); + if (!ret) + return false; + buff.resize(span.size()); + return true; +} + + /** Send raw bytes */ bool send( blob - 60749f41c58b753468d478666c6e0568f18c687b blob + a112a379cd9c0119277eb500296040c22a07bf0d --- vostok/utils.h +++ vostok/utils.h @@ -2,8 +2,7 @@ #include #include -#include -#include +#include #include @@ -65,34 +64,24 @@ class Span public: using element_type = ElemT; using iterator = ElemT *; + using reverse_iterator = std::reverse_iterator; constexpr Span() : m_p{nullptr}, m_count{0} {} constexpr Span(element_type *p, std::size_t count) : m_p{count ? p : nullptr}, m_count{count} {} template constexpr Span(element_type (&arr)[N]) : m_p{arr}, m_count{N} {} - template - constexpr Span(const std::array &arr) : m_p{arr.data()}, m_count{N} {} - template - Span(const std::vector &v) - { - if (v.size()) - { - m_p = v.data(); - m_count = v.size(); - } - else - { - m_p = nullptr; - m_count = 0; - } - } + template + constexpr Span(Container &c) : m_p{c.size() ? &*c.begin() : nullptr}, m_count{c.size()} {} 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(); } + reverse_iterator rbegin() const noexcept { return reverse_iterator(end()); } + reverse_iterator rend() const noexcept { return reverse_iterator(begin());} + Span first(std::size_t count) const { assert(count <= m_count); @@ -104,6 +93,7 @@ class Span return (offset < m_count) ? Span{m_p + offset, m_count - offset} : Span{}; } + element_type *data() const { return m_p; } element_type &operator[](std::size_t idx) const { assert(idx < m_count); @@ -123,13 +113,6 @@ constexpr Span cut_null(const char (&arr)[ static_assert(N > 0, "!(N > 0)"); return Span{arr, N - 1}; } -template -Span cut_null(const std::vector &v) -{ - assert(v.size() > 0); - assert(v[v.size() - 1] == '\0'); - return Span{v.data(), v.size() - 1}; -} /** Smart pointer for file descriptor */ blob - 35c28e6faddbc3bc9f924773c695981ddadef313 blob + cc2dda64753c42de52c31b28c53ec3385379e310 --- vostok/vostok.cc +++ vostok/vostok.cc @@ -11,6 +11,7 @@ #include #include +#include #include @@ -21,33 +22,24 @@ namespace 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); +const std::string _EMPTY; +const std::string BAD_REQUEST{"Bad request"}; +const std::string URL_TOO_SHORT{"URL is too short"}; +const std::string NON_GEMINI{"No proxying to non-Gemini content"}; +const std::string ROOT_TRAVERSE{"Wrong traverse"}; +const std::string NOT_FOUND{"Not found"}; +const std::string TEMPORARY_FAILURE{"Temporary failure"}; } // namespace meta -bool send_response(NotNull ctx, gemini::Status status, Span meta) + +bool send_response(NotNull ctx, gemini::Status status, const std::string &meta) { // if (!transport::send(ctx, status)) return false; if (!transport::send(ctx, gemini::SPACE)) return false; - if (meta.size()) + if (!meta.empty()) { if (!transport::send(ctx, meta)) return false; @@ -59,7 +51,7 @@ bool send_response(NotNull ctx, gemini:: -void client_thread(const transport::AcceptedClient *accepted_client, int directory_fd) +void client_thread(const transport::AcceptedClient *accepted_client, int directory_fd, const Mime &mime) { assert(accepted_client); std::unique_ptr accepted_client_deleter{accepted_client}; @@ -68,24 +60,25 @@ void client_thread(const transport::AcceptedClient *ac if (!transport::recv(accepted_client->get_ctx(), request.get_buffer())) return; - const auto parse_result = request.parse(); + std::string path; + const auto parse_result = request.parse(path); switch (parse_result) { 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); + error::occurred("Parse request", []{error::g_log << meta::BAD_REQUEST;}); + send_response(accepted_client->get_ctx(), gemini::STATUS_59_BAD_REQUEST, meta::BAD_REQUEST); return; 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); + error::occurred("Parse request", []{error::g_log << meta::URL_TOO_SHORT;}); + send_response(accepted_client->get_ctx(), gemini::STATUS_59_BAD_REQUEST, meta::URL_TOO_SHORT); return; 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); + error::occurred("Parse request", []{error::g_log << meta::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); + error::occurred("Parse request", []{error::g_log << meta::ROOT_TRAVERSE;}); + send_response(accepted_client->get_ctx(), gemini::STATUS_50_PERMANENT_FAILURE, meta::ROOT_TRAVERSE); return; case Request::URL_OK: @@ -93,24 +86,29 @@ void client_thread(const transport::AcceptedClient *ac } UniqueFd opened_file{}; - Span opened_path; - const auto open_file_result = open_file(directory_fd, request.get_path(), opened_file, opened_path); + const std::string *opened_path = nullptr; + const auto open_file_result = open_file(directory_fd, path, opened_file, opened_path); switch (open_file_result) { case FILE_NOT_FOUND: - 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: - 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: - assert(opened_file); + assert(opened_file && opened_path); break; } // > If is an empty string, the MIME type MUST default to "text/gemini; charset=utf-8". - send_response(accepted_client->get_ctx(), gemini::STATUS_20_SUCCESS, {}); + const auto mime_type = mime.lookup(*opened_path); + send_response( + accepted_client->get_ctx(), + gemini::STATUS_20_SUCCESS, + mime_type ? *mime_type : meta::_EMPTY + ); std::vector buffer; buffer.resize(64 * 1024); @@ -120,9 +118,9 @@ void client_thread(const transport::AcceptedClient *ac if (ret == -1) { error::occurred( - [&request] + [&path] { - error::g_log << "Read file \"" << request.get_path().data() << "\""; + error::g_log << "Read file \"" << path.c_str() << "\""; }, error::Print{} ); @@ -137,11 +135,11 @@ void client_thread(const transport::AcceptedClient *ac if (readed < buffer.size()) break; } - error::g_log << "20 " << "\"" << request.get_path().data() << "\"" << std::endl; + error::g_log << "20 " << (mime_type ? *mime_type : meta::_EMPTY) << " \"" << path.c_str() << "\"" << std::endl; } -bool server_loop(int server_socket, NotNullserver_ctx, int directory_fd) +bool server_loop(int server_socket, NotNullserver_ctx, int directory_fd, const Mime &mime) { error::g_log << "🚀 Vostok server listening..." << std::endl; for (; ; ) @@ -159,7 +157,7 @@ bool server_loop(int server_socket, NotNull