From 6232d13f5321b87ddf12a1aa36b4545da45f173d Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Wed, 17 Nov 2021 22:34:57 +0100 Subject: Travel3: Simple image and video display site Reads the images and videos from filesystem and builds a site in memroy. --- src/args.cc | 291 ++++++++++++++ src/args.hh | 53 +++ src/buffer.cc | 236 ++++++++++++ src/buffer.hh | 25 ++ src/common.hh | 16 + src/config.cc | 182 +++++++++ src/config.hh | 48 +++ src/date.cc | 66 ++++ src/date.hh | 70 ++++ src/document.cc | 66 ++++ src/document.hh | 30 ++ src/fcgi_protocol.cc | 594 +++++++++++++++++++++++++++++ src/fcgi_protocol.hh | 191 ++++++++++ src/file_opener.cc | 106 ++++++ src/file_opener.hh | 33 ++ src/files_finder.cc | 231 ++++++++++++ src/files_finder.hh | 51 +++ src/geo_json.cc | 345 +++++++++++++++++ src/geo_json.hh | 28 ++ src/hash_method.cc | 17 + src/hash_method.hh | 23 ++ src/hash_method_openssl.cc | 34 ++ src/hasher.cc | 71 ++++ src/hasher.hh | 29 ++ src/htmlutil.cc | 59 +++ src/htmlutil.hh | 21 ++ src/http_protocol.cc | 916 +++++++++++++++++++++++++++++++++++++++++++++ src/http_protocol.hh | 170 +++++++++ src/image.cc | 344 +++++++++++++++++ src/image.hh | 67 ++++ src/inet.cc | 112 ++++++ src/inet.hh | 29 ++ src/io.cc | 234 ++++++++++++ src/io.hh | 113 ++++++ src/jsutil.cc | 72 ++++ src/jsutil.hh | 22 ++ src/location.hh | 23 ++ src/logger.hh | 57 +++ src/logger_base.cc | 69 ++++ src/logger_base.hh | 25 ++ src/logger_file.cc | 52 +++ src/logger_null.cc | 20 + src/logger_stdio.cc | 41 ++ src/logger_syslog.cc | 45 +++ src/looper.hh | 38 ++ src/looper_poll.cc | 223 +++++++++++ src/mime_types.cc | 29 ++ src/mime_types.hh | 12 + src/observer_list.hh | 148 ++++++++ src/pathutil.cc | 44 +++ src/pathutil.hh | 13 + src/ro_buffer.cc | 29 ++ src/ro_buffer.hh | 23 ++ src/rotation.hh | 16 + src/send_file.cc | 45 +++ src/send_file.hh | 35 ++ src/server.cc | 256 +++++++++++++ src/signal_handler.cc | 75 ++++ src/signal_handler.hh | 29 ++ src/site.cc | 488 ++++++++++++++++++++++++ src/site.hh | 35 ++ src/static_files.cc | 113 ++++++ src/static_files.hh | 33 ++ src/str_buffer.cc | 92 +++++ src/str_buffer.hh | 14 + src/strutil.cc | 211 +++++++++++ src/strutil.hh | 51 +++ src/tag.cc | 139 +++++++ src/tag.hh | 42 +++ src/task_runner.hh | 24 ++ src/task_runner_looper.cc | 75 ++++ src/task_runner_reply.hh | 19 + src/task_runner_thread.cc | 73 ++++ src/timezone.cc | 38 ++ src/timezone.hh | 27 ++ src/transport.cc | 195 ++++++++++ src/transport.hh | 147 ++++++++ src/transport_base.cc | 677 +++++++++++++++++++++++++++++++++ src/transport_base.hh | 113 ++++++ src/transport_fastcgi.cc | 707 ++++++++++++++++++++++++++++++++++ src/transport_fastcgi.hh | 8 + src/transport_http.cc | 193 ++++++++++ src/transport_http.hh | 8 + src/travel.cc | 556 +++++++++++++++++++++++++++ src/travel.hh | 137 +++++++ src/tz_info.cc | 329 ++++++++++++++++ src/tz_info.hh | 28 ++ src/tz_str.cc | 265 +++++++++++++ src/tz_str.hh | 16 + src/unique_fd.cc | 10 + src/unique_fd.hh | 48 +++ src/unique_pipe.cc | 47 +++ src/unique_pipe.hh | 51 +++ src/urlutil.cc | 138 +++++++ src/urlutil.hh | 60 +++ src/video.cc | 171 +++++++++ src/video.hh | 31 ++ src/weak_ptr.hh | 42 +++ 98 files changed, 11793 insertions(+) create mode 100644 src/args.cc create mode 100644 src/args.hh create mode 100644 src/buffer.cc create mode 100644 src/buffer.hh create mode 100644 src/common.hh create mode 100644 src/config.cc create mode 100644 src/config.hh create mode 100644 src/date.cc create mode 100644 src/date.hh create mode 100644 src/document.cc create mode 100644 src/document.hh create mode 100644 src/fcgi_protocol.cc create mode 100644 src/fcgi_protocol.hh create mode 100644 src/file_opener.cc create mode 100644 src/file_opener.hh create mode 100644 src/files_finder.cc create mode 100644 src/files_finder.hh create mode 100644 src/geo_json.cc create mode 100644 src/geo_json.hh create mode 100644 src/hash_method.cc create mode 100644 src/hash_method.hh create mode 100644 src/hash_method_openssl.cc create mode 100644 src/hasher.cc create mode 100644 src/hasher.hh create mode 100644 src/htmlutil.cc create mode 100644 src/htmlutil.hh create mode 100644 src/http_protocol.cc create mode 100644 src/http_protocol.hh create mode 100644 src/image.cc create mode 100644 src/image.hh create mode 100644 src/inet.cc create mode 100644 src/inet.hh create mode 100644 src/io.cc create mode 100644 src/io.hh create mode 100644 src/jsutil.cc create mode 100644 src/jsutil.hh create mode 100644 src/location.hh create mode 100644 src/logger.hh create mode 100644 src/logger_base.cc create mode 100644 src/logger_base.hh create mode 100644 src/logger_file.cc create mode 100644 src/logger_null.cc create mode 100644 src/logger_stdio.cc create mode 100644 src/logger_syslog.cc create mode 100644 src/looper.hh create mode 100644 src/looper_poll.cc create mode 100644 src/mime_types.cc create mode 100644 src/mime_types.hh create mode 100644 src/observer_list.hh create mode 100644 src/pathutil.cc create mode 100644 src/pathutil.hh create mode 100644 src/ro_buffer.cc create mode 100644 src/ro_buffer.hh create mode 100644 src/rotation.hh create mode 100644 src/send_file.cc create mode 100644 src/send_file.hh create mode 100644 src/server.cc create mode 100644 src/signal_handler.cc create mode 100644 src/signal_handler.hh create mode 100644 src/site.cc create mode 100644 src/site.hh create mode 100644 src/static_files.cc create mode 100644 src/static_files.hh create mode 100644 src/str_buffer.cc create mode 100644 src/str_buffer.hh create mode 100644 src/strutil.cc create mode 100644 src/strutil.hh create mode 100644 src/tag.cc create mode 100644 src/tag.hh create mode 100644 src/task_runner.hh create mode 100644 src/task_runner_looper.cc create mode 100644 src/task_runner_reply.hh create mode 100644 src/task_runner_thread.cc create mode 100644 src/timezone.cc create mode 100644 src/timezone.hh create mode 100644 src/transport.cc create mode 100644 src/transport.hh create mode 100644 src/transport_base.cc create mode 100644 src/transport_base.hh create mode 100644 src/transport_fastcgi.cc create mode 100644 src/transport_fastcgi.hh create mode 100644 src/transport_http.cc create mode 100644 src/transport_http.hh create mode 100644 src/travel.cc create mode 100644 src/travel.hh create mode 100644 src/tz_info.cc create mode 100644 src/tz_info.hh create mode 100644 src/tz_str.cc create mode 100644 src/tz_str.hh create mode 100644 src/unique_fd.cc create mode 100644 src/unique_fd.hh create mode 100644 src/unique_pipe.cc create mode 100644 src/unique_pipe.hh create mode 100644 src/urlutil.cc create mode 100644 src/urlutil.hh create mode 100644 src/video.cc create mode 100644 src/video.hh create mode 100644 src/weak_ptr.hh (limited to 'src') diff --git a/src/args.cc b/src/args.cc new file mode 100644 index 0000000..243b284 --- /dev/null +++ b/src/args.cc @@ -0,0 +1,291 @@ +#include "common.hh" + +#include "args.hh" + +#include +#include +#include + +namespace { + +class OptionImpl : public Option { +public: + OptionImpl(char short_name, std::string long_name, std::string description, + bool require_arg, std::string arg_description) + : short_name_(short_name), + long_name_(std::move(long_name)), + description_(std::move(description)), + require_arg_(require_arg), + arg_description_(std::move(arg_description)) { + } + + bool is_set() const override { return set_; } + + std::string const& arg() const override { return arg_; } + + char short_name() const { return short_name_; } + + std::string const& long_name() const { return long_name_; } + + std::string const& description() const { return description_; } + + bool require_arg() const { return require_arg_; } + + std::string const& arg_description() const { return arg_description_; } + + void reset() { + set_ = false; + arg_.clear(); + } + + void set() { + set_ = true; + } + + void set_arg(std::string arg) { + arg_ = std::move(arg); + } + +private: + char const short_name_; + std::string const long_name_; + std::string const description_; + bool const require_arg_; + std::string const arg_description_; + bool set_ = false; + std::string arg_; +}; + +class ArgsImpl : public Args { +public: + ArgsImpl() = default; + + Option const* add_option(char short_name, std::string long_name, + std::string description) override { + prepare_option(short_name, long_name); + options_.push_back(std::make_unique(short_name, + std::move(long_name), + std::move(description), + false, std::string())); + return options_.back().get(); + } + + Option const* add_option_with_arg(char short_name, std::string long_name, + std::string description, + std::string arg_description) override { + prepare_option(short_name, long_name); + options_.push_back(std::make_unique(short_name, + std::move(long_name), + std::move(description), + true, arg_description)); + return options_.back().get(); + } + + bool run(int argc, char** argv, std::string_view prgname, std::ostream& err, + std::vector* out) override { + for (int a = 1; a < argc; ++a) { + if (argv[a][0] == '-') { + if (argv[a][1] == '-') { + if (argv[a][2] != '\0') { + // A long name with optional "=" argument + size_t len = 2; + while (argv[a][len] != '=' && argv[a][len]) + ++len; + std::string name(argv[a] + 2, len - 2); + auto it = long_names_.find(name); + if (it == long_names_.end()) { + err << prgname << ": unrecognized option '--" + << name << "'" << std::endl; + return false; + } + auto* opt = options_[it->second].get(); + opt->set(); + if (argv[a][len]) { + if (opt->require_arg()) { + opt->set_arg(std::string(argv[a] + len + 1)); + } else { + err << prgname << ": option '--" + << name << "' doesn't allow an argument" << std::endl; + return false; + } + } else { + if (opt->require_arg()) { + if (a + 1 >= argc) { + err << prgname << ": option '--" + << name << "' requires an argument" << std::endl; + return false; + } else { + opt->set_arg(argv[++a]); + } + } + } + continue; + } else { + // "--", all following values are arguments + for (++a; a < argc; ++a) + out->push_back(argv[a]); + break; + } + } else if (argv[a][1] != '\0') { + // One or more short names + for (auto* name = argv[a] + 1; *name; ++name) { + auto it = short_names_.find(*name); + if (it == short_names_.end()) { + err << prgname << ": invalid option -- '" + << *name << "'" << std::endl; + return false; + } + auto* opt = options_[it->second].get(); + opt->set(); + if (opt->require_arg()) { + if (a + 1 >= argc) { + err << prgname << ": option requires an argument" + << " -- '" << *name << "'" << std::endl; + return false; + } else { + opt->set_arg(argv[++a]); + } + } + } + continue; + } else { + // single "-", treat as argument + } + } + + out->push_back(argv[a]); + } + return true; + } + + void print_descriptions(std::ostream& out, + uint32_t column_width) const override { + uint32_t max_left = 0; + for (auto const& option : options_) { + uint32_t left = 0; + if (option->short_name() != '\0') { + if (!option->long_name().empty()) { + left = 6 + option->long_name().size(); // -S, --long + } else { + left = 2; // -S + } + } else if (!option->long_name().empty()) { + left = 2 + option->long_name().size(); // --long + } + if (option->require_arg()) + left += 1 + option->arg_description().size(); // (=| )ARG + if (left > 0) + left += 2; // Need at least two spaces between option and desc + // Prefix with two spaces (either infront of option or desc) + left += 2; + + if (left > max_left) + max_left = left; + } + + uint32_t const avail_right = + max_left > column_width ? 0 : column_width - max_left; + + if (avail_right < 20) { + // Fallback mode, description on its own row. + for (auto const& option : options_) { + print_option(out, *option); + out << '\n' << option->description() << '\n'; + } + return; + } + + // Check if all descriptions fit, justify to the right on a 80 col width + bool all_desc_fit = true; + uint32_t max_right = 0; + for (auto const& option : options_) { + uint32_t right = option->description().size(); + if (right > avail_right) { + all_desc_fit = false; + break; + } + if (right > max_right) + max_right = right; + } + + if (all_desc_fit) + max_left = std::max(80u, column_width) - max_right; + + for (auto const& option : options_) { + out << " "; + uint32_t left = 2 + print_option(out, *option); + std::fill_n(std::ostreambuf_iterator(out), max_left - left, ' '); + + if (option->description().size() <= avail_right) { + out << option->description() << '\n'; + continue; + } + + // Wrap description + size_t last = 0; + bool first = true; + while (true) { + if (first) { + first = false; + } else { + std::fill_n(std::ostreambuf_iterator(out), max_left, ' '); + } + + size_t end = last + avail_right; + if (end >= option->description().size()) { + out << option->description().substr(last) << '\n'; + break; + } + size_t space = option->description().rfind(' ', end); + if (space == std::string::npos || space < last) { + space = end; + } + out << option->description().substr(last, space - last) << '\n'; + last = space < end ? space + 1 : end; + } + } + } + +private: + void prepare_option(char short_name, std::string const& long_name) { + if (short_name != '\0') + short_names_.emplace(short_name, options_.size()); + if (!long_name.empty()) { + assert(long_name.find('=') == std::string::npos); + long_names_.emplace(long_name, options_.size()); + } + } + + size_t print_option(std::ostream& out, const OptionImpl& option) const { + bool only_short = false; + size_t ret = 0; + if (option.short_name() != '\0') { + out << '-' << option.short_name(); + if (!option.long_name().empty()) { + out << ", --" << option.long_name(); + ret = 6 + option.long_name().size(); + } else { + ret = 2; + only_short = true; + } + } else if (!option.long_name().empty()) { + out << "--" << option.long_name(); + ret = 2 + option.long_name().size(); + } + if (option.require_arg()) { + out << (only_short ? ' ' : '=') << option.arg_description(); + ret += 1 + option.arg_description().size(); + } + return ret; + } + + std::vector> options_; + std::unordered_map short_names_; + std::unordered_map long_names_; +}; + +} // namespace + +std::unique_ptr Args::create() { + return std::make_unique(); +} diff --git a/src/args.hh b/src/args.hh new file mode 100644 index 0000000..b581307 --- /dev/null +++ b/src/args.hh @@ -0,0 +1,53 @@ +#ifndef ARGS_HH +#define ARGS_HH + +#include +#include +#include +#include +#include + +class Option { +public: + virtual ~Option() = default; + + virtual bool is_set() const = 0; + virtual std::string const& arg() const = 0; + +protected: + Option() = default; + Option(Option const&) = delete; + Option& operator=(Option const&) = delete; +}; + +class Args { +public: + virtual ~Args() = default; + + static std::unique_ptr create(); + + // Returned Option is owned by Args instance. + virtual Option const* add_option( + char short_name, + std::string long_name, + std::string description) = 0; + + virtual Option const* add_option_with_arg( + char short_name, + std::string long_name, + std::string description, + std::string arg_description) = 0; + + virtual bool run(int argc, char** argv, std::string_view prgname, + std::ostream& err, std::vector* out) = 0; + + virtual void print_descriptions(std::ostream& out, + uint32_t column_width) const = 0; + +protected: + Args() = default; + Args(Args const&) = delete; + Args& operator=(Args const&) = delete; +}; + +#endif // ARGS_HH diff --git a/src/buffer.cc b/src/buffer.cc new file mode 100644 index 0000000..2412dca --- /dev/null +++ b/src/buffer.cc @@ -0,0 +1,236 @@ +#include "common.hh" + +#include "buffer.hh" + +#include +#include + +namespace { + +class Round : public Buffer { +public: + explicit Round(size_t size) + : data_(std::make_unique(size)), end_(data_.get() + size), + rptr_(data_.get()), wptr_(data_.get()), full_(false) {} + + bool empty() const override { + return rptr_ == wptr_ && !full_; + } + + bool full() const override { + return rptr_ == wptr_ && full_; + } + + void clear() override { + rptr_ = wptr_ = data_.get(); + full_ = false; + } + + char const* rbuf(size_t want, size_t& avail) override { + if (rptr_ < wptr_) { + avail = wptr_ - rptr_; + } else if (rptr_ == wptr_ && !full_) { + avail = 0; + } else { + avail = end_ - rptr_; + if (want > avail) { + auto* target = data_.get(); + for (; rptr_ < end_; ++rptr_) { + std::swap(*target, *rptr_); + ++target; + } + rptr_ = data_.get(); + wptr_ += avail; + if (wptr_ == end_) { + assert(full_); + wptr_ = data_.get(); + avail = end_ - rptr_; + } else { + avail = wptr_ - rptr_; + } + } + } + return rptr_; + } + + void rcommit(size_t bytes) override { + if (bytes == 0) + return; + full_ = false; + assert(rptr_ < wptr_ ? rptr_ + bytes <= wptr_ : rptr_ + bytes <= end_); + rptr_ += bytes; + if (rptr_ == end_) + rptr_ = data_.get(); + if (rptr_ == wptr_) + rptr_ = wptr_ = data_.get(); + } + + char* wbuf(size_t request, size_t& avail) override { + if (wptr_ < rptr_) { + avail = rptr_ - wptr_; + } else if (rptr_ == wptr_ && full_) { + avail = 0; + } else { + avail = end_ - wptr_; + if (avail < request && wptr_ != data_.get() && rptr_ != data_.get()) { + std::copy(rptr_, wptr_, data_.get()); + auto size = wptr_ - rptr_; + wptr_ = data_.get() + size; + rptr_ = data_.get(); + avail = end_ - wptr_; + } + } + return wptr_; + } + + void wcommit(size_t bytes) override { + if (bytes == 0) + return; + assert(wptr_ < rptr_ ? wptr_ + bytes <= rptr_ : wptr_ + bytes <= end_); + wptr_ += bytes; + if (wptr_ == end_) + wptr_ = data_.get(); + if (wptr_ == rptr_) + full_ = true; + } + +private: + std::unique_ptr data_; + char* const end_; + char* rptr_; + char* wptr_; + bool full_; +}; + +class Growing : public Buffer { +public: + Growing(size_t base_size, size_t max_size) + : base_size_(base_size), max_size_(max_size), data_(base_size) { + } + + bool empty() const override { + return rptr_ == wptr_; + } + + bool full() const override { + return rptr_ == 0 && wptr_ == max_size_; + } + + void clear() override { + data_.resize(base_size_); + rptr_ = wptr_ = 0; + } + + char const* rbuf(size_t, size_t& avail) override { + avail = wptr_ - rptr_; + return data_.data() + rptr_; + } + + void rcommit(size_t bytes) override { + assert(rptr_ + bytes <= wptr_); + rptr_ += bytes; + if (rptr_ == wptr_) + rptr_ = wptr_ = 0; + } + + char* wbuf(size_t request, size_t& avail) override { + avail = data_.size() - wptr_; + if (request > avail && rptr_ > 0) { + std::copy(data_.begin() + rptr_, data_.begin() + wptr_, data_.begin()); + wptr_ -= rptr_; + rptr_ = 0; + avail = data_.size() - wptr_; + } + if (request > avail && data_.size() < max_size_) { + data_.resize( + std::min(max_size_, + data_.size() + std::max(request - avail, + (max_size_ - base_size_) / 8))); + avail = data_.size() - wptr_; + } + return data_.data() + wptr_; + } + + void wcommit(size_t bytes) override { + assert(wptr_ + bytes <= data_.size()); + wptr_ += bytes; + } + +private: + size_t const base_size_; + size_t const max_size_; + std::vector data_; + size_t rptr_{0}; + size_t wptr_{0}; +}; + +class Null : public Buffer { +public: + bool empty() const override { + return true; + } + + char const* rbuf(size_t, size_t& avail) override { + avail = 0; + return buf_; + } + + void rcommit(size_t bytes) override { + assert(bytes == 0); + } + + bool full() const override { + return false; + } + + void clear() override {} + + char* wbuf(size_t, size_t& avail) override { + avail = sizeof(buf_); + return buf_; + } + + void wcommit(size_t) override { + } + +private: + char buf_[4096]; +}; + +} // namespace + +std::unique_ptr Buffer::fixed(size_t size) { + return std::make_unique(size); +} + +std::unique_ptr Buffer::growing(size_t base_size, size_t max_size) { + return std::make_unique(base_size, max_size); +} + +std::unique_ptr Buffer::null() { + return std::make_unique(); +} + +size_t Buffer::write(Buffer* buf, void const* data, size_t len) { + assert(buf); + assert(data); + if (len == 0) + return 0; + auto* d = reinterpret_cast(data); + size_t wrote = 0; + while (true) { + size_t avail; + auto want = len - wrote; + auto* ptr = buf->wbuf(want, avail); + if (avail == 0) + return wrote; + if (avail >= want) { + std::copy_n(d + wrote, want, ptr); + buf->wcommit(want); + return len; + } + std::copy_n(d + wrote, avail, ptr); + buf->wcommit(avail); + wrote += avail; + } +} diff --git a/src/buffer.hh b/src/buffer.hh new file mode 100644 index 0000000..7f11a7c --- /dev/null +++ b/src/buffer.hh @@ -0,0 +1,25 @@ +#ifndef BUFFER_HH +#define BUFFER_HH + +#include "ro_buffer.hh" + +#include + +class Buffer : public RoBuffer { +public: + static std::unique_ptr fixed(size_t size); + static std::unique_ptr growing(size_t base_size, size_t max_size); + // Acts as /dev/null, ie always empty, can write anything to it. + static std::unique_ptr null(); + + virtual bool full() const = 0; + + virtual void clear() = 0; + + virtual char* wbuf(size_t request, size_t& avail) = 0; + virtual void wcommit(size_t bytes) = 0; + + static size_t write(Buffer* buf, void const* data, size_t len); +}; + +#endif // BUFFER_HH diff --git a/src/common.hh b/src/common.hh new file mode 100644 index 0000000..4eec123 --- /dev/null +++ b/src/common.hh @@ -0,0 +1,16 @@ +#ifndef COMMON_HH +#define COMMON_HH + +#include + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#if HAVE_BUILTIN_UNREACHABLE +#define NOTREACHED __builtin_unreachable() +#else +#define NOTREACHED abort() +#endif + +#endif // COMMON_HH diff --git a/src/config.cc b/src/config.cc new file mode 100644 index 0000000..4d7cf52 --- /dev/null +++ b/src/config.cc @@ -0,0 +1,182 @@ +#include "common.hh" + +#include "config.hh" +#include "io.hh" +#include "logger.hh" +#include "strutil.hh" + +#include +#include +#include +#include +#include + +namespace { + +class ConfigImpl : public Config { +public: + ConfigImpl() + : data_(), root_() {} + + ConfigImpl(std::unordered_map data, + std::filesystem::path root) + : data_(std::move(data)), root_(std::move(root)) {} + + std::string_view get(std::string const& key, + std::string_view default_) const override { + auto it = data_.find(key); + if (it == data_.end()) + return default_; + return it->second; + } + + char const* get(std::string const& key, + char const* default_) const override { + auto it = data_.find(key); + if (it == data_.end()) + return default_; + return it->second.c_str(); + } + + std::optional get(std::string const& key, uint64_t default_) + const override { + auto* data = get(key, nullptr); + if (!data) + return default_; + return str::parse_uint64(data); + } + + std::optional get_size(std::string const& key, uint64_t default_) + const override { + auto* data = get(key, nullptr); + if (!data) + return default_; + char* end = nullptr; + double value = strtod(data, &end); + if (end == data) + return std::nullopt; + std::string_view suffix(end); + if (suffix == "t" || suffix == "T" || suffix == "TB" || suffix == "Tb") + return value * 1024 * 1024 * 1024 * 1024; + if (suffix == "g" || suffix == "G" || suffix == "GB" || suffix == "Gb") + return value * 1024 * 1024 * 1024; + if (suffix == "m" || suffix == "M" || suffix == "MB" || suffix == "Mb") + return value * 1024 * 1024; + if (suffix == "k" || suffix == "K" || suffix == "KB" || suffix == "Kb") + return value * 1024; + if (suffix == "b" || suffix == "B" || suffix == "") + return value; + return std::nullopt; + } + + std::optional get_duration(std::string const& key, double default_) + const override { + auto* data = get(key, nullptr); + if (!data) + return default_; + char* end = nullptr; + double value = strtod(data, &end); + if (end == data) + return std::nullopt; + std::string_view suffix(end); + if (suffix == "h" || suffix == "H") + return value * 60.0 * 60.0; + if (suffix == "m" || suffix == "M") + return value * 60.0; + if (suffix == "ms" || suffix == "MS") + return value / 1000.0; + if (suffix == "ns" || suffix == "NS") + return value / 1000000.0; + if (suffix == "s" || suffix == "S" || suffix == "") + return value; + return std::nullopt; + } + + std::filesystem::path get_path(std::string const& key, + std::string_view default_) const override { + auto it = data_.find(key); + if (it == data_.end()) { + if (default_.empty()) + return std::filesystem::path(); + return root_ / default_; + } + return root_ / it->second; + } + +private: + std::unordered_map const data_; + std::filesystem::path const root_; +}; + + +inline bool is_space(char c) { + return c == ' ' || c == '\t'; +} + +void trim(std::string_view str, size_t* start, size_t* end) { + while (*start < *end && is_space(str[*start])) + ++*start; + while (*end > *start && is_space(str[*end - 1])) + --*end; +} + +std::unique_ptr load(Logger* logger, + std::filesystem::path const& path) { + std::ifstream in(path); + if (!in.good()) { + logger->warn("Unable to open %s for reading: %s", + path.c_str(), strerror(errno)); + return nullptr; + } + auto root = path.parent_path(); + std::unordered_map data; + std::string line; + unsigned long num = 0; + while (std::getline(in, line)) { + ++num; + if (line.empty() || line.front() == '#') + continue; + auto eq = line.find('='); + if (eq == std::string::npos) { + logger->warn("%s:%lu: Invalid line, no equal sign (=).", + path.c_str(), num); + return nullptr; + } + size_t key_start = 0; + size_t key_end = eq; + trim(line, &key_start, &key_end); + if (key_start == key_end) { + logger->warn("%s:%lu: Invalid line, no key before equal sign (=).", + path.c_str(), num); + return nullptr; + } + size_t value_start = eq + 1; + size_t value_end = line.size(); + trim(line, &value_start, &value_end); + auto key = line.substr(key_start, key_end - key_start); + data[key] = line.substr(value_start, value_end - value_start); + } + if (!in.eof()) { + logger->warn("Error reading %s: %s", + path.c_str(), strerror(errno)); + return nullptr; + } + return Config::create(std::move(data), std::move(root)); +} + +} // namespace + +std::unique_ptr Config::create(Logger* logger, + std::filesystem::path const& filepath) { + return load(logger, filepath); +} + +std::unique_ptr Config::create( + std::unordered_map data, + std::filesystem::path root) { + return std::make_unique(std::move(data), std::move(root)); +} + +std::unique_ptr Config::create_empty() { + return std::make_unique(); +} diff --git a/src/config.hh b/src/config.hh new file mode 100644 index 0000000..fd0f067 --- /dev/null +++ b/src/config.hh @@ -0,0 +1,48 @@ +#ifndef CONFIG_HH +#define CONFIG_HH + +#include +#include +#include +#include +#include +#include +#include + +class Logger; + +class Config { +public: + virtual ~Config() = default; + + static std::unique_ptr create(Logger* logger, + std::filesystem::path const& filepath); + + static std::unique_ptr create( + std::unordered_map data, + std::filesystem::path root); + static std::unique_ptr create_empty(); + + virtual std::string_view get(std::string const& key, + std::string_view default_) const = 0; + virtual char const* get(std::string const& key, + char const* default_) const = 0; + virtual std::optional get(std::string const& key, + uint64_t default_) const = 0; + + virtual std::optional get_size(std::string const& key, + uint64_t default_) const = 0; + + virtual std::optional get_duration(std::string const& key, + double default_) const = 0; + + virtual std::filesystem::path get_path(std::string const& key, + std::string_view default_) const = 0; + +protected: + Config() = default; + Config(Config const&) = delete; + Config& operator=(Config const&) = delete; +}; + +#endif // CONFIG_HH diff --git a/src/date.cc b/src/date.cc new file mode 100644 index 0000000..23f0ad6 --- /dev/null +++ b/src/date.cc @@ -0,0 +1,66 @@ +#include "common.hh" + +#include "date.hh" + +#include +#include + +namespace { + +// localtime_r doesn't guarantee to call tzset as localtime does. +pthread_once_t tzset_called = PTHREAD_ONCE_INIT; + +} // namespace + +Date Date::from_format(std::string const& format, + std::string const& str, + bool local_time) { + struct tm tm = {}; + tm.tm_isdst = -1; // Must be -1 so that mktime figures it out by itself. + auto* end = strptime(str.c_str(), format.c_str(), &tm); + if (end && !*end) + return local_time ? mktime(&tm) : timegm(&tm); + return Date(); +} + +std::string Date::to_format(std::string const& format, + bool local_time) const { + if (empty() || format.empty()) + return std::string(); + + if (local_time) + pthread_once(&tzset_called, tzset); + + struct tm mem = {}; + auto* tm = local_time ? localtime_r(&time_, &mem) : gmtime_r(&time_, &mem); + if (!tm) + return std::string(); + + std::string ret; + ret.resize(64); + while (true) { + auto len = strftime(ret.data(), ret.size(), format.c_str(), tm); + if (len > 0) { + ret.resize(len); + return ret; + } + ret.resize(ret.size() * 2); + } +} + +Date Date::day(bool local_time) const { + if (empty()) + return *this; + + if (local_time) + pthread_once(&tzset_called, tzset); + + struct tm tm = {}; + auto* ret = local_time ? localtime_r(&time_, &tm) : gmtime_r(&time_, &tm); + if (!ret) + return *this; + ret->tm_hour = 0; + ret->tm_min = 0; + ret->tm_sec = 0; + return local_time ? mktime(ret) : timegm(ret); +} diff --git a/src/date.hh b/src/date.hh new file mode 100644 index 0000000..8db396e --- /dev/null +++ b/src/date.hh @@ -0,0 +1,70 @@ +#ifndef DATE_HH +#define DATE_HH + +#include +#include + +class Date { +public: + Date() + : time_(-1) {} + + Date(time_t time) + : time_(time) {} + + Date(Date const&) = default; + + Date& operator=(Date const&) = default; + + bool empty() const { + return time_ < 0; + } + + time_t value() const { + return time_; + } + + // Return day of date (ie, set time to 00:00:00) + Date day(bool local_time = true) const; + + bool operator==(Date const& date) const { + return empty() ? date.empty() : time_ == date.time_; + } + + bool operator!=(Date const& date) const { + return !(*this == date); + } + + bool operator<(Date const& date) const { + if (empty()) + return !date.empty(); + if (date.empty()) + return false; + return time_ < date.time_; + } + + bool operator<=(Date const& date) const { + return *this < date || *this == date; + } + + bool operator>(Date const& date) const { + return !(*this <= date); + } + + bool operator>=(Date const& date) const { + return !(*this < date); + } + + // If str matches format, returns a non-empty date. + static Date from_format(std::string const& format, + std::string const& str, + bool local_time = true); + + std::string to_format(std::string const& format, + bool local_time = true) const; + +private: + time_t time_; +}; + +#endif // DATE_HH diff --git a/src/document.cc b/src/document.cc new file mode 100644 index 0000000..498e50f --- /dev/null +++ b/src/document.cc @@ -0,0 +1,66 @@ +#include "common.hh" + +#include "document.hh" +#include "hash_method.hh" +#include "htmlutil.hh" +#include "tag.hh" + +#include + +namespace { + +class DocumentImpl : public Document { +public: + explicit DocumentImpl(std::string title) + : html_(Tag::create("html")), head_(html_->add_tag("head")), + title_(head_->add_tag("title", std::move(title))), + body_(html_->add_tag("body")) { + } + + void add_style(std::string rel_path) override { + head_->add_tag("link") + ->attr("rel", "stylesheet") + ->attr("href", std::move(rel_path)); + } + + void add_script(std::string src_path) override { + head_->add_tag("script") + ->attr("type", "text/javascript") + ->attr("src", std::move(src_path)); + } + + void add_script(std::unique_ptr script) override { + if (!script->has_attr("type")) + script->attr("type", "text/javascript"); + head_->add(std::move(script)); + } + + Tag* body() override { + return body_; + } + + std::unique_ptr build(Transport* transport) override { + std::string data; + html_->render(&data); + auto sha256 = HashMethod::sha256(); + sha256->update(data.data(), data.size()); + auto etag = "\"" + sha256->finish() + "\""; + auto resp = transport->create_ok_data(std::move(data)); + resp->add_header("Content-Type", "text/html; charset=utf-8"); + resp->add_header("ETag", etag); + return resp; + } + +private: + std::unique_ptr html_; + Tag* head_; + Tag* title_; + Tag* body_; +}; + +} // namespace + +std::unique_ptr Document::create(std::string title) { + return std::make_unique(std::move(title)); +} + diff --git a/src/document.hh b/src/document.hh new file mode 100644 index 0000000..20e22f8 --- /dev/null +++ b/src/document.hh @@ -0,0 +1,30 @@ +#ifndef DOCUMENT_HH +#define DOCUMENT_HH + +#include "tag.hh" +#include "transport.hh" + +#include +#include + +class Document { +public: + virtual ~Document() = default; + + static std::unique_ptr create(std::string title); + + virtual void add_style(std::string rel_path) = 0; + virtual void add_script(std::string src_path) = 0; + virtual void add_script(std::unique_ptr script) = 0; + + virtual Tag* body() = 0; + + virtual std::unique_ptr build(Transport* transport) = 0; + +protected: + Document() = default; + Document(Document const&) = delete; + Document& operator=(Document const&) = delete; +}; + +#endif // DOCUMENT_HH diff --git a/src/fcgi_protocol.cc b/src/fcgi_protocol.cc new file mode 100644 index 0000000..b2a6be1 --- /dev/null +++ b/src/fcgi_protocol.cc @@ -0,0 +1,594 @@ +#include "common.hh" + +#include "buffer.hh" +#include "fcgi_protocol.hh" + +#include +#include +#include +#include + +namespace fcgi { + +namespace { + +constexpr const uint8_t FCGI_VERSION_1 = 1; + +struct RawRecord { + uint8_t version; + uint8_t type; + uint8_t request_id_b1; + uint8_t request_id_b0; + uint8_t content_length_b1; + uint8_t content_length_b0; + uint8_t padding_length; + uint8_t reserved; +}; + +class RecordImpl : public Record { +public: + RecordImpl(uint8_t type, uint16_t request_id, uint16_t content_length, + uint8_t padding_length) + : good_(true), type_(type), request_id_(request_id), + content_length_(content_length), padding_length_(padding_length) {} + + RecordImpl() + : good_(false), type_(RecordType::UnknownType), request_id_(0), + content_length_(0), padding_length_(0) {} + + bool good() const override { + return good_; + } + + uint8_t type() const override { + return type_; + } + + uint16_t request_id() const override { + return request_id_; + } + + uint16_t content_length() const override { + return content_length_; + } + + uint8_t padding_length() const override { + return padding_length_; + } + +private: + bool const good_; + uint8_t const type_; + uint16_t const request_id_; + uint16_t const content_length_; + uint8_t const padding_length_; +}; + +class BeginRequestBodyImpl : public BeginRequestBody { +public: + BeginRequestBodyImpl() + : good_(false), role_(0), flags_(0) {} + + BeginRequestBodyImpl(uint16_t role, uint8_t flags) + : good_(true), role_(role), flags_(flags) { + } + + bool good() const override { + return good_; + } + + uint16_t role() const override { + return role_; + } + + uint8_t flags() const override { + return flags_; + } + + static std::unique_ptr parse(uint8_t const* data, + size_t len) { + if (len != 8) + return std::make_unique(); + return std::make_unique( + static_cast(data[0]) << 8 | data[1], data[2]); + } + +private: + bool const good_; + uint16_t const role_; + uint8_t const flags_; +}; + +uint8_t calc_padding(uint16_t length) { + auto extra = length % 8; + return extra ? 8 - extra : 0; +} + +class RecordBuilderImpl : public RecordBuilder { +public: + RecordBuilderImpl(RecordType type, uint16_t request_id, + uint16_t content_length, uint8_t padding_length) + : type_(type), request_id_(request_id), content_length_(content_length), + padding_length_(padding_length) {} + + RecordBuilderImpl(RecordType type, uint16_t request_id, std::string body) + : type_(type), request_id_(request_id), content_length_(body.size()), + padding_length_(calc_padding(content_length_)), + body_(std::move(body)) { + assert(body_->size() <= std::numeric_limits::max()); + } + + bool build(Buffer* dst) const override { + size_t avail; + size_t need = sizeof(RawRecord) + + (body_ ? content_length_ + padding_length_ : 0); + auto* ptr = dst->wbuf(need, avail); + if (!build(ptr, avail)) + return false; + dst->wcommit(need); + return true; + } + + bool build(char* ptr, size_t avail) const override { + if (avail < sizeof(RawRecord) + + (body_ ? content_length_ + padding_length_ : 0)) + return false; + auto* raw = reinterpret_cast(ptr); + raw->version = FCGI_VERSION_1; + raw->type = type_; + raw->request_id_b1 = request_id_ >> 8; + raw->request_id_b0 = request_id_ & 0xff; + raw->content_length_b1 = content_length_ >> 8; + raw->content_length_b0 = content_length_ & 0xff; + raw->padding_length = padding_length_; + if (body_) { + std::copy_n(body_->data(), body_->size(), ptr + sizeof(RawRecord)); + std::fill_n(ptr + sizeof(RawRecord) + body_->size(), padding_length_, + '\0'); + } + return true; + } + + size_t size() const override { + return sizeof(RawRecord) + content_length_ + padding_length_; + } + + bool padding(Buffer* dst) const override { + size_t avail; + auto* ptr = dst->wbuf(padding_length_, avail); + if (!padding(ptr, avail)) + return false; + dst->wcommit(padding_length_); + return true; + } + + bool padding(char* ptr, size_t avail) const override { + if (avail < padding_length_) + return false; + std::fill_n(ptr, padding_length_, '\0'); + return true; + } + +private: + RecordType const type_; + uint16_t const request_id_; + uint16_t const content_length_; + uint8_t const padding_length_; + std::optional const body_; +}; + +std::unique_ptr parse_raw(RawRecord const& raw) { + if (raw.version != FCGI_VERSION_1) + return std::make_unique(); + return std::make_unique( + raw.type, + static_cast(raw.request_id_b1) << 8 | raw.request_id_b0, + static_cast(raw.content_length_b1) << 8 | raw.content_length_b0, + raw.padding_length); +} + +bool parse_pair(RecordStream* stream, RoBuffer* buf, + std::pair& pair) { + size_t avail; + auto* ptr = stream->rbuf(buf, 2, avail); + if (avail < 2) + return false; + auto* u8 = reinterpret_cast(ptr); + if (u8[0] & 0x80) { + size_t need = 5; + ptr = stream->rbuf(buf, need, avail); + if (avail < need) + return false; + u8 = reinterpret_cast(ptr); + auto name_len = static_cast(u8[0] & 0x7f) << 24 | + static_cast(u8[1]) << 16 | + static_cast(u8[2]) << 8 | + u8[3]; + if (u8[4] & 0x80) { + need = 8 + name_len; + ptr = stream->rbuf(buf, need, avail); + if (avail < need) + return false; + u8 = reinterpret_cast(ptr); + auto value_len = static_cast(u8[4] & 0x7f) << 24 | + static_cast(u8[5]) << 16 | + static_cast(u8[6]) << 8 | + u8[7]; + need = 8 + name_len + value_len; + ptr = stream->rbuf(buf, need, avail); + if (avail < need) + return false; + pair.first.assign(ptr + 8, name_len); + pair.second.assign(ptr + 8 + name_len, value_len); + stream->rcommit(buf, need); + return true; + } else { + auto value_len = u8[4]; + need = 5 + name_len + value_len; + ptr = stream->rbuf(buf, need, avail); + if (avail < need) + return false; + pair.first.assign(ptr + 5, name_len); + pair.second.assign(ptr + 5 + name_len, value_len); + stream->rcommit(buf, need); + return true; + } + } else if (u8[1] & 0x80) { + auto name_len = u8[0]; + size_t need = 5 + name_len; + ptr = stream->rbuf(buf, need, avail); + if (avail < need) + return false; + u8 = reinterpret_cast(ptr); + auto value_len = static_cast(u8[1] & 0x7f) << 24 | + static_cast(u8[2]) << 16 | + static_cast(u8[3]) << 8 | + u8[4]; + need = 5 + name_len + value_len; + ptr = stream->rbuf(buf, need, avail); + if (avail < need) + return false; + pair.first.assign(ptr + 5, name_len); + pair.second.assign(ptr + 5 + name_len, value_len); + stream->rcommit(buf, need); + return true; + } else { + auto name_len = u8[0]; + auto value_len = u8[1]; + size_t need = 2 + name_len + value_len; + ptr = stream->rbuf(buf, need, avail); + if (avail < need) + return false; + pair.first.assign(ptr + 2, name_len); + pair.second.assign(ptr + 2 + name_len, value_len); + stream->rcommit(buf, need); + return true; + } +} + +class RecordStreamImpl : public RecordStream { +public: + RecordStreamImpl(Record const* record, bool ended) + : type_(record->type()), + padding_(record->padding_length()), + left_(static_cast(record->content_length()) + padding_), + leftover_(Buffer::growing(64, 8192)), + ended_(ended || record->content_length() == 0) { + check_end_of_stream(); + } + + char const* rbuf(RoBuffer* buf, size_t want, size_t& avail) override { + if (leftover_->empty()) { + size_t content_avail = left_ - padding_; + // Avoid want == content_avail as that might miss the padding. + if (want < content_avail) { + auto* ptr = buf->rbuf(want, avail); + if (avail > content_avail) + avail = content_avail; + return ptr; + } + auto* ptr = buf->rbuf(left_, avail); + if (avail < left_) { + avail = 0; + return nullptr; + } + Buffer::write(leftover_.get(), ptr, left_ - padding_); + buf->rcommit(left_); + left_ = 0; + padding_ = 0; + } + auto* ptr = leftover_->rbuf(want, avail); + if (left_ > 0 && want > avail) { + size_t tmp_avail; + size_t tmp_need = want - avail; + size_t tmp_want = tmp_need; + if (tmp_want >= left_ - padding_) + tmp_want = left_; + auto* tmp = buf->rbuf(tmp_want, tmp_avail); + if (tmp_avail < tmp_want) + return ptr; + Buffer::write(leftover_.get(), tmp, tmp_need); + buf->rcommit(tmp_want); + left_ -= tmp_want; + if (left_ == 0) + padding_ = 0; + ptr = leftover_->rbuf(want, avail); + } + return ptr; + } + + void rcommit(RoBuffer* buf, size_t bytes) override { + if (leftover_->empty()) { + assert(bytes <= left_ - padding_); + left_ -= bytes; + assert(left_ >= padding_); + if (left_ == padding_) { + buf->rcommit(bytes + padding_); + left_ = 0; + padding_ = 0; + check_end_of_stream(); + } else { + buf->rcommit(bytes); + } + } else { + leftover_->rcommit(bytes); + check_end_of_stream(); + } + } + + bool end_of_record() const override { + return left_ == 0; + } + + bool end_of_stream() const override { + return end_of_stream_; + } + + bool all_available() override { + if (!ended_) + return false; + return left_ == 0; + } + + void add(Record const* record) override { + assert(left_ == 0); + assert(!ended_); + assert(record->type() == type_); + + if (record->content_length() == 0) + ended_ = true; + padding_ = record->padding_length(); + left_ = static_cast(record->content_length()) + padding_; + + check_end_of_stream(); + } + +private: + void check_end_of_stream() { + if (end_of_stream_) + return; + if (ended_ && left_ == 0 && leftover_->empty()) + end_of_stream_ = true; + } + + uint8_t const type_; + uint8_t padding_; + size_t left_; + + std::unique_ptr leftover_; + + // True if zero length record was added(). + bool ended_; + // True if ended_ is true and all content has been read. + bool end_of_stream_{false}; +}; + +class PairImpl : public Pair { +public: + PairImpl(std::string name, std::string value) + : good_(true), name_(std::move(name)), value_(std::move(value)) {} + PairImpl() + : good_(false) {} + + bool good() const override { + return good_; + } + + std::string const& name() const override { + return name_; + } + + std::string const& value() const override { + return value_; + } + + bool next(RecordStream* stream, RoBuffer* buf) override { + if (stream->end_of_stream()) + return false; + std::pair tmp; + if (parse_pair(stream, buf, tmp)) { + if (good_) { + name_ = std::move(tmp.first); + value_ = std::move(tmp.second); + } + return true; + } + if (stream->all_available()) { + good_ = false; + name_.clear(); + value_.clear(); + return true; + } + return false; + } + +private: + bool good_; + std::string name_; + std::string value_; +}; + +class PairBuilderImpl : public PairBuilder { +public: + PairBuilderImpl() = default; + + void add(std::string name, std::string value) override { + data_.emplace_back(std::move(name), std::move(value)); + } + + size_t size() const override { + size_t count = 0; + for (auto const& pair : data_) { + count += str_need(pair.first.size()); + count += str_need(pair.second.size()); + } + return count; + } + + bool build(Buffer* buf) const override { + auto need = size(); + size_t avail; + auto* ptr = reinterpret_cast(buf->wbuf(need, avail)); + if (avail < need) + return false; + size_t offset = 0; + for (auto const& pair : data_) { + if (pair.first.size() < 128) { + ptr[offset++] = pair.first.size(); + } else { + writeu32(ptr + offset, pair.first.size()); + offset += 4; + } + if (pair.second.size() < 128) { + ptr[offset++] = pair.second.size(); + } else { + writeu32(ptr + offset, pair.second.size()); + offset += 4; + } + std::copy_n(pair.first.data(), pair.first.size(), ptr + offset); + offset += pair.first.size(); + std::copy_n(pair.second.data(), pair.second.size(), ptr + offset); + offset += pair.second.size(); + } + assert(offset == need); + buf->wcommit(offset); + return true; + } + +private: + static size_t str_need(size_t len) { + return len < 128 ? 1 + len : 4 + len; + } + + static void writeu32(uint8_t* dst, uint32_t value) { + assert(value <= 0x7ffffffful); + dst[0] = (value >> 24) | 0x80; + dst[1] = (value >> 16) & 0xff; + dst[2] = (value >> 8) & 0xff; + dst[3] = value & 0xff; + } + + std::vector> data_; +}; + +} // namespace + +std::unique_ptr Record::parse(RoBuffer* buffer) { + static_assert(sizeof(RawRecord) == 8); + size_t avail; + auto* ptr = buffer->rbuf(sizeof(RawRecord), avail); + if (avail < sizeof(RawRecord)) + return nullptr; + auto ret = parse_raw(*reinterpret_cast(ptr)); + buffer->rcommit(sizeof(RawRecord)); + return ret; +} + +std::unique_ptr BeginRequestBody::parse(Record const* record, + RoBuffer* buffer) { + if (record->type() != RecordType::BeginRequest) + return std::make_unique(); + if (record->content_length() != 8) + return std::make_unique(); + auto need = static_cast(record->content_length()) + + record->padding_length(); + size_t avail; + auto* ptr = reinterpret_cast(buffer->rbuf(need, avail)); + if (avail < need) + return nullptr; + auto ret = BeginRequestBodyImpl::parse(ptr, record->content_length()); + buffer->rcommit(need); + return ret; +} + +std::unique_ptr RecordStream::create_stream( + Record const* record) { + return std::make_unique(record, false); +} + +std::unique_ptr RecordStream::create_single( + Record const* record) { + return std::make_unique(record, true); +} + +std::unique_ptr Pair::start(RecordStream* stream, RoBuffer* buf) { + std::pair tmp; + if (stream->end_of_stream()) + return nullptr; + if (parse_pair(stream, buf, tmp)) + return std::make_unique(std::move(tmp.first), + std::move(tmp.second)); + if (stream->all_available()) + return std::make_unique(); + return nullptr; +} + +std::unique_ptr RecordBuilder::create(RecordType type, + uint16_t request_id, + uint16_t content_length, + int16_t padding_length) { + if (padding_length < 0) + padding_length = calc_padding(content_length); + return std::make_unique(type, request_id, content_length, + padding_length & 0xff); +} + +std::unique_ptr RecordBuilder::create(RecordType type, + uint16_t request_id, + std::string body) { + return std::make_unique(type, request_id, std::move(body)); +} + +std::unique_ptr RecordBuilder::create_unknown_type( + uint8_t unknown_type) { + std::string body(8, '\0'); + body[0] = unknown_type; + return create(RecordType::UnknownType, 0, std::move(body)); +} + +std::unique_ptr RecordBuilder::create_begin_request( + uint16_t request_id, Role role, uint8_t flags) { + std::string body(8, '\0'); + uint16_t tmp_role = role; + body[0] = tmp_role >> 8; + body[1] = tmp_role & 0xff; + body[2] = flags; + return create(RecordType::BeginRequest, request_id, std::move(body)); +} + +std::unique_ptr RecordBuilder::create_end_request( + uint16_t request_id, uint32_t app_status, ProtocolStatus protocol_status) { + std::string body(8, '\0'); + body[0] = app_status >> 24; + body[1] = (app_status >> 16) & 0xff; + body[2] = (app_status >> 8) & 0xff; + body[3] = app_status & 0xff; + body[4] = protocol_status; + return create(RecordType::EndRequest, request_id, std::move(body)); +} + +std::unique_ptr PairBuilder::create() { + return std::make_unique( ); +} + +} // namespace fcgi diff --git a/src/fcgi_protocol.hh b/src/fcgi_protocol.hh new file mode 100644 index 0000000..f41c4a8 --- /dev/null +++ b/src/fcgi_protocol.hh @@ -0,0 +1,191 @@ +#ifndef FCGI_PROTOCOL_HH +#define FCGI_PROTOCOL_HH + +#include +#include +#include + +class Buffer; +class RoBuffer; + +namespace fcgi { + +enum RecordType { + BeginRequest = 1, + AbortRequest = 2, + EndRequest = 3, + Params = 4, + Stdin = 5, + Stdout = 6, + Stderr = 7, + Data = 8, + GetValues = 9, + GetValuesResult = 10, + UnknownType = 11, +}; + +enum Role { + Responder = 1, + Authorizer = 2, + Filter = 3, +}; + +enum Flags { + KeepConn = 1, +}; + +enum ProtocolStatus { + RequestComplete = 0, + CantMpxConn = 1, + Overloaded = 2, + UnknownRole = 3, +}; + +class Record { +public: + virtual ~Record() = default; + + virtual bool good() const = 0; + + virtual uint8_t type() const = 0; + virtual uint16_t request_id() const = 0; + virtual uint16_t content_length() const = 0; + virtual uint8_t padding_length() const = 0; + + static std::unique_ptr parse(RoBuffer* buffer); + +protected: + Record() = default; + Record(Record const&) = delete; + Record& operator=(Record const&) = delete; +}; + +class BeginRequestBody { +public: + virtual ~BeginRequestBody() = default; + + virtual bool good() const = 0; + + virtual uint16_t role() const = 0; + virtual uint8_t flags() const = 0; + + static std::unique_ptr parse(Record const* record, + RoBuffer* buffer); + +protected: + BeginRequestBody() = default; + BeginRequestBody(BeginRequestBody const&) = delete; + BeginRequestBody& operator=(BeginRequestBody const&) = delete; +}; + +class RecordStream { +public: + virtual ~RecordStream() = default; + + // True if all data has been read from stream. + virtual bool end_of_stream() const = 0; + // True if all data from last added record has been read. + virtual bool end_of_record() const = 0; + // True if no more data is coming, ie. what rbuf() returned last is all + // there will ever be. + virtual bool all_available() = 0; + + virtual char const* rbuf(RoBuffer* buf, size_t want, size_t& avail) = 0; + virtual void rcommit(RoBuffer* buf, size_t bytes) = 0; + + // Call when next package for stream arrives. + virtual void add(Record const* record) = 0; + + // Create stream with record as the first package in stream. + static std::unique_ptr create_stream(Record const* record); + + // Create stream with record as the only package in stream. + static std::unique_ptr create_single(Record const* record); + +protected: + RecordStream() = default; + RecordStream(RecordStream const&) = delete; + RecordStream& operator=(RecordStream const&) = delete; +}; + +class Pair { +public: + virtual ~Pair() = default; + + // Returns false if bad encoding or an invalid stream was found. + // Calling next() will NOT change a false value back to true. + virtual bool good() const = 0; + virtual std::string const& name() const = 0; + virtual std::string const& value() const = 0; + + // Returns nullptr if stream and buf needs more data. + static std::unique_ptr start( + RecordStream* stream, RoBuffer* buf); + + // Returns false if stream and buf needs more data. In that case Pair + // is not modified. + virtual bool next(RecordStream* stream, RoBuffer* buf) = 0; + +protected: + Pair() = default; + Pair(Pair const&) = delete; + Pair& operator=(Pair const&) = delete; +}; + +class RecordBuilder { +public: + virtual ~RecordBuilder() = default; + + virtual bool build(Buffer* dst) const = 0; + virtual bool build(char* dst, size_t avail) const = 0; + virtual bool padding(Buffer* dst) const = 0; + virtual bool padding(char* dst, size_t avail) const = 0; + virtual size_t size() const = 0; + + static std::unique_ptr create( + RecordType type, + uint16_t request_id, + uint16_t content_length, + int16_t padding_length = -1); + + static std::unique_ptr create( + RecordType type, + uint16_t request_id, + std::string body); + + static std::unique_ptr create_unknown_type( + uint8_t unknown_type); + + static std::unique_ptr create_begin_request( + uint16_t request_id, Role role, uint8_t flags); + + static std::unique_ptr create_end_request( + uint16_t request_id, uint32_t app_status, ProtocolStatus protocol_status); + +protected: + RecordBuilder() = default; + RecordBuilder(RecordBuilder const&) = delete; + RecordBuilder& operator=(RecordBuilder const&) = delete; +}; + +class PairBuilder { +public: + virtual ~PairBuilder() = default; + + virtual void add(std::string name, std::string value) = 0; + + virtual size_t size() const = 0; + + virtual bool build(Buffer* buf) const = 0; + + static std::unique_ptr create(); + +protected: + PairBuilder() = default; + PairBuilder(PairBuilder const&) = delete; + PairBuilder& operator=(PairBuilder const&) = delete; +}; + +} // namespace fcgi + +#endif // FCGI_PROTOCOL_HH diff --git a/src/file_opener.cc b/src/file_opener.cc new file mode 100644 index 0000000..60aaa66 --- /dev/null +++ b/src/file_opener.cc @@ -0,0 +1,106 @@ +#include "common.hh" + +#include "file_opener.hh" +#include "io.hh" +#include "task_runner.hh" +#include "weak_ptr.hh" + +#include +#include + +namespace { + +class FileOpenerImpl : public FileOpener { +public: + FileOpenerImpl(std::shared_ptr runner, size_t threads) + : runner_(std::move(runner)), workers_(TaskRunner::create(threads)), + weak_ptr_owner_(this) {} + + uint32_t open(std::filesystem::path path, + std::function callback) override { + uint32_t id; + { + std::lock_guard lock(jobs_mutex_); + while (true) { + id = next_id_++; + if (next_id_ == 0) + next_id_ = 1; + if (jobs_.find(id) == jobs_.end()) + break; + } + jobs_[id].callback_ = std::move(callback); + } + workers_->post(std::bind(&FileOpenerImpl::do_open, this, id, path)); + return id; + } + + void cancel(uint32_t id) override { + std::lock_guard lock(jobs_mutex_); + auto it = jobs_.find(id); + if (it == jobs_.end()) + return; + jobs_.erase(it); + } + +private: + struct Job { + std::function callback_; + unique_fd fd_; + }; + + void done(uint32_t id) { + std::lock_guard lock(jobs_mutex_); + auto it = jobs_.find(id); + if (it == jobs_.end()) + return; + auto fd = std::move(it->second.fd_); + auto callback = std::move(it->second.callback_); + jobs_.erase(it); + callback(id, std::move(fd)); + } + + void do_open(uint32_t id, std::filesystem::path path) { + auto fd = io::open(path, io::open_flags::rdonly); + if (fd) { + if (!io::make_nonblocking(fd.get())) { + assert(false); + fd.reset(); + } + } + if (fd) { + std::lock_guard lock(jobs_mutex_); + auto it = jobs_.find(id); + if (it == jobs_.end()) + return; + it->second.fd_ = std::move(fd); + } + runner_->post( + std::bind(&FileOpenerImpl::weak_done, weak_ptr_owner_.get(), id)); + } + + static void weak_done(std::shared_ptr> weak_ptr, + uint32_t id) { + auto* ptr = weak_ptr->get(); + if (ptr) + ptr->done(id); + } + + std::shared_ptr runner_; + std::shared_ptr> weak_ptr_; + uint32_t next_id_{1}; + + std::mutex jobs_mutex_; + std::unordered_map jobs_; + + // It is important that workers_ is (next to) last as it blocks leftover + // workers in destructor so should be destroyed first. + std::unique_ptr workers_; + WeakPtrOwner weak_ptr_owner_; +}; + +} // namespace + +std::unique_ptr FileOpener::create( + std::shared_ptr runner, size_t threads) { + return std::make_unique(std::move(runner), threads); +} diff --git a/src/file_opener.hh b/src/file_opener.hh new file mode 100644 index 0000000..7b01b9f --- /dev/null +++ b/src/file_opener.hh @@ -0,0 +1,33 @@ +#ifndef FILE_OPENER_HH +#define FILE_OPENER_HH + +#include "unique_fd.hh" + +#include +#include +#include + +class TaskRunner; + +class FileOpener { +public: + virtual ~FileOpener() = default; + + // All callbacks are posted to runner. open() and cancel() must be called + // on same thread as runner "runs" on. + static std::unique_ptr create(std::shared_ptr runner, + size_t threads = 1); + + // Never returns 0. + virtual uint32_t open(std::filesystem::path path, + std::function callback) = 0; + + virtual void cancel(uint32_t id) = 0; + +protected: + FileOpener() = default; + FileOpener(FileOpener const&) = delete; + FileOpener& operator=(FileOpener const&) = delete; +}; + +#endif // FILE_OPENER_HH diff --git a/src/files_finder.cc b/src/files_finder.cc new file mode 100644 index 0000000..fef05ba --- /dev/null +++ b/src/files_finder.cc @@ -0,0 +1,231 @@ +#include "common.hh" + +#include "files_finder.hh" +#include "io.hh" +#include "logger.hh" +#include "task_runner.hh" +#include "unique_fd.hh" +#include "weak_ptr.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr uint8_t kMaxQueued = 128; + +class FilesFinderImpl : public FilesFinder { +public: + FilesFinderImpl( + std::shared_ptr logger, + std::shared_ptr runner, + std::filesystem::path root, + Delegate* delegate, + size_t threads) + : logger_(std::move(logger)), runner_(std::move(runner)), + workers_(TaskRunner::create(threads)), root_(std::move(root)), + delegate_(delegate) { + workers_->post(std::bind(&FilesFinderImpl::open_root, this)); + } + +private: + void open_root() { + unique_fd fd = io::open( + root_, io::open_flags::rdonly | io::open_flags::directory); + if (fd) { + increment_queued(); + list_dir(std::move(fd), root_, 0); + } else { + logger_->warn("Unable to open %s: %s", root_.c_str(), strerror(errno)); + runner_->post(std::bind(&Delegate::done, delegate_)); + } + } + + void open_dir(int fd, std::filesystem::path path, uint16_t depth) { + list_dir(unique_fd(fd), path, depth); + } + + void list_dir(unique_fd fd, std::filesystem::path path, uint16_t depth) { + DIR* dh = fdopendir(fd.get()); + if (!dh) { + logger_->warn("Unable to list %s: %s", path.c_str(), strerror(errno)); + return; + } + fd.release(); // fd is now owned by dh + while (true) { + errno = 0; + auto* de = readdir(dh); + if (!de) { + if (errno) + logger_->warn("Error listing %s: %s", path.c_str(), strerror(errno)); + break; + } + size_t namlen; +#ifdef _DIRENT_HAVE_D_NAMLEN + namlen = de->d_namlen; +#else + namlen = strlen(de->d_name); +#endif + if (namlen == 2 && de->d_name[0] == '.' && de->d_name[1] == '.') + continue; + std::optional is_dir; +#ifdef _DIRENT_HAVE_D_TYPE + switch (de->d_type) { + case DT_DIR: + is_dir = true; + break; + case DT_CHR: + case DT_FIFO: + case DT_BLK: + case DT_REG: + case DT_SOCK: + case DT_WHT: + is_dir = false; + break; + case DT_LNK: + case DT_UNKNOWN: + default: + break; + } +#endif + if (!is_dir.has_value()) { + struct stat buf; + if (fstatat(dirfd(dh), de->d_name, &buf, 0)) { + logger_->warn("Unable to stat: %s/%s: %s", + path.c_str(), de->d_name, strerror(errno)); + continue; + } + is_dir = S_ISDIR(buf.st_mode); + } + + std::string_view name(de->d_name, namlen); + + if (is_dir.value()) { + if (delegate_->include_dir(name, depth)) { + unique_fd new_fd = io::openat( + dirfd(dh), de->d_name, + io::open_flags::rdonly | io::open_flags::directory); + if (new_fd) { + increment_queued(); + workers_->post(std::bind(&FilesFinderImpl::open_dir, this, + new_fd.release(), path / name, + depth + 1)); + } else { + logger_->warn("Unable to open %s/%s: %s", path.c_str(), de->d_name, + strerror(errno)); + } + } + } else { + if (delegate_->include_file(name, depth)) { + increment_active(); + runner_->post(std::bind( + &FilesFinderImpl::weak_call_file, + weak_ptr_owner_.get(), + path / name)); + } + } + } + closedir(dh); + + decrement_queued(); + } + + void increment_queued() { + // Queued is used both to keep track of when done() should be called + // but also to max sure we don't queued too many directories as + // each queued entry costs one open file. + std::unique_lock lock(queued_mutex_); + while (queued_ >= kMaxQueued) { + queued_cond_.wait(lock); + } + ++queued_; + } + + void increment_active() { + std::lock_guard lock(queued_mutex_); + ++active_; + } + + void decrement_queued() { + bool notify; + bool post_done; + { + std::lock_guard lock(queued_mutex_); + notify = queued_ >= kMaxQueued; + --queued_; + post_done = queued_ == 0 && active_ == 0; + } + if (notify) + queued_cond_.notify_one(); + if (post_done) + runner_->post(std::bind(&Delegate::done, delegate_)); + } + + void decrement_active() { + bool post_done; + { + std::lock_guard lock(queued_mutex_); + --active_; + post_done = queued_ == 0 && active_ == 0; + } + if (post_done) + runner_->post(std::bind(&Delegate::done, delegate_)); + } + + static void weak_call_file(std::shared_ptr> weak_ptr, + std::filesystem::path path) { + auto* ptr = weak_ptr->get(); + if (ptr) + ptr->call_file(std::move(path)); + } + + void call_file(std::filesystem::path path) { + delegate_->file(std::move(path)); + decrement_active(); + } + + std::shared_ptr logger_; + std::shared_ptr runner_; + std::shared_ptr workers_; + std::filesystem::path const root_; + Delegate* const delegate_; + + std::mutex queued_mutex_; + std::condition_variable queued_cond_; + uint8_t queued_{0}; + size_t active_{0}; + + WeakPtrOwner weak_ptr_owner_{this}; +}; + +} // namespace + +bool FilesFinder::Delegate::include_file(std::string_view name, + uint16_t /* depth */) const { + return name.empty() || name.front() != '.'; +} + +bool FilesFinder::Delegate::include_dir(std::string_view name, + uint16_t /* depth */) const { + return name.empty() || name.front() != '.'; +} + +void FilesFinder::Delegate::done() {} + +std::unique_ptr FilesFinder::create( + std::shared_ptr logger, + std::shared_ptr runner, + std::filesystem::path root, + Delegate* delegate, + size_t threads) { + return std::make_unique(std::move(logger), std::move(runner), + std::move(root), delegate, threads); +} + diff --git a/src/files_finder.hh b/src/files_finder.hh new file mode 100644 index 0000000..2928efa --- /dev/null +++ b/src/files_finder.hh @@ -0,0 +1,51 @@ +#ifndef FILES_FINDER_HH +#define FILES_FINDER_HH + +#include +#include + +class Logger; +class TaskRunner; + +class FilesFinder { +public: + class Delegate { + public: + virtual ~Delegate() = default; + + // Called on any thread, default implementation + // returns true for files not hidden. + // Depth is counted from root, files in root have depth zero. + virtual bool include_file(std::string_view name, uint16_t depth) const; + + // Called on any thread, default implementation + // returns true for dirs not hidden. + // Depth is counted from root, files in root have depth zero. + virtual bool include_dir(std::string_view name, uint16_t depth) const; + + // Called for each file found. Called on runner. + virtual void file(std::filesystem::path path) = 0; + + // Called after all files have been found. Called on runner. + // Default implementation does nothing. + virtual void done(); + + protected: + Delegate() = default; + }; + + virtual ~FilesFinder() = default; + + static std::unique_ptr create(std::shared_ptr logger, + std::shared_ptr runner, + std::filesystem::path root, + Delegate* delegate, + size_t threads = 1); + +protected: + FilesFinder() = default; + FilesFinder(FilesFinder const&) = delete; + FilesFinder& operator=(FilesFinder const&) = delete; +}; + +#endif // FILES_HH diff --git a/src/geo_json.cc b/src/geo_json.cc new file mode 100644 index 0000000..3c1431b --- /dev/null +++ b/src/geo_json.cc @@ -0,0 +1,345 @@ +#include "common.hh" + +#include "geo_json.hh" +#include "logger.hh" + +#include +#include +#include +#include +#include + +namespace { + +struct Point { + double x; + double y; + + Point(double x, double y) + : x(x), y(y) {} + + bool operator==(Point const& pt) const { + return x == pt.x && y == pt.y; + } + + bool operator!=(Point const& pt) const { + return x != pt.x || y != pt.y; + } +}; + +// Copyright 2000 softSurfer, 2012 Dan Sunday +// This code may be freely used and modified for any purpose +// providing that this copyright notice is included with it. +// SoftSurfer makes no warranty for this code, and cannot be held +// liable for any real or imagined damage resulting from its use. +// Users of this code must verify correctness for their application. +inline double is_left(Point p0, Point p1, Point p2) { + return (p1.x - p0.x) * (p2.y - p0.y) + - (p2.x - p0.x) * (p1.y - p0.y); +} + +int wn_pnpoly(Point pt, std::vector const& poly) { + int wn = 0; // the winding number counter + + assert(poly.size() >= 2); + assert(poly.front() == poly.back()); + + // loop through all edges of the polygon + // edge from poly[i] to poly[i+1]. poly[n] == poly[0]. + for (size_t i = 0; i < poly.size() -1 ; ++i) { + if (poly[i].y <= pt.y) { // start y <= pt.y + if (poly[i + 1].y > pt.y) // an upward crossing + if (is_left(poly[i], poly[i + 1], pt) > 0) // P left of edge + ++wn; // have a valid up intersect + } else { // start y > P.y (no test needed) + if (poly[i + 1].y <= pt.y) // a downward crossing + if (is_left(poly[i], poly[i + 1], pt) < 0) // P right of edge + --wn; // have a valid down intersect + } + } + return wn; +} + +bool in_polygon(Point pt, std::vector const& poly) { + return wn_pnpoly(pt, poly) != 0; +} + +class GeoJsonImpl : public GeoJson { +public: + GeoJsonImpl(std::shared_ptr logger, std::filesystem::path db) + : logger_(std::move(logger)), db_(std::move(db)) {} + + std::optional get_data(double lat, double lng, + std::string_view data) const override { + if (!db_.empty()) { + FILE* fh = fopen(db_.c_str(), "rb"); + if (fh) { + rapidjson::Reader reader; + char buffer[1024 * 1024]; + rapidjson::FileReadStream in(fh, buffer, sizeof(buffer)); + Handler handler(logger_.get(), lat, lng, data); + reader.Parse(in, handler); + fclose(fh); + return handler.data(); + } else { + logger_->warn("Unable to open %s for reading: %s", + db_.c_str(), strerror(errno)); + } + } + return std::nullopt; + } + +private: + class Handler : public rapidjson::BaseReaderHandler, + Handler> { + enum class Expect { + NONE, + FEATURES, + GEOMETRY, + PROPERTIES, + KEY_VALUE, + GEOMETRY_TYPE, + GEOMETRY_COORDINATES, + }; + + public: + Handler(Logger* logger, double lat, double lng, std::string_view key) + : logger_(logger), pt_(lng, lat), key_(key) {} + + bool StartObject() { + if (depth_ == std::numeric_limits::max()) + return false; + ++depth_; + + switch (expect_) { + case Expect::GEOMETRY: + geometry_ = true; + break; + case Expect::PROPERTIES: + properties_ = true; + break; + default: + break; + } + return Default(); + } + + bool Key(const char* str, rapidjson::SizeType len, bool /* copy */) { + expect_ = Expect::NONE; + + auto key = std::string_view(str, len); + if (depth_ == 1) { + if (key == "features") { + expect_ = Expect::FEATURES; + } + } else if (depth_ == 2) { + if (features_) { + if (key == "properties") { + expect_ = Expect::PROPERTIES; + } else if (key == "geometry") { + expect_ = Expect::GEOMETRY; + } + } + } else if (depth_ == 3) { + if (properties_) { + if (key == key_) { + expect_ = Expect::KEY_VALUE; + } + } else if (geometry_) { + if (key == "type") { + expect_ = Expect::GEOMETRY_TYPE; + } else if (key == "coordinates") { + expect_ = Expect::GEOMETRY_COORDINATES; + } + } + } + return true; + } + + bool Default() { + expect_ = Expect::NONE; + return true; + } + + bool StartArray() { + if (depth_ == 1) { + if (expect_ == Expect::FEATURES) { + features_ = true; + } + } else if (depth_ == 3) { + if (polygon_coordinates_) { + polygon_coordinate_ = true; + } else if (list_of_polygons_) { + polygon_coordinates_ = true; + } else if (expect_ == Expect::GEOMETRY_COORDINATES) { + list_of_polygons_ = true; + } + } + return Default(); + } + + bool Int(int value) { + return Double(value); + } + + bool Uint(unsigned value) { + return Double(value); + } + + bool Int64(int64_t value) { + return Double(value); + } + + bool Uint64(uint64_t value) { + return Double(value); + } + + bool Double(double value) { + if (depth_ == 3 && polygon_coordinate_) { + coord_.push_back(value); + } + return Default(); + } + + bool String(const char* data, rapidjson::SizeType len, bool /* copy */) { + std::string_view str(data, len); + if (depth_ == 3) { + if (expect_ == Expect::GEOMETRY_TYPE) { + geometry_type_ = str; + } else if (expect_ == Expect::KEY_VALUE) { + key_value_ = str; + } + } + return Default(); + } + + bool EndArray(rapidjson::SizeType /* count */) { + if (depth_ == 1) { + if (features_) { + features_ = false; + } + } else if (depth_ == 3) { + if (polygon_coordinate_) { + coords_.emplace_back(std::move(coord_)); + coord_.clear(); + polygon_coordinate_ = false; + } else if (polygon_coordinates_) { + polygons_.emplace_back(std::move(coords_)); + coords_.clear(); + polygon_coordinates_ = false; + } else if (list_of_polygons_) { + list_of_polygons_ = false; + } + } + return Default(); + } + + bool EndObject(rapidjson::SizeType /* members */) { + if (depth_ == 0) + return false; + --depth_; + if (depth_ == 2) { + if (geometry_) { + geometry_ = false; + } else if (properties_) { + properties_ = false; + } + } if (depth_ == 1) { + if (features_) { + if (check_if_done()) + return false; + + key_value_.reset(); + geometry_type_.reset(); + coord_.clear(); + coords_.clear(); + polygons_.clear(); + } + } + return Default(); + } + + std::optional data() const { + return data_; + } + + private: + bool check_if_done() { + if (geometry_type_.has_value() && geometry_type_.value() == "Polygon") { + if (polygons_.empty()) { + logger_->dbg("No polygons in Polygon"); + return true; + } + auto exterior = get_polygon(polygons_[0]); + if (exterior.empty()) + return false; + if (in_polygon(pt_, exterior)) { + bool in_hole = false; + for (size_t i = 1; i < polygons_.size(); ++i) { + auto hole = get_polygon(polygons_[i]); + if (hole.empty()) + return false; + if (in_polygon(pt_, hole)) { + in_hole = true; + break; + } + } + if (!in_hole) { + data_ = key_value_; + return true; + } + } + } + return false; + } + + std::vector get_polygon(std::vector> const& in) { + std::vector out; + for (auto const& pair : in) { + if (pair.size() != 2) { + logger_->dbg("Coordinate of size != 2"); + return {}; + } + out.emplace_back(pair[0], pair[1]); + } + if (out.empty()) { + logger_->dbg("Empty LineString"); + return {}; + } + if (out.size() < 2 || out.front() != out.back()) { + logger_->dbg("Polygon does not start and end in same point."); + return {}; + } + return out; + } + + Logger* const logger_; + Point const pt_; + std::string_view const key_; + std::optional data_; + uint32_t depth_{0}; + Expect expect_{Expect::NONE}; + bool features_{false}; + bool geometry_{false}; + bool properties_{false}; + bool list_of_polygons_{false}; + bool polygon_coordinates_{false}; + bool polygon_coordinate_{false}; + + std::optional key_value_; + std::optional geometry_type_; + std::vector>> polygons_; + std::vector> coords_; + std::vector coord_; + }; + + std::shared_ptr logger_; + std::filesystem::path db_; +}; + +} // namespace + +std::unique_ptr GeoJson::create(std::shared_ptr logger, + std::filesystem::path db) { + return std::make_unique(std::move(logger), std::move(db)); +} diff --git a/src/geo_json.hh b/src/geo_json.hh new file mode 100644 index 0000000..d5f4030 --- /dev/null +++ b/src/geo_json.hh @@ -0,0 +1,28 @@ +#ifndef GEO_JSON_HH +#define GEO_JSON_HH + +#include +#include +#include +#include +#include + +class Logger; + +class GeoJson { +public: + virtual ~GeoJson() = default; + + static std::unique_ptr create(std::shared_ptr logger, + std::filesystem::path db); + + virtual std::optional get_data(double lat, double lng, + std::string_view data) const = 0; + +protected: + GeoJson() = default; + GeoJson(GeoJson const&) = delete; + GeoJson& operator=(GeoJson const&) = delete; +}; + +#endif // GEO_JSON_HH diff --git a/src/hash_method.cc b/src/hash_method.cc new file mode 100644 index 0000000..1eee04e --- /dev/null +++ b/src/hash_method.cc @@ -0,0 +1,17 @@ +#include "common.hh" + +#include "hash_method.hh" + +std::string HashMethod::to_string(uint8_t const* data, size_t len) { + static const char kChar[] = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f' + }; + std::string ret; + ret.reserve(len * 2); + for (size_t i = 0; i < len; ++i) { + ret.push_back(kChar[data[i] >> 4]); + ret.push_back(kChar[data[i] & 0xf]); + } + return ret; +} diff --git a/src/hash_method.hh b/src/hash_method.hh new file mode 100644 index 0000000..5a61c23 --- /dev/null +++ b/src/hash_method.hh @@ -0,0 +1,23 @@ +#ifndef HASH_METHOD_HH +#define HASH_METHOD_HH + +#include +#include + +class HashMethod { +public: + virtual ~HashMethod() = default; + + static std::unique_ptr sha256(); + + virtual void update(void const* data, size_t count) = 0; + + virtual std::string finish() = 0; + +protected: + HashMethod() = default; + + std::string to_string(uint8_t const* data, size_t len); +}; + +#endif // HASH_METHOD_HH diff --git a/src/hash_method_openssl.cc b/src/hash_method_openssl.cc new file mode 100644 index 0000000..08884cb --- /dev/null +++ b/src/hash_method_openssl.cc @@ -0,0 +1,34 @@ +#include "common.hh" + +#include "hash_method.hh" + +#include + +namespace { + +class Sha256HashMethod : public HashMethod { +public: + Sha256HashMethod() { + SHA256_Init(&ctx_); + } + + void update(void const* data, size_t count) override { + SHA256_Update(&ctx_, data, count); + } + + std::string finish() override { + uint8_t out[SHA256_DIGEST_LENGTH]; + SHA256_Final(out, &ctx_); + SHA256_Init(&ctx_); + return to_string(out, sizeof(out)); + } + +private: + SHA256_CTX ctx_; +}; + +} // namespace + +std::unique_ptr HashMethod::sha256() { + return std::make_unique(); +} diff --git a/src/hasher.cc b/src/hasher.cc new file mode 100644 index 0000000..dd94ce4 --- /dev/null +++ b/src/hasher.cc @@ -0,0 +1,71 @@ +#include "common.hh" + +#include "hash_method.hh" +#include "hasher.hh" +#include "io.hh" +#include "logger.hh" +#include "task_runner.hh" + +#include +#include + +namespace { + +class HasherImpl : public Hasher { +public: + HasherImpl(std::shared_ptr logger, + std::shared_ptr runner, + size_t threads) + : logger_(std::move(logger)), runner_(std::move(runner)), + workers_(TaskRunner::create(threads)) { + } + + void hash(std::filesystem::path path, + std::function callback) override { + workers_->post(std::bind(&HasherImpl::do_hash, this, path, callback)); + } + +private: + void do_hash(std::filesystem::path path, + std::function callback) { + auto fd = io::open(path, io::open_flags::rdonly); + std::string result; + uint64_t size = 0; + if (fd) { + auto method = HashMethod::sha256(); + char buffer[1 * 1024 * 1024]; + while (true) { + auto got = io::read(fd.get(), buffer, sizeof(buffer)); + if (got < 0) { + logger_->warn("Error reading: %s: %s", path.c_str(), + strerror(errno)); + size = 0; + break; + } + if (got == 0) { + result = method->finish(); + break; + } + size += got; + method->update(buffer, got); + } + } else { + logger_->warn("Unable to open: %s: %s", path.c_str(), + strerror(errno)); + } + runner_->post(std::bind(callback, result, size)); + } + + std::shared_ptr logger_; + std::shared_ptr runner_; + std::unique_ptr workers_; +}; + +} // namespace + +std::unique_ptr Hasher::create(std::shared_ptr logger, + std::shared_ptr runner, + size_t threads) { + return std::make_unique(std::move(logger), std::move(runner), + threads); +} diff --git a/src/hasher.hh b/src/hasher.hh new file mode 100644 index 0000000..16b361f --- /dev/null +++ b/src/hasher.hh @@ -0,0 +1,29 @@ +#ifndef HASHER_HH +#define HASHER_HH + +#include +#include +#include + +class Logger; +class TaskRunner; + +class Hasher { +public: + virtual ~Hasher() = default; + + static std::unique_ptr create(std::shared_ptr logger, + std::shared_ptr runner, + size_t threads = 1); + + virtual void hash(std::filesystem::path path, + std::function callback) = 0; + +protected: + Hasher() = default; + Hasher(Hasher const&) = delete; + Hasher& operator=(Hasher const&) = delete; +}; + +#endif // HASHER_HH diff --git a/src/htmlutil.cc b/src/htmlutil.cc new file mode 100644 index 0000000..42abc4c --- /dev/null +++ b/src/htmlutil.cc @@ -0,0 +1,59 @@ +#include "common.hh" + +#include "htmlutil.hh" + +namespace html { + +namespace { + +constexpr const std::string_view kBodyChars = "&<>"; +constexpr const std::string_view kAttributeChars = "&<>\"'"; + +} // namespace + +std::string escape(std::string_view in, EscapeTarget target) { + std::string out; + escape(in, &out, target); + return out; +} + +void escape(std::string_view in, std::string* out, EscapeTarget target) { + std::string_view chars; + switch (target) { + case EscapeTarget::BODY: + chars = kBodyChars; + break; + case EscapeTarget::ATTRIBUTE: + chars = kAttributeChars; + break; + } + size_t last = 0; + while (true) { + auto next = in.find_first_of(chars, last); + if (next == std::string::npos) { + out->append(in, last); + break; + } + out->append(in, last, next - last); + switch (in[next]) { + case '&': + out->append("&"); + break; + case '<': + out->append("<"); + break; + case '>': + out->append(">"); + break; + case '"': + out->append("""); + break; + case '\'': + out->append("'"); + break; + } + last = next + 1; + } +} + +} // namespace html diff --git a/src/htmlutil.hh b/src/htmlutil.hh new file mode 100644 index 0000000..ad96956 --- /dev/null +++ b/src/htmlutil.hh @@ -0,0 +1,21 @@ +#ifndef HTMLUTIL_HH +#define HTMLUTIL_HH + +#include +#include + +namespace html { + +enum class EscapeTarget { + BODY, + ATTRIBUTE, +}; + +std::string escape(std::string_view in, + EscapeTarget target = EscapeTarget::BODY); +void escape(std::string_view in, std::string* out, + EscapeTarget target = EscapeTarget::BODY); + +} // namespace html + +#endif // HTMLUTIL_HH diff --git a/src/http_protocol.cc b/src/http_protocol.cc new file mode 100644 index 0000000..c0e40ad --- /dev/null +++ b/src/http_protocol.cc @@ -0,0 +1,916 @@ +#include "common.hh" + +#include "http_protocol.hh" + +#include +#include +#include +#include + +namespace { + +uint16_t number(std::string_view data, size_t start, size_t end) { + uint16_t ret = 0; + assert(start < end); + for (; start < end; ++start) { + ret *= 10; + ret += data[start] - '0'; + } + return ret; +} + +inline char lower_ascii(char c) { + return (c >= 'A' && c <= 'Z') ? (c - 'A' + 'a') : c; +} + +inline bool is_lws(char c) { + return c == ' ' || c == '\t'; +} + +inline bool is_char(char c) { + return !(c & 0x80); +} + +inline bool is_ctl(char c) { + return c < ' ' || c == 0x7f; +} + +inline bool is_separator(char c) { + return is_lws(c) || c == '(' || c == ')' || c == '<' || c == '>' || c == '@' + || c == ',' || c == ';' || c == ':' || c == '\\' || c == '\"' || c == '/' + || c == '[' || c == ']' || c == '?' || c == '=' || c == '{' || c == '}'; +} + +inline bool is_token(char c) { + return is_char(c) && !is_ctl(c) && !is_separator(c); +} + +void make_lowercase(std::string& data, size_t start, size_t end) { + for (size_t i = start; i <= end; ++i) { + char lower = lower_ascii(data[i]); + if (lower != data[i]) + data[i] = lower; + } +} + +bool allow_header_append(std::string_view name) { + // These headers doesn't handle being merged with ',' even if the standard + // say they must + return !(name == "set-cookie" || name == "set-cookie2"); +} + +enum ParseResult { + GOOD, + BAD, + INCOMPLETE, +}; + +class HeaderIteratorImpl : public HeaderIterator { +public: + HeaderIteratorImpl(std::string_view data, std::vector const* headers) + : data_(data), headers_(headers), iter_(headers_->begin()) { + } + + bool valid() const override { + return iter_ != headers_->end(); + } + + std::string_view name() const override { + return data_.substr(iter_[0], iter_[1] - iter_[0]); + } + + std::string value() const override { + std::string ret(data_.substr(iter_[2], iter_[3] - iter_[2])); + if (allow_header_append(name())) { + auto i = iter_ + 4; + while (i != headers_->end()) { + if (i[0] != i[1]) break; + ret.push_back(','); + ret.append(data_.substr(i[2], i[3] - i[2])); + i += 4; + } + } + return ret; + } + + void next() override { + if (iter_ != headers_->end()) { + while (true) { + iter_ += 4; + if (iter_ == headers_->end() || iter_[0] != iter_[1]) + break; + } + } + } + +private: + std::string_view const data_; + std::vector const* const headers_; + std::vector::const_iterator iter_; +}; + +class FilterHeaderIteratorImpl : public HeaderIteratorImpl { +public: + FilterHeaderIteratorImpl(std::string_view data, + std::vector const* headers, + std::string_view filter) + : HeaderIteratorImpl(data, headers), filter_(filter) { + check_filter(); + } + + void next() override { + HeaderIteratorImpl::next(); + check_filter(); + } + +private: + void check_filter() { + while (true) { + if (!valid() || name() == filter_) + return; + HeaderIteratorImpl::next(); + } + } + + std::string_view const filter_; +}; + +class HeaderTokenIteratorImpl : public HeaderTokenIterator { +public: + explicit HeaderTokenIteratorImpl(std::unique_ptr&& header) + : header_(std::move(header)), start_(0), middle_(0), end_(0) { + check_token(); + } + + bool valid() const override { + return header_->valid(); + } + + std::string token() const override { + return header_->value().substr(start_, middle_ - start_); + } + + void next() override { + start_ = end_; + check_token(); + } + +private: + static size_t skip_lws(std::string const& str, size_t pos) { + while (pos < str.size() && is_lws(str[pos])) ++pos; + return pos; + } + + static size_t skip_token(std::string const& str, size_t pos) { + assert(is_token(str[pos])); + ++pos; + while (pos < str.size() && is_token(str[pos])) ++pos; + return pos; + } + + static size_t skip_quoted(std::string const& str, size_t pos) { + assert(str[pos] == '"'); + ++pos; + while (pos < str.size()) { + if (str[pos] == '\\') { + pos += 2; + } else if (str[pos] == '\"') { + ++pos; + break; + } else { + ++pos; + } + } + return pos; + } + + void check_token() { + while (true) { + if (!header_->valid()) return; + auto const& value = header_->value(); + start_ = skip_lws(value, start_); + if (start_ >= value.size()) { + header_->next(); + start_ = 0; + continue; + } + if (!is_token(value[start_])) { + if (value[start_] != ';') { + ++start_; + while (start_ < value.size() + && !(is_lws(value[start_]) || value[start_] == ',' + || value[start_] == ';')) { + ++start_; + } + if (start_ < value.size() && value[start_] != ';') { + continue; + } + } + // This will cause us to loop again after paramters + // are read + middle_ = start_; + } else { + middle_ = skip_token(value, start_); + } + end_ = middle_; + while (true) { + end_ = skip_lws(value, end_); + if (end_ == value.size() || value[end_] != ';') break; + end_ = skip_lws(value, end_ + 1); + if (!is_token(value[end_])) { + while (end_ < value.size() && !is_separator(value[end_])) ++end_; + continue; + } + end_ = skip_token(value, end_); + end_ = skip_lws(value, end_); + if (end_ == value.size() || value[end_] != '=') break; + end_ = skip_lws(value, end_ + 1); + if (end_ < value.size() && value[end_] == '"') { + end_ = skip_quoted(value, end_); + } else { + if (!is_token(value[end_])) { + while (end_ < value.size() && !is_separator(value[end_])) ++end_; + continue; + } + end_ = skip_token(value, end_); + } + } + if (end_ < value.size() && value[end_] == ',') ++end_; + if (start_ < middle_) return; + start_ = end_; + } + } + + std::unique_ptr header_; + size_t start_; + size_t middle_; + size_t end_; +}; + +size_t find_newline(std::string_view data, size_t start, size_t* next) { + assert(start <= data.size()); + for (; start < data.size(); ++start) { + if (data[start] == '\r') { + if (start + 1 < data.size() && data[start + 1] == '\n') { + if (next) *next = start + 2; + } else { + if (next) *next = start + 1; + } + return start; + } else if (data[start] == '\n') { + if (next) *next = start + 1; + return start; + } + } + return std::string::npos; +} + +size_t find(std::string_view data, size_t start, char c, size_t end) { + assert(start <= end); + for (; start < end; ++start) { + if (data[start] == c) return start; + } + return std::string::npos; +} + +size_t skip_lws(std::string_view data, size_t start, size_t end) { + assert(start <= end); + while (start < end && is_lws(data[start])) ++start; + return start; +} + +size_t valid_number(std::string_view data, size_t start, size_t end) { + assert(start <= end); + if (start == end) + return std::string::npos; + if (data[start] == '0') { + return start + 1; + } + if (data[start] < '0' || data[start] > '9') + return std::string::npos; + for (++start; start < end; ++start) { + if (data[start] < '0' || data[start] > '9') + break; + } + return start; +} + +ParseResult parse_headers(std::string_view data, size_t* offset, + std::vector* headers) { + assert(*offset <= data.size()); + assert(headers->empty()); + while (true) { + auto start = *offset; + auto end = find_newline(data, start, offset); + if (end == std::string::npos) + return INCOMPLETE; + if (end == start) { + // The final newline can only be a alone '\r' if the one in front of + // it is also '\r', otherwise we expect a missing '\n' + if (data[start - 1] == '\n' && data[*offset - 1] == '\r') { + return INCOMPLETE; + } + break; + } + if (is_lws(data[start])) { + if (headers->empty()) + return BAD; + headers->push_back(start); + headers->push_back(start); + headers->push_back(start + 1); + headers->push_back(end); + } else { + auto colon = find(data, start, ':', end); + if (colon == std::string::npos) return BAD; + auto value_start = skip_lws(data, colon + 1, end); + while (colon > start && is_lws(data[colon - 1])) --colon; + headers->push_back(start); + headers->push_back(colon); + headers->push_back(value_start); + headers->push_back(end); + } + } + return GOOD; +} + +std::string make_lowercase_header_names(std::string_view data, + std::vector const& headers) { + std::string ret(data); + for (size_t i = 0; i < headers.size(); i += 4) { + make_lowercase(ret, headers[i], headers[i + 1]); + } + return ret; +} + +class AbstractHttp : public virtual HttpPackage { +public: + AbstractHttp(std::string data, bool good, size_t proto_start, + size_t proto_slash, size_t proto_dot, size_t proto_end, + std::vector headers, size_t content_start) + : data_(std::move(data)), good_(good), proto_start_(proto_start), + proto_slash_(proto_slash), proto_dot_(proto_dot), proto_end_(proto_end), + headers_(std::move(headers)), content_start_(content_start) { + } + + bool good() const override { + return good_; + } + + std::string_view proto() const override { + return std::string_view(data_).substr( + proto_start_, proto_slash_ - proto_start_); + } + + Version proto_version() const override { + Version ret; + ret.major = number(data_, proto_slash_ + 1, proto_dot_); + ret.minor = number(data_, proto_dot_ + 1, proto_end_); + return ret; + } + + std::unique_ptr header() const override { + return std::make_unique(data_, &headers_); + } + std::unique_ptr header( + std::string_view name) const override { + return std::make_unique(data_, &headers_, name); + } + + std::unique_ptr header_tokens(std::string_view name) + const override { + return std::make_unique(header(name)); + } + + size_t size() const override { + return content_start_; + } + +protected: + std::string const data_; + bool const good_; + size_t const proto_start_; + size_t const proto_slash_; + size_t const proto_dot_; + size_t const proto_end_; + std::vector const headers_; + size_t const content_start_; +}; + +class HttpResponseImpl : public HttpResponse, protected AbstractHttp { +public: + HttpResponseImpl(std::string data, bool good, + size_t proto_start, size_t proto_slash, + size_t proto_dot, size_t proto_end, + size_t status_start, + size_t status_end, size_t status_msg_start, + size_t status_msg_end, + std::vector headers, + size_t content_start) + : AbstractHttp(std::move(data), good, proto_start, proto_slash, proto_dot, + proto_end, std::move(headers), content_start), + status_start_(status_start), status_end_(status_end), + status_msg_start_(status_msg_start), status_msg_end_(status_msg_end) { + } + + uint16_t status_code() const override { + return number(data_, status_start_, status_end_); + } + + std::string_view status_message() const override { + return std::string_view(data_).substr(status_msg_start_, + status_msg_end_ - status_msg_start_); + } + + static std::unique_ptr parse(std::string_view data) { + size_t content_start = 0; + size_t status_msg_end = find_newline(data, 0, &content_start); + if (status_msg_end == std::string::npos) + return nullptr; + size_t proto_start = 0; + size_t proto_slash = find(data, 0, '/', status_msg_end); + if (proto_slash == std::string::npos) + return make_bad_http_response(); + size_t proto_dot = valid_number(data, proto_slash + 1, status_msg_end); + if (proto_dot == std::string::npos || data[proto_dot] != '.') + return make_bad_http_response(); + size_t proto_end = valid_number(data, proto_dot + 1, status_msg_end); + if (proto_end == std::string::npos || !is_lws(data[proto_end])) + return make_bad_http_response(); + size_t status_start = skip_lws(data, proto_end + 1, status_msg_end); + size_t status_end = valid_number(data, status_start, status_msg_end); + if (status_end == std::string::npos) + return make_bad_http_response(); + size_t status_msg_start; + if (is_lws(data[status_end])) { + status_msg_start = skip_lws(data, status_end + 1, status_msg_end); + } else { + status_msg_start = status_end; + if (status_msg_start != status_msg_end) + return make_bad_http_response(); + } + + std::vector headers; + switch (parse_headers(data, &content_start, &headers)) { + case GOOD: + return std::make_unique( + make_lowercase_header_names(data, headers), true, + proto_start, proto_slash, proto_dot, proto_end, + status_start, status_end, status_msg_start, status_msg_end, + std::move(headers), content_start); + case BAD: + return make_bad_http_response(); + case INCOMPLETE: + return nullptr; + } + assert(false); + return nullptr; + } + +private: + static std::unique_ptr make_bad_http_response() { + return std::make_unique(std::string(), false, + 0, 0, 0, 0, 0, 0, 0, 0, + std::vector(), 0); + } + + size_t const status_start_; + size_t const status_end_; + size_t const status_msg_start_; + size_t const status_msg_end_; +}; + +class HttpRequestImpl : public HttpRequest, protected AbstractHttp { +public: + HttpRequestImpl(std::string data, bool good, + size_t method_end, size_t url_start, size_t url_end, + size_t proto_start, size_t proto_slash, + size_t proto_dot, size_t proto_end, + std::vector headers, + size_t content_start) + : AbstractHttp(std::move(data), good, proto_start, proto_slash, + proto_dot, proto_end, std::move(headers), content_start), + method_end_(method_end), url_start_(url_start), url_end_(url_end) { + } + + std::string_view method() const override { + return std::string_view(data_).substr(0, method_end_); + } + + std::string_view url() const override { + return std::string_view(data_).substr(url_start_, url_end_ - url_start_); + } + + static std::unique_ptr parse(std::string_view data) { + size_t content_start = 0; + size_t proto_end = find_newline(data, 0, &content_start); + if (proto_end == std::string::npos) + return nullptr; + size_t method_end = 0; + while (method_end < proto_end && !is_lws(data[method_end])) { + ++method_end; + } + if (method_end == 0 || method_end == proto_end) + return make_bad_request(); + size_t url_start = skip_lws(data, method_end + 1, proto_end); + size_t url_end = url_start; + while (url_end < proto_end && !is_lws(data[url_end])) + ++url_end; + if (url_end == url_start || url_end == proto_end) + return make_bad_request(); + size_t proto_start = skip_lws(data, url_end + 1, proto_end); + size_t proto_slash = find(data, proto_start, '/', proto_end); + if (proto_slash == std::string::npos) + return make_bad_request(); + size_t proto_dot = valid_number(data, proto_slash + 1, proto_end); + if (proto_dot == std::string::npos || data[proto_dot] != '.') + return make_bad_request(); + auto tmp = valid_number(data, proto_dot + 1, proto_end); + if (tmp != proto_end) + return make_bad_request(); + + std::vector headers; + switch (parse_headers(data, &content_start, &headers)) { + case GOOD: + return std::make_unique( + make_lowercase_header_names(data, headers), true, + method_end, url_start, url_end, + proto_start, proto_slash, + proto_dot, proto_end, + std::move(headers), content_start); + case BAD: + return make_bad_request(); + case INCOMPLETE: + return nullptr; + } + assert(false); + return nullptr; + } + +private: + static std::unique_ptr make_bad_request() { + return std::make_unique("", false, 0, 0, 0, 0, 0, 0, 0, + std::vector(), 0); + } + + size_t const method_end_; + size_t const url_start_; + size_t const url_end_; +}; + +class AbstractHttpBuilder { +public: + void add_header(std::string name, std::string value) { + headers_.emplace_back(std::move(name), std::move(value)); + } + + bool build(Buffer* dst) const { + for (auto const& pair : headers_) { + if (pair.first.empty()) { + if (!append(dst, " ")) + return false; + } else { + if (!append(dst, pair.first) || + !append(dst, ": ")) + return false; + } + if (!append(dst, pair.second) || + !append(dst, "\r\n")) + return false; + } + return append(dst, "\r\n"); + } + + size_t size() const { + size_t ret = 0; + for (auto const& pair : headers_) { + if (pair.first.empty()) { + ++ret; // ' ' + } else { + ret += pair.first.size() + 2; // ": " + } + ret += pair.second.size() + 2; // \r\” + } + return ret + 2; // \r\n + } + +protected: + bool append(Buffer* dst, std::string_view str) const { + return Buffer::write(dst, str.data(), str.size()) == str.size(); + } + + std::vector> headers_; +}; + +class HttpRequestBuilderImpl : public HttpRequestBuilder, AbstractHttpBuilder { +public: + HttpRequestBuilderImpl(std::string method, std::string url, std::string proto, + Version version) + : method_(std::move(method)), url_(std::move(url)), + proto_(std::move(proto)), version_(version) {} + + void add_header(std::string name, std::string value) override { + AbstractHttpBuilder::add_header(std::move(name), std::move(value)); + } + + bool build(Buffer* dst) const override { + if (!append(dst, method_) || + !append(dst, " ") || + !append(dst, url_) || + !append(dst, " ") || + !append(dst, proto_) || + !append(dst, "/")) + return false; + char tmp[10]; + auto len = snprintf(tmp, sizeof(tmp), "%u", + static_cast(version_.major)); + if (!append(dst, std::string_view(tmp, len))) + return false; + if (!append(dst, ".")) + return false; + len = snprintf(tmp, sizeof(tmp), "%u", + static_cast(version_.minor)); + if (!append(dst, std::string_view(tmp, len))) + return false; + if (!append(dst, "\r\n")) + return false; + return AbstractHttpBuilder::build(dst); + } + + size_t size() const override { + size_t ret = 0; + ret += method_.size() + 1 + url_.size() + 1 + proto_.size() + 1; + char tmp[10]; + auto len = snprintf(tmp, sizeof(tmp), "%u", + static_cast(version_.major)); + ret += len; + ++ret; // '.' + len = snprintf(tmp, sizeof(tmp), "%u", + static_cast(version_.minor)); + ret += len; + ret += 2; // \r\n + return ret + AbstractHttpBuilder::size(); + } + +private: + std::string const method_; + std::string const url_; + std::string const proto_; + Version const version_; +}; + +class HttpResponseBuilderImpl : public HttpResponseBuilder, + AbstractHttpBuilder { +public: + HttpResponseBuilderImpl(std::string proto, Version version, + uint16_t status_code, std::string status) + : proto_(std::move(proto)), version_(version), status_code_(status_code), + status_(std::move(status)) { + } + + void add_header(std::string name, std::string value) override { + AbstractHttpBuilder::add_header(std::move(name), std::move(value)); + } + + bool build(Buffer* dst) const override { + if (!append(dst, proto_) || + !append(dst, "/")) + return false; + char tmp[10]; + auto len = snprintf(tmp, sizeof(tmp), "%u", + static_cast(version_.major)); + if (!append(dst, std::string_view(tmp, len)) || + !append(dst, ".")) + return false; + len = snprintf(tmp, sizeof(tmp), "%u", + static_cast(version_.minor)); + if (!append(dst, std::string_view(tmp, len)) || + !append(dst, " ")) + return false; + len = snprintf(tmp, sizeof(tmp), "%u", + static_cast(status_code_)); + if (!append(dst, std::string_view(tmp, len)) || + !append(dst, " ") || + !append(dst, status_) || + !append(dst, "\r\n")) + return false; + return AbstractHttpBuilder::build(dst); + } + + size_t size() const override { + size_t ret = 0; + ret += proto_.size(); + ++ret; // "/" + char tmp[10]; + auto len = snprintf(tmp, sizeof(tmp), "%u", + static_cast(version_.major)); + ret += len; + ++ret; // "." + len = snprintf(tmp, sizeof(tmp), "%u", + static_cast(version_.minor)); + ret += len; + ++ret; // " " + len = snprintf(tmp, sizeof(tmp), "%u", + static_cast(status_code_)); + ret += len; + ++ret; // " " + ret += status_.length(); + ret += 2; // \r\n + return ret + AbstractHttpBuilder::size(); + } + +private: + std::string const proto_; + Version const version_; + uint16_t const status_code_; + std::string const status_; +}; + +class CgiResponseBuilderImpl : public CgiResponseBuilder, + AbstractHttpBuilder { +public: + explicit CgiResponseBuilderImpl(uint16_t status_code) { + AbstractHttpBuilder::add_header("Status", std::to_string(status_code)); + } + + void add_header(std::string name, std::string value) override { + AbstractHttpBuilder::add_header(std::move(name), std::move(value)); + } + + bool build(Buffer* dst) const override { + return AbstractHttpBuilder::build(dst); + } + + size_t size() const override { + return AbstractHttpBuilder::size(); + } +}; + +} // namespace + +// static +std::unique_ptr HttpResponse::parse(RoBuffer* buffer) { + size_t want = 1024; + size_t last_avail = 0; + while (true) { + size_t avail; + auto* rptr = buffer->rbuf(want, avail); + if (avail == last_avail) + return nullptr; + last_avail = avail; + auto resp = HttpResponseImpl::parse(std::string_view(rptr, avail)); + if (resp) { + buffer->rcommit(resp->size()); + return resp; + } + want = avail + 1024; + } +} + +std::string HttpPackage::first_header(std::string_view name) const { + static std::string empty_str; + auto iter = header(name); + if (iter->valid()) { + return iter->value(); + } + return empty_str; +} + +// static +std::unique_ptr HttpRequest::parse(RoBuffer* buffer) { + size_t want = 1024; + size_t last_avail = 0; + while (true) { + size_t avail; + auto* rptr = buffer->rbuf(want, avail); + if (avail == last_avail) + return nullptr; + last_avail = avail; + auto req = HttpRequestImpl::parse(std::string_view(rptr, avail)); + if (req) { + buffer->rcommit(req->size()); + return req; + } + want = avail + 1024; + } +} + +// static +std::unique_ptr HttpRequestBuilder::create( + std::string method, + std::string url, + std::string proto, + Version version) { + return std::make_unique(std::move(method), + std::move(url), + std::move(proto), version); +} + +// static +std::unique_ptr HttpResponseBuilder::create( + std::string proto, + Version version, + uint16_t status_code, + std::string status) { + return std::make_unique(std::move(proto), version, + status_code, + std::move(status)); +} + +// static +std::unique_ptr CgiResponseBuilder::create( + uint16_t status_code) { + return std::make_unique(status_code); +} + +std::string_view http_standard_message(uint16_t code) { + switch (code) { + case 100: + return "Continue"; + case 101: + return "Switching Protocols"; + case 200: + return "OK"; + case 201: + return "Created"; + case 202: + return "Accepted"; + case 203: + return "Non-Authorative Information"; + case 204: + return "No Content"; + case 205: + return "Reset Content"; + case 206: + return "Partial Content"; + case 300: + return "Multiple Choices"; + case 301: + return "Moved Permanently"; + case 302: + return "Found"; + case 303: + return "See Other"; + case 304: + return "Not Modified"; + case 305: + return "Use Proxy"; + case 307: + return "Temporary Redirect"; + case 400: + return "Bad Request"; + case 401: + return "Unauthorized"; + case 402: + return "Payment Required"; + case 403: + return "Forbidden"; + case 404: + return "Not Found"; + case 405: + return "Method Not Allowed"; + case 406: + return "Not Acceptable"; + case 407: + return "Proxy Authentication Required"; + case 408: + return "Request Timeout"; + case 409: + return "Conflict"; + case 410: + return "Gone"; + case 411: + return "Length Required"; + case 412: + return "Precondition Failed"; + case 413: + return "Request Entity Too Large"; + case 414: + return "Request-URI Too Long"; + case 415: + return "Unsupported Media Type"; + case 416: + return "Requested Range Not Satisfiable"; + case 417: + return "Expectation Failed"; + case 500: + return "Internal Server Error"; + case 501: + return "Not Implemented"; + case 502: + return "Bad Gateway"; + case 503: + return "Service Unavailable"; + case 504: + return "Gateway Timeout"; + case 505: + return "HTTP Version Not Supported"; + } + return ""; +} + +std::string http_date(time_t in) { + char tmp[50]; + auto len = strftime(tmp, sizeof(tmp), "%a, %d %b %Y %H:%M:%S GMT", + gmtime(&in)); + return std::string(tmp, len); +} diff --git a/src/http_protocol.hh b/src/http_protocol.hh new file mode 100644 index 0000000..e558b27 --- /dev/null +++ b/src/http_protocol.hh @@ -0,0 +1,170 @@ +#ifndef HTTP_PROTOCOL_HH +#define HTTP_PROTOCOL_HH + +#include +#include +#include +#include + +#include "buffer.hh" + +// glibc defines these even tho it shouldn't if you ask posix +#ifdef major +# undef major +#endif +#ifdef minor +# undef minor +#endif + +struct Version { + uint16_t major; + uint16_t minor; + + Version() + : major(0), minor(0) { + } + Version(uint16_t major, uint16_t minor) + : major(major), minor(minor) { + } +}; + +class HeaderIterator { +public: + virtual ~HeaderIterator() = default; + + virtual bool valid() const = 0; + virtual std::string_view name() const = 0; + virtual std::string value() const = 0; + virtual void next() = 0; + +protected: + HeaderIterator() = default; + HeaderIterator(HeaderIterator const&) = delete; + HeaderIterator& operator=(HeaderIterator const&) = delete; +}; + +class HeaderTokenIterator { +public: + virtual ~HeaderTokenIterator() = default; + + virtual bool valid() const = 0; + virtual std::string token() const = 0; + virtual void next() = 0; + +protected: + HeaderTokenIterator() = default; + HeaderTokenIterator(HeaderTokenIterator const&) = delete; + HeaderTokenIterator& operator=(HeaderTokenIterator const&) = delete; +}; + +class HttpPackage { +public: + virtual ~HttpPackage() = default; + + virtual bool good() const = 0; + + virtual std::string_view proto() const = 0; + virtual Version proto_version() const = 0; + virtual std::unique_ptr header() const = 0; + virtual std::unique_ptr header( + std::string_view name) const = 0; + std::string first_header(std::string_view name) const; + virtual std::unique_ptr header_tokens( + std::string_view name) const = 0; + virtual size_t size() const = 0; + +protected: + HttpPackage() = default; + HttpPackage(HttpPackage const&) = delete; + HttpPackage& operator=(HttpPackage const&) = delete; +}; + +class HttpResponse : public virtual HttpPackage { +public: + virtual ~HttpResponse() = default; + + static std::unique_ptr parse(RoBuffer* buffer); + + virtual uint16_t status_code() const = 0; + virtual std::string_view status_message() const = 0; + +protected: + HttpResponse() = default; + HttpResponse(HttpResponse const&) = delete; + HttpResponse& operator=(HttpResponse const&) = delete; +}; + +class HttpRequest : public virtual HttpPackage { +public: + virtual ~HttpRequest() = default; + + static std::unique_ptr parse(RoBuffer* buffer); + + virtual std::string_view method() const = 0; + virtual std::string_view url() const = 0; + +protected: + HttpRequest() = default; + HttpRequest(HttpRequest const&) = delete; + HttpRequest& operator=(HttpRequest const&) = delete; +}; + +class HttpResponseBuilder { +public: + virtual ~HttpResponseBuilder() = default; + + static std::unique_ptr create( + std::string proto, Version version, + uint16_t status_code, std::string status_message); + + virtual void add_header(std::string name, std::string value) = 0; + + virtual bool build(Buffer* dst) const = 0; + virtual size_t size() const = 0; + +protected: + HttpResponseBuilder() = default; + HttpResponseBuilder(HttpResponseBuilder const&) = delete; + HttpResponseBuilder& operator=(HttpResponseBuilder const&) = delete; +}; + +class HttpRequestBuilder { +public: + virtual ~HttpRequestBuilder() = default; + + static std::unique_ptr create( + std::string method, std::string url, std::string proto, Version version); + + virtual void add_header(std::string name, std::string value) = 0; + + virtual bool build(Buffer* dst) const = 0; + virtual size_t size() const = 0; + +protected: + HttpRequestBuilder() = default; + HttpRequestBuilder(HttpRequestBuilder const&) = delete; + HttpRequestBuilder& operator=(HttpRequestBuilder const&) = delete; +}; + +class CgiResponseBuilder { +public: + virtual ~CgiResponseBuilder() = default; + + static std::unique_ptr create(uint16_t status_code); + + virtual void add_header(std::string name, std::string value) = 0; + + virtual bool build(Buffer* dst) const = 0; + virtual size_t size() const = 0; + +protected: + CgiResponseBuilder() = default; + CgiResponseBuilder(CgiResponseBuilder const&) = delete; + CgiResponseBuilder& operator=(CgiResponseBuilder const&) = delete; +}; + +std::string_view http_standard_message(uint16_t code); + +std::string http_date(time_t in); + +#endif // HTTP_PROTOCOL_HH diff --git a/src/image.cc b/src/image.cc new file mode 100644 index 0000000..111568c --- /dev/null +++ b/src/image.cc @@ -0,0 +1,344 @@ +#include "common.hh" + +#include "buffer.hh" +#include "image.hh" +#include "mime_types.hh" + +#include +#include + +#if HAVE_JPEG +#include +#include +#endif + +#if HAVE_EXIF +#include +#include +#include +#include +#include +#endif + +namespace { + +bool is_jpeg(const unsigned char* data, size_t size); + +class ThumbnailImpl : public Image::Thumbnail { +public: + ThumbnailImpl(std::string mime_type, uint64_t size) + : mime_type_(std::move(mime_type)), size_(size) {} + + std::string_view mime_type() const override { + return mime_type_; + } + + uint64_t size() const override { + return size_; + } + +private: + std::string mime_type_; + uint64_t size_; +}; + +class ImageImpl : public Image { +public: + ImageImpl(uint64_t width, uint64_t height) + : width_(width), height_(height) {} + + uint64_t width() const override { + return width_; + } + + uint64_t height() const override { + return height_; + } + + Location location() const override { + return location_; + } + + Rotation rotation() const override { + return rotation_; + } + + Date date() const override { + return date_; + } + + Thumbnail* thumbnail() const override { + return thumbnail_.get(); + } + + void set_location(Location location) { + location_ = location; + } + + void set_rotation(Rotation rotation) { + rotation_ = rotation; + } + + void set_date(Date date) { + date_ = date; + } + + void set_thumbnail(std::string mime_type, uint64_t size) { + thumbnail_ = std::make_unique(std::move(mime_type), size); + } + +private: + uint64_t width_; + uint64_t height_; + Location location_; + Rotation rotation_{Rotation::UNKNOWN}; + Date date_; + std::unique_ptr thumbnail_; +}; + +#if HAVE_EXIF +double make_double(ExifRational rat) { + return static_cast(rat.numerator) / rat.denominator; +} + +void load_exif(std::filesystem::path const& path, ImageImpl* img) { + ExifData *data = exif_data_new_from_file(path.c_str()); + if (!data) + return; + + auto byte_order = exif_data_get_byte_order(data); + + auto* entry = exif_content_get_entry(data->ifd[EXIF_IFD_0], + EXIF_TAG_ORIENTATION); + if (entry && entry->format == EXIF_FORMAT_SHORT && entry->components == 1) { + auto orientation = exif_get_short(entry->data, byte_order); + switch (orientation) { + case 1: + img->set_rotation(Rotation::NONE); + break; + case 2: + img->set_rotation(Rotation::MIRRORED); + break; + case 3: + img->set_rotation(Rotation::ROTATED_180); + break; + case 4: + img->set_rotation(Rotation::ROTATED_180_MIRRORED); + break; + case 5: + img->set_rotation(Rotation::ROTATED_90); + break; + case 6: + img->set_rotation(Rotation::ROTATED_90_MIRRORED); + break; + case 7: + img->set_rotation(Rotation::ROTATED_270); + break; + case 8: + img->set_rotation(Rotation::ROTATED_270_MIRRORED); + break; + default: + break; + } + } + entry = exif_content_get_entry(data->ifd[EXIF_IFD_0], EXIF_TAG_DATE_TIME); + if (entry && entry->format == EXIF_FORMAT_ASCII) { + auto date = Date::from_format("%Y:%m:%d %H:%M:%S", + reinterpret_cast(entry->data)); + if (!date.empty()) + img->set_date(date); + } + auto* lat = exif_content_get_entry( + data->ifd[EXIF_IFD_GPS], + static_cast(EXIF_TAG_GPS_LATITUDE)); + auto* lat_ref = exif_content_get_entry( + data->ifd[EXIF_IFD_GPS], + static_cast(EXIF_TAG_GPS_LATITUDE_REF)); + auto* lng = exif_content_get_entry( + data->ifd[EXIF_IFD_GPS], + static_cast(EXIF_TAG_GPS_LONGITUDE)); + auto* lng_ref = exif_content_get_entry( + data->ifd[EXIF_IFD_GPS], + static_cast(EXIF_TAG_GPS_LONGITUDE_REF)); + if (lat && lat->format == EXIF_FORMAT_RATIONAL && lat->components == 3 && + lat_ref && lat_ref->format == EXIF_FORMAT_ASCII && + lng && lng->format == EXIF_FORMAT_RATIONAL && lng->components == 3 && + lng_ref && lng_ref->format == EXIF_FORMAT_ASCII) { + auto step = exif_format_get_size(EXIF_FORMAT_RATIONAL); + auto lat_double = + make_double(exif_get_rational(lat->data + step * 0, byte_order)) + + make_double(exif_get_rational(lat->data + step * 1, + byte_order)) / 60.0 + + make_double(exif_get_rational(lat->data + step * 2, + byte_order)) / 3600.0; + if (lat_ref->data[0] != 'N') + lat_double = -lat_double; + auto lng_double = + make_double(exif_get_rational(lng->data + step * 0, byte_order)) + + make_double(exif_get_rational(lng->data + step * 1, + byte_order)) / 60.0 + + make_double(exif_get_rational(lng->data + step * 2, + byte_order)) / 3600.0; + if (lng_ref->data[0] != 'E') + lng_double = -lng_double; + + img->set_location(Location(lat_double, lng_double)); + } + + if (data->data) { + if (is_jpeg(data->data, data->size)) { + img->set_thumbnail("image/jpeg", data->size); + } + } + + exif_data_free(data); +} + +class ExifThumbnailReader : public ThumbnailReader { +public: + ExifThumbnailReader() + : loader_(exif_loader_new()), data_(nullptr) {} + + ~ExifThumbnailReader() { + if (loader_) + exif_loader_unref(loader_); + if (data_) + exif_data_free(data_); + } + + Return drain(RoBuffer* buf, size_t* bytes) override { + if (bytes) *bytes = 0; + if (loader_) { + while (true) { + size_t avail; + auto* ptr = buf->rbuf(1, avail); + if (avail == 0) + return Return::NEED_MORE; + avail = std::min( + avail, + static_cast(std::numeric_limits::max())); + if (exif_loader_write(loader_, reinterpret_cast( + const_cast(ptr)), avail)) { + buf->rcommit(avail); + if (bytes) *bytes += avail; + // Loop to see if there is any more data avail. + } else { + // No data read or no hope there will be any. + break; + } + } + data_ = exif_loader_get_data(loader_); + exif_loader_unref(loader_); + loader_ = nullptr; + } + if (data_ && data_->data) + return Return::DONE; + return Return::ERR; + } + + std::string_view data() const override { + return data_ && data_->data + ? std::string_view(reinterpret_cast(data_->data), + data_->size) + : std::string_view(); + } + +private: + ExifLoader* loader_; + ExifData* data_; +}; +#else // HAVE_EXIF +class FailingThumbnailReader : public ThumbnailReader { +public: + FailingThumbnailReader() = default; + + Return drain(RoBuffer*, size_t* bytes) override { + if (bytes) *bytes = 0; + return Return::ERR; + } + + std::string_view data() const override { + return std::string_view(); + } +}; +#endif // HAVE_EXIT + +#if HAVE_JPEG +struct my_jpeg_error { + struct jpeg_error_mgr base; + jmp_buf jmp_buffer; +}; + +void jpeg_error_exit(j_common_ptr cinfo) { + auto myerr = reinterpret_cast(cinfo->err); + longjmp(myerr->jmp_buffer, 1); +} + +bool is_jpeg(const unsigned char* data, size_t size) { + struct jpeg_decompress_struct info; + struct my_jpeg_error err; + info.err = jpeg_std_error(&err.base); + err.base.error_exit = jpeg_error_exit; + if (setjmp(err.jmp_buffer)) { + jpeg_destroy_decompress(&info); + return false; + } + jpeg_create_decompress(&info); + jpeg_mem_src(&info, data, size); + jpeg_read_header(&info, TRUE); + jpeg_destroy_decompress(&info); + return true; +} + +std::unique_ptr load_jpeg(std::filesystem::path const& path) { + struct jpeg_decompress_struct info; + struct my_jpeg_error err; + FILE* fh = fopen(path.c_str(), "rb"); + if (!fh) + return nullptr; + info.err = jpeg_std_error(&err.base); + err.base.error_exit = jpeg_error_exit; + if (setjmp(err.jmp_buffer)) { + jpeg_destroy_decompress(&info); + fclose(fh); + return nullptr; + } + jpeg_create_decompress(&info); + jpeg_stdio_src(&info, fh); + jpeg_read_header(&info, TRUE); + + auto img = std::make_unique(info.image_width, info.image_height); + + jpeg_destroy_decompress(&info); + fclose(fh); + +#if HAVE_EXIF + load_exif(path, img.get()); +#endif + + return img; +} +#endif // HAVE_JPEG + +} // namespace + +std::unique_ptr Image::load(std::filesystem::path const& path) { + if (!path.has_extension()) + return nullptr; + auto mime_type = mime_types::from_extension( + std::string(path.extension()).substr(1)); +#if HAVE_JPEG + if (mime_type == "image/jpeg") { + return load_jpeg(path); + } +#endif // HAVE_JPEG + return nullptr; +} + +std::unique_ptr ThumbnailReader::create() { +#if HAVE_EXIF + return std::make_unique(); +#else + return std::make_unique(); +#endif +} diff --git a/src/image.hh b/src/image.hh new file mode 100644 index 0000000..b9644b7 --- /dev/null +++ b/src/image.hh @@ -0,0 +1,67 @@ +#ifndef IMAGE_HH +#define IMAGE_HH + +#include "date.hh" +#include "location.hh" +#include "rotation.hh" + +#include +#include + +class RoBuffer; + +class Image { +public: + virtual ~Image() = default; + + class Thumbnail { + public: + virtual ~Thumbnail() = default; + + virtual std::string_view mime_type() const = 0; + virtual uint64_t size() const = 0; + + protected: + Thumbnail() = default; + }; + + static std::unique_ptr load(std::filesystem::path const& path); + + virtual uint64_t width() const = 0; + virtual uint64_t height() const = 0; + virtual Location location() const = 0; + virtual Rotation rotation() const = 0; + virtual Date date() const = 0; + + virtual Thumbnail* thumbnail() const = 0; + +protected: + Image() = default; + Image(Image const&) = delete; + Image& operator=(Image const&) = delete; +}; + +class ThumbnailReader { +public: + virtual ~ThumbnailReader() = default; + + static std::unique_ptr create(); + + enum class Return { + NEED_MORE, // Call drain() again if you want data. + DONE, // Data is available, no need to call drain() again. + ERR, // Error, no data will ever be available. + }; + + virtual Return drain(RoBuffer* in, size_t* bytes = nullptr) = 0; + + // Returns empty until drain() returns Return::DONE. + virtual std::string_view data() const = 0; + +protected: + ThumbnailReader() = default; + ThumbnailReader(ThumbnailReader const&) = delete; + ThumbnailReader& operator=(ThumbnailReader const&) = delete; +}; + +#endif // IMAGE_HH diff --git a/src/inet.cc b/src/inet.cc new file mode 100644 index 0000000..1700a14 --- /dev/null +++ b/src/inet.cc @@ -0,0 +1,112 @@ +#include "common.hh" + +#include "inet.hh" +#include "io.hh" +#include "logger.hh" + +#include +#include +#include +#include +#include +#include +#include + +namespace inet { + +unique_fd accept(Logger* logger, int fd, bool make_nonblock) { +#if HAVE_ACCEPT4 + unique_fd ret(accept4(fd, nullptr, nullptr, + make_nonblock ? SOCK_NONBLOCK : 0)); + if (!ret) + logger->warn("accept: %s", strerror(errno)); + return ret; +#else + unique_fd ret(::accept(fd, nullptr, nullptr)); + if (ret) { + if (make_nonblock && !io::make_nonblocking(ret.get())) { + logger->warn("make nonblock failed: %s", strerror(errno)); + ret.reset(); + } + } else { + logger->warn("accept: %s", strerror(errno)); + } + return ret; +#endif +} + +bool bind_and_listen(Logger* logger, + std::string const& addr, + std::string const& port, + std::vector* out) { + out->clear(); + struct addrinfo hints = {}; + struct addrinfo* ret; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG | AI_PASSIVE; + auto err = getaddrinfo(addr.empty() ? nullptr : addr.c_str(), + port.c_str(), &hints, &ret); + if (err) { + logger->warn("%s:%s: getaddrinfo: %s", + addr.c_str(), port.c_str(), gai_strerror(err)); + return false; + } + + for (auto* rp = ret; rp != nullptr; rp = rp->ai_next) { + unique_fd fd(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)); + if (!fd) + continue; + + if (bind(fd.get(), rp->ai_addr, rp->ai_addrlen)) { + logger->warn("%s:%s: bind: %s", + addr.c_str(), port.c_str(), strerror(errno)); + continue; + } + + if (listen(fd.get(), 4096)) { + logger->warn("%s:%s: listen: %s", + addr.c_str(), port.c_str(), strerror(errno)); + continue; + } + + out->emplace_back(std::move(fd)); + } + freeaddrinfo(ret); + return !out->empty(); +} + +} // namespace inet + +namespace unix { + +unique_fd bind_and_listen(Logger* logger, + std::string_view path) { + unique_fd fd(socket(AF_UNIX, SOCK_STREAM, 0)); + if (fd) { + sockaddr_un addr; + if (path.size() >= sizeof(addr.sun_path)) { + logger->warn("%.*s: Too long path", + static_cast(path.size()), path.data()); + return unique_fd(); + } + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + std::copy_n(path.data(), path.size(), addr.sun_path); + addr.sun_path[path.size()] = '\0'; + if (bind(fd.get(), reinterpret_cast(&addr), + sizeof(addr))) { + logger->warn("%.*s: bind: %s", + static_cast(path.size()), path.data(), strerror(errno)); + return unique_fd(); + } + if (listen(fd.get(), 4096)) { + logger->warn("%.*s: listen: %s", + static_cast(path.size()), path.data(), strerror(errno)); + return unique_fd(); + } + } + return fd; +} + +} // namespace unix diff --git a/src/inet.hh b/src/inet.hh new file mode 100644 index 0000000..1575a17 --- /dev/null +++ b/src/inet.hh @@ -0,0 +1,29 @@ +#ifndef INET_HH +#define INET_HH + +#include "unique_fd.hh" + +#include +#include +#include + +class Logger; + +namespace inet { + +unique_fd accept(Logger* logger, int fd, bool make_nonblock = false); + +bool bind_and_listen(Logger* logger, + std::string const& addr, + std::string const& port, + std::vector* out); + +} // namespace inet + +namespace unix { + +unique_fd bind_and_listen(Logger* logger, std::string_view path); + +} // namespace unix + +#endif // INET_HH diff --git a/src/io.cc b/src/io.cc new file mode 100644 index 0000000..04e6dfa --- /dev/null +++ b/src/io.cc @@ -0,0 +1,234 @@ +#include "common.hh" + +#include "buffer.hh" +#include "io.hh" +#include "unique_fd.hh" + +#include +#include +#include +#include + +namespace { + +bool mkdirs_internal(int root, std::filesystem::path const& path, mode_t mode, + unique_fd* out) { + if (mkdirat(root, path.c_str(), mode) == 0) { + if (out) + *out = io::openat(root, path, io::open_flags::path | + io::open_flags::directory); + return true; + } + if (errno == EEXIST) { + struct stat buf; + if (fstatat(root, path.c_str(), &buf, 0)) + return false; + if (!S_ISDIR(buf.st_mode)) + return false; + if (out) + *out = io::openat(root, path, + io::open_flags::path | io::open_flags::directory); + return true; + } + if (errno != ENOENT) + return false; + + auto parent = path.parent_path(); + if (parent == path || !parent.has_relative_path()) + return false; + + unique_fd parent_fd; + if (!mkdirs_internal(root, parent, mode, &parent_fd)) + return false; + auto name = path.filename(); + if (mkdirat(parent_fd.get(), name.c_str(), mode)) + return false; + if (out) + *out = io::openat(parent_fd.get(), name, + io::open_flags::path | io::open_flags::directory); + return true; +} + +} // namespace + +namespace io { + +bool read_all(int fd, void* data, size_t size) { + auto* d = reinterpret_cast(data); + size_t offset = 0; + while (offset < size) { + auto got = read(fd, d + offset, size - offset); + if (got < 0) { + if (errno == EINTR) + continue; + return false; + } + if (got == 0) + return false; + offset += got; + } + return true; +} + +bool write_all(int fd, void const* data, size_t size) { + auto* d = reinterpret_cast(data); + size_t offset = 0; + while (offset < size) { + auto wrote = write(fd, d + offset, size - offset); + if (wrote < 0) { + if (errno == EINTR) + continue; + return false; + } + if (wrote == 0) + return false; + offset += wrote; + } + return true; +} + +Return fill(int fd, Buffer* out, size_t buf_request_size, + size_t* bytes) { + if (bytes) + *bytes = 0; + while (true) { + size_t avail; + auto* ptr = out->wbuf(buf_request_size, avail); + if (avail == 0) + return Return::OK; + auto got = read(fd, ptr, avail); + if (got < 0) { + if (errno == EINTR) + continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) + return Return::OK; + return Return::ERR; + } + if (got == 0) + return Return::CLOSED; + if (bytes) + *bytes += got; + out->wcommit(got); + // No point in trying again, will most likely get EAGAIN or EWOULDBLOCK. + // Might also be because the connection got closed, but we are in no hurry + // to find out so worth the not extra syscall at every read. + if (static_cast(got) < avail) + return Return::OK; + } +} + +bool drain(RoBuffer* in, int fd, size_t* bytes) { + if (bytes) + *bytes = 0; + while (true) { + size_t avail; + auto* ptr = in->rbuf(0, avail); + if (avail == 0) + return true; + auto wrote = write(fd, ptr, avail); + if (wrote < 0) { + if (errno == EINTR) + continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) + return true; + return false; + } + if (wrote == 0) + return false; + if (bytes) + *bytes += wrote; + in->rcommit(wrote); + // No point in trying again, will most likely get EAGAIN or EWOULDBLOCK + if (static_cast(wrote) < avail) + return true; + } +} + +bool mkdirs(std::filesystem::path const& path, mode_t mode) { + return mkdirs(AT_FDCWD, path, mode); +} + +bool mkdirs(int fd, std::filesystem::path const& path, mode_t mode) { + return mkdirs_internal(fd, path, mode, nullptr); +} + +bool make_nonblocking(int fd) { + int status = fcntl(fd, F_GETFL, 0); + if (status == -1) + return false; + if ((status & O_NONBLOCK) == 0) { + if (fcntl(fd, F_SETFL, status | O_NONBLOCK)) + return false; + } + return true; +} + +unique_fd open(std::filesystem::path const& path, open_flags flags, + std::filesystem::perms perms) { + return openat(AT_FDCWD, path, flags, perms); +} + +unique_fd openat(int fd, std::filesystem::path const& path, open_flags flags, + std::filesystem::perms perms) { + int posix_flags = O_RDONLY; + if ((flags & open_flags::wronly) == open_flags::wronly) + posix_flags |= O_WRONLY; + if ((flags & open_flags::rdwr) == open_flags::rdwr) + posix_flags |= O_RDWR; + if ((flags & open_flags::create) == open_flags::create) + posix_flags |= O_CREAT; + if ((flags & open_flags::excl) == open_flags::excl) + posix_flags |= O_EXCL; + if ((flags & open_flags::trunc) == open_flags::trunc) + posix_flags |= O_TRUNC; + if ((flags & open_flags::append) == open_flags::append) + posix_flags |= O_APPEND; + if ((flags & open_flags::directory) == open_flags::directory) + posix_flags |= O_DIRECTORY; + if ((flags & open_flags::path) == open_flags::path) + posix_flags |= O_PATH; + return unique_fd(::openat(fd, path.c_str(), posix_flags, + static_cast(perms))); +} + +bool close(int fd) { + return ::close(fd) == 0; +} + +ssize_t pread(int fd, void *buf, size_t count, off_t offset) { + return ::pread(fd, buf, count, offset); +} + +ssize_t read(int fd, void *buf, size_t count) { + return ::read(fd, buf, count); +} + +ssize_t write(int fd, const void *buf, size_t count) { + return ::write(fd, buf, count); +} + +bool access(std::filesystem::path const& path, access_mode mode) { + int posix_mode; + if (mode == access_mode::exists) { + posix_mode = F_OK; + } else { + posix_mode = 0; + if ((mode & access_mode::read) == access_mode::read) + posix_mode |= R_OK; + if ((mode & access_mode::write) == access_mode::write) + posix_mode |= W_OK; + if ((mode & access_mode::exec) == access_mode::exec) + posix_mode |= X_OK; + } + return ::access(path.c_str(), posix_mode) == 0; +} + +bool unlinkat(int fd, std::filesystem::path const& path) { + return ::unlinkat(fd, path.c_str(), 0) == 0; +} + +bool fallocate(int fd, off_t offset, off_t len) { + return posix_fallocate(fd, offset, len) == 0; +} + +} // namespace io diff --git a/src/io.hh b/src/io.hh new file mode 100644 index 0000000..f13b51b --- /dev/null +++ b/src/io.hh @@ -0,0 +1,113 @@ +#ifndef IO_HH +#define IO_HH + +#include "unique_fd.hh" + +#include +#include + +class Buffer; +class RoBuffer; + +namespace io { + +bool make_nonblocking(int fd); + +bool mkdirs(std::filesystem::path const& path, mode_t mode); +bool mkdirs(int fd, std::filesystem::path const& path, mode_t mode); + +bool read_all(int fd, void* data, size_t size); +bool write_all(int fd, void const* data, size_t size); +bool seek_all(int fd, size_t bytes); + +enum class Return { + OK, + ERR, + CLOSED, +}; + +Return fill(int fd, Buffer* out, size_t buf_request_size = 1, + size_t* bytes = nullptr); +// Returns false in case of error. EAGAIN or EWOULDBLOCK are not errors. +bool drain(RoBuffer* in, int fd, size_t* bytes = nullptr); + +enum class open_flags : unsigned { + rdonly = 00, + wronly = 01, + rdwr = 02, + create = 0100, + excl = 0200, + trunc = 01000, + append = 02000, + directory = 0200000, + path = 010000000, +}; + +unique_fd open(std::filesystem::path const& path, open_flags flags, + std::filesystem::perms perms = std::filesystem::perms::none); +unique_fd openat(int fd, std::filesystem::path const& path, open_flags flags, + std::filesystem::perms perms = std::filesystem::perms::none); + +bool close(int fd); + +ssize_t pread(int fd, void *buf, size_t count, off_t offset); +ssize_t read(int fd, void *buf, size_t count); +ssize_t write(int fd, const void *buf, size_t count); + +enum class access_mode : unsigned { + exists = 00, + read = 01, + write = 02, + exec = 04, +}; + +bool access(std::filesystem::path const& path, access_mode mode); +bool unlinkat(int fd, std::filesystem::path const& path); +bool fallocate(int fd, off_t offset, off_t len); + +constexpr open_flags operator&(open_flags a, open_flags b) noexcept { + using utype = typename std::underlying_type::type; + return static_cast(static_cast(a) & static_cast(b)); +} + +constexpr open_flags operator|(open_flags a, open_flags b) noexcept { + using utype = typename std::underlying_type::type; + return static_cast(static_cast(a) | static_cast(b)); +} + +constexpr open_flags operator^(open_flags a, open_flags b) noexcept { + using utype = typename std::underlying_type::type; + return static_cast(static_cast(a) ^ static_cast(b)); +} + +constexpr open_flags operator~(open_flags a) noexcept { + using utype = typename std::underlying_type::type; + return static_cast(~static_cast(a)); +} + +constexpr access_mode operator&(access_mode a, access_mode b) noexcept { + using utype = typename std::underlying_type::type; + return static_cast( + static_cast(a) & static_cast(b)); +} + +constexpr access_mode operator|(access_mode a, access_mode b) noexcept { + using utype = typename std::underlying_type::type; + return static_cast( + static_cast(a) | static_cast(b)); +} + +constexpr access_mode operator^(access_mode a, access_mode b) noexcept { + using utype = typename std::underlying_type::type; + return static_cast( + static_cast(a) ^ static_cast(b)); +} + +constexpr access_mode operator~(access_mode a) noexcept { + using utype = typename std::underlying_type::type; + return static_cast(~static_cast(a)); +} + +} // namespace io + +#endif // IO_HH diff --git a/src/jsutil.cc b/src/jsutil.cc new file mode 100644 index 0000000..ca74b48 --- /dev/null +++ b/src/jsutil.cc @@ -0,0 +1,72 @@ +#include "common.hh" + +#include "jsutil.hh" + +namespace js { + +namespace { + +constexpr const char kSingleQuoteChars[] = "'\0\\\n\r\v\t\b\f"; +constexpr const char kDoubleQuoteChars[] = "\"\0\\\n\r\v\t\b\f"; + +} // namespace + +std::string quote(std::string_view in, QuoteChar quote_char) { + std::string ret; + quote(in, ret, quote_char); + return ret; +} + +void quote(std::string_view in, std::string& out, QuoteChar quote_char) { + out.reserve(out.size() + 2 + in.size()); + std::string_view chars; + switch (quote_char) { + case QuoteChar::SINGLE: + chars = std::string_view(kSingleQuoteChars, sizeof(kSingleQuoteChars) - 1); + break; + case QuoteChar::DOUBLE: + chars = std::string_view(kDoubleQuoteChars, sizeof(kDoubleQuoteChars) - 1); + break; + } + out.push_back(chars.front()); + size_t last = 0; + while (true) { + auto next = in.find_first_of(chars, last); + if (next == std::string::npos) { + out.append(in, last); + break; + } + out.append(in, last, next - last); + out.push_back('\\'); + switch (in[next]) { + case '\0': + out.push_back('0'); + break; + case '\n': + out.push_back('n'); + break; + case '\r': + out.push_back('r'); + break; + case '\v': + out.push_back('v'); + break; + case '\t': + out.push_back('t'); + break; + case '\b': + out.push_back('b'); + break; + case '\f': + out.push_back('f'); + break; + default: + out.push_back(in[next]); + break; + } + last = next + 1; + } + out.push_back(chars.front()); +} + +} // namespace js diff --git a/src/jsutil.hh b/src/jsutil.hh new file mode 100644 index 0000000..ff9582e --- /dev/null +++ b/src/jsutil.hh @@ -0,0 +1,22 @@ +#ifndef JSUTIL_HH +#define JSUTIL_HH + +#include +#include + +namespace js { + +enum class QuoteChar { + SINGLE, + DOUBLE, +}; + + +std::string quote(std::string_view in, + QuoteChar quote_char = QuoteChar::DOUBLE); +void quote(std::string_view in, std::string& out, + QuoteChar quote_char = QuoteChar::DOUBLE); + +} // namespace js + +#endif // JSUTIL_HH diff --git a/src/location.hh b/src/location.hh new file mode 100644 index 0000000..ce4984e --- /dev/null +++ b/src/location.hh @@ -0,0 +1,23 @@ +#ifndef LOCATION_HH +#define LOCATION_HH + +#include +#include + +struct Location { + double lat; + double lng; + + Location(double lat, double lng) + : lat(lat), lng(lng) {} + + Location() + : lat(std::numeric_limits::quiet_NaN()), + lng(std::numeric_limits::quiet_NaN()) {} + + bool empty() const { + return std::isnan(lat) || std::isnan(lng); + } +}; + +#endif // LOCATION_HH diff --git a/src/logger.hh b/src/logger.hh new file mode 100644 index 0000000..47bbedd --- /dev/null +++ b/src/logger.hh @@ -0,0 +1,57 @@ +#ifndef LOGGER_HH +#define LOGGER_HH + +#include +#include +#include + +class Logger { +public: + virtual ~Logger() = default; + + static std::unique_ptr create_stdio(); + // Can return nullptr, will write reason to fallback. + static std::unique_ptr create_file(std::filesystem::path const& path, + Logger* fallback); + static std::unique_ptr create_syslog(std::string const& prgname); + static std::unique_ptr create_null(); + + virtual void err(char const* format, ...) +#if HAVE_ATTRIBUTE_FORMAT + __attribute__((format(printf, 2, 3))) // this takes up 1 +#endif // HAVE_ATTRIBUTE_FORMAT + = 0; + + virtual void warn(char const* format, ...) +#if HAVE_ATTRIBUTE_FORMAT + __attribute__((format(printf, 2, 3))) +#endif // HAVE_ATTRIBUTE_FORMAT + = 0; + + virtual void info(char const* format, ...) +#if HAVE_ATTRIBUTE_FORMAT + __attribute__((format(printf, 2, 3))) +#endif // HAVE_ATTRIBUTE_FORMAT + = 0; + +#ifdef NDEBUG + void dbg(char const* format, ...) +#if HAVE_ATTRIBUTE_FORMAT + __attribute__((format(printf, 2, 3))) +#endif // HAVE_ATTRIBUTE_FORMAT + {} +#else + virtual void dbg(char const* format, ...) +#if HAVE_ATTRIBUTE_FORMAT + __attribute__((format(printf, 2, 3))) +#endif // HAVE_ATTRIBUTE_FORMAT + = 0; +#endif + +protected: + Logger() = default; + Logger(Logger const&) = delete; + Logger& operator=(Logger const&) = delete; +}; + +#endif // LOGGER_HH diff --git a/src/logger_base.cc b/src/logger_base.cc new file mode 100644 index 0000000..51f13b9 --- /dev/null +++ b/src/logger_base.cc @@ -0,0 +1,69 @@ +#include "common.hh" + +#include "logger_base.hh" + +#include +#include +#include + +void LoggerBase::err(char const* format, ...) { + va_list args; + va_start(args, format); + char* tmp = nullptr; + auto len = vasprintf(&tmp, format, args); + va_end(args); + if (len == -1) { + assert(false); + return; + } + while (len > 0 && tmp[len - 1] == '\n') --len; + msg(Level::ERR, std::string_view(tmp, len)); + free(tmp); +} + +void LoggerBase::warn(char const* format, ...) { + va_list args; + va_start(args, format); + char* tmp = nullptr; + auto len = vasprintf(&tmp, format, args); + va_end(args); + if (len == -1) { + assert(false); + return; + } + while (len > 0 && tmp[len - 1] == '\n') --len; + msg(Level::WARN, std::string_view(tmp, len)); + free(tmp); +} + +void LoggerBase::info(char const* format, ...) { + va_list args; + va_start(args, format); + char* tmp = nullptr; + auto len = vasprintf(&tmp, format, args); + va_end(args); + if (len == -1) { + assert(false); + return; + } + while (len > 0 && tmp[len - 1] == '\n') --len; + msg(Level::INFO, std::string_view(tmp, len)); + free(tmp); +} + +#ifndef NDEBUG +void LoggerBase::dbg(char const* format, ...) { + va_list args; + va_start(args, format); + char* tmp = nullptr; + auto len = vasprintf(&tmp, format, args); + va_end(args); + if (len == -1) { + assert(false); + return; + } + while (len > 0 && tmp[len - 1] == '\n') --len; + msg(Level::DBG, std::string_view(tmp, len)); + free(tmp); +} +#endif diff --git a/src/logger_base.hh b/src/logger_base.hh new file mode 100644 index 0000000..feed8ef --- /dev/null +++ b/src/logger_base.hh @@ -0,0 +1,25 @@ +#ifndef LOGGER_BASE_HH +#define LOGGER_BASE_HH + +#include "logger.hh" + +#include + +class LoggerBase : public Logger { +public: + void err(char const* format, ...) override; + void warn(char const* format, ...) override; + void info(char const* format, ...) override; +#ifndef NDEBUG + void dbg(char const* format, ...) override; +#endif + +protected: + enum class Level { + ERR, WARN, INFO, DBG + }; + + virtual void msg(Level level, std::string_view str) = 0; +}; + +#endif // LOGGER_BASE_HH diff --git a/src/logger_file.cc b/src/logger_file.cc new file mode 100644 index 0000000..ad11398 --- /dev/null +++ b/src/logger_file.cc @@ -0,0 +1,52 @@ +#include "common.hh" + +#include "logger_base.hh" + +#include +#include + +namespace { + +class LoggerFile : public LoggerBase { +public: + explicit LoggerFile(std::filesystem::path const& path) + : out_(path, std::ios::out | std::ios::app) {} + + bool good() const { + return out_.good(); + } + +protected: + void msg(Level lvl, std::string_view msg) override { + std::lock_guard lock(mutex_); + switch (lvl) { + case Level::ERR: + out_ << "Error: " << msg << std::endl; + break; + case Level::WARN: + out_ << "Warning: " << msg << std::endl; + break; + case Level::INFO: + out_ << msg << std::endl; + break; + case Level::DBG: + out_ << "Debug: " << msg << std::endl; + break; + } + } + +private: + std::mutex mutex_; + std::fstream out_; +}; + +} // namespace + +std::unique_ptr Logger::create_file(std::filesystem::path const& path, + Logger* fallback) { + auto logger = std::make_unique(path); + if (logger->good()) + return logger; + fallback->warn("Unable to open %s for appending.", path.c_str()); + return nullptr; +} diff --git a/src/logger_null.cc b/src/logger_null.cc new file mode 100644 index 0000000..a49db7b --- /dev/null +++ b/src/logger_null.cc @@ -0,0 +1,20 @@ +#include "common.hh" + +#include "logger_base.hh" + +namespace { + +class LoggerNull : public LoggerBase { +public: + LoggerNull() = default; + +protected: + void msg(Level, std::string_view) override { + } +}; + +} // namespace + +std::unique_ptr Logger::create_null() { + return std::make_unique(); +} diff --git a/src/logger_stdio.cc b/src/logger_stdio.cc new file mode 100644 index 0000000..17490a3 --- /dev/null +++ b/src/logger_stdio.cc @@ -0,0 +1,41 @@ +#include "common.hh" + +#include "logger_base.hh" + +#include +#include + +namespace { + +class LoggerStdio : public LoggerBase { +public: + LoggerStdio() = default; + +protected: + void msg(Level lvl, std::string_view msg) override { + std::lock_guard lock(mutex_); + switch (lvl) { + case Level::ERR: + std::cerr << "Error: " << msg << std::endl; + break; + case Level::WARN: + std::cout << "Warning: " << msg << std::endl; + break; + case Level::INFO: + std::cout << msg << std::endl; + break; + case Level::DBG: + std::cout << "Debug: " << msg << std::endl; + break; + } + } + +private: + std::mutex mutex_; +}; + +} // namespace + +std::unique_ptr Logger::create_stdio() { + return std::make_unique(); +} diff --git a/src/logger_syslog.cc b/src/logger_syslog.cc new file mode 100644 index 0000000..684405c --- /dev/null +++ b/src/logger_syslog.cc @@ -0,0 +1,45 @@ +#include "common.hh" + +#include "logger_base.hh" + +#include +#include + +namespace { + +class LoggerSyslog : public LoggerBase { +public: + explicit LoggerSyslog(std::string const& prgname) { + openlog(prgname.c_str(), LOG_PID, LOG_DAEMON); + } + + ~LoggerSyslog() override { + closelog(); + } + +protected: + void msg(Level lvl, std::string_view msg) override { + int prio; + switch (lvl) { + case Level::ERR: + prio = LOG_ERR; + break; + case Level::WARN: + prio = LOG_WARNING; + break; + case Level::INFO: + prio = LOG_INFO; + break; + case Level::DBG: + prio = LOG_DEBUG; + break; + } + syslog(prio, "%.*s", static_cast(msg.length()), msg.data()); + } +}; + +} // namespace + +std::unique_ptr Logger::create_syslog(std::string const& prgname) { + return std::make_unique(prgname); +} diff --git a/src/looper.hh b/src/looper.hh new file mode 100644 index 0000000..3286cb1 --- /dev/null +++ b/src/looper.hh @@ -0,0 +1,38 @@ +#ifndef LOOPER_HH +#define LOOPER_HH + +#include +#include + +class Logger; + +class Looper { +public: + constexpr static uint8_t EVENT_READ = 1; + constexpr static uint8_t EVENT_WRITE = 2; + constexpr static uint8_t EVENT_ERROR = 4; + + virtual ~Looper() = default; + + static std::unique_ptr create(); + + virtual void add(int fd, uint8_t events, + std::function callback) = 0; + virtual void update(int fd, uint8_t events) = 0; + virtual void remove(int fd) = 0; + + // Returned id is never 0 + virtual uint32_t schedule(double delay, + std::function callback) = 0; + virtual void cancel(uint32_t id) = 0; + + virtual bool run(Logger* logger) = 0; + virtual void quit() = 0; + +protected: + Looper() = default; + Looper(Looper const&) = delete; + Looper& operator=(Looper const&) = delete; +}; + +#endif // LOOPER_HH diff --git a/src/looper_poll.cc b/src/looper_poll.cc new file mode 100644 index 0000000..b08a686 --- /dev/null +++ b/src/looper_poll.cc @@ -0,0 +1,223 @@ +#include "common.hh" + +#include "looper.hh" +#include "logger.hh" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +class LooperPoll : public Looper { +public: + LooperPoll() = default; + + void add(int fd, uint8_t events, + std::function callback) override { + if (fd < 0) + return; + auto ret = entry_.emplace(fd, Entry(events)); + if (!ret.second) { + assert(ret.first->second.delete_); + ret.first->second.delete_ = false; + ret.first->second.events_ = events; + } + ret.first->second.callback_ = std::move(callback); + } + + void update(int fd, uint8_t events) override { + if (fd < 0) + return; + auto it = entry_.find(fd); + if (it == entry_.end() || it->second.delete_) { + assert(false); + return; + } + it->second.events_ = events; + } + + void remove(int fd) override { + if (fd < 0) + return; + auto it = entry_.find(fd); + if (it == entry_.end()) + return; + it->second.delete_ = true; + } + + bool run(Logger* logger) override { + while (!quit_) { + int timeout; + if (scheduled_.empty()) { + timeout = -1; + } else { + auto now = std::chrono::steady_clock::now(); + while (true) { + if (now < scheduled_.front().target_) { + auto delay = std::chrono::duration_cast( + scheduled_.front().target_ - now); + if (delay.count() <= std::numeric_limits::max()) + timeout = delay.count(); + else + timeout = std::numeric_limits::max(); + break; + } + auto id = scheduled_.front().id_; + auto callback = std::move(scheduled_.front().callback_); + scheduled_.pop_front(); + callback(id); + if (scheduled_.empty()) { + timeout = -1; + break; + } + } + // Scheduled callbacks might call quit(). + if (quit_) + break; + } + std::vector pollfd; + pollfd.reserve(entry_.size()); + auto it = entry_.begin(); + while (it != entry_.end()) { + if (it->second.delete_) { + it = entry_.erase(it); + } else { + struct pollfd tmp; + tmp.fd = it->first; + tmp.events = events_looper2poll(it->second.events_); + pollfd.push_back(std::move(tmp)); + ++it; + } + } + int active = poll(pollfd.data(), pollfd.size(), timeout); + if (active < 0) { + if (errno == EINTR) + continue; + logger->err("Poll failed: %s", strerror(errno)); + return false; + } + for (auto it2 = pollfd.begin(); active; ++it2) { + if (it2->revents == 0) + continue; + --active; + auto events = events_poll2looper(it2->revents); + if (events) { + it = entry_.find(it2->fd); + if (!it->second.delete_) { + events &= (it->second.events_ | EVENT_ERROR); + if (events) { + it->second.callback_(events); + } + } + } + } + } + // Reset quit_ so run() can be called again + quit_ = false; + return true; + } + + void quit() override { + quit_ = true; + } + + uint32_t schedule(double delay, + std::function callback) override { + assert(delay >= 0.0); + uint32_t id = next_schedule_id(); + auto target = std::chrono::steady_clock::now() + + std::chrono::duration_cast( + std::chrono::duration(delay)); + auto insert = scheduled_.end(); + while (insert != scheduled_.begin()) { + auto prev = insert - 1; + if (prev->target_ < target) + break; + insert = prev; + } + scheduled_.emplace(insert, std::move(callback), id, target); + return id; + } + + void cancel(uint32_t id) override { + for (auto it = scheduled_.begin(); it != scheduled_.end(); ++it) { + if (it->id_ == id) { + scheduled_.erase(it); + break; + } + } + } + +private: + struct Entry { + uint8_t events_; + std::function callback_; + bool delete_; + + explicit Entry(uint8_t events) + : events_(events), delete_(false) {} + }; + + struct Scheduled { + std::function callback_; + uint32_t id_; + std::chrono::steady_clock::time_point target_; + + Scheduled(std::function callback, uint32_t id, + std::chrono::steady_clock::time_point target) + : callback_(std::move(callback)), id_(id), target_(target) {} + }; + + static short events_looper2poll(uint8_t events) { + short ret = 0; + if (events & EVENT_READ) + ret |= POLLIN | POLLPRI; + if (events & EVENT_WRITE) + ret |= POLLOUT; + return ret; + } + + static uint8_t events_poll2looper(short events) { + uint8_t ret = 0; + if (events & (POLLIN | POLLPRI | POLLHUP)) + ret |= EVENT_READ; + if (events & POLLOUT) + ret |= EVENT_WRITE; + if (events & (POLLERR | POLLNVAL)) + ret |= EVENT_ERROR; + return ret; + } + + uint32_t next_schedule_id() { + while (true) { + uint32_t ret = next_schedule_id_++; + if (ret) { + bool found = false; + for (auto const& scheduled : scheduled_) { + if (scheduled.id_ == ret) { + found = true; + break; + } + } + if (!found) + return ret; + } + } + } + + bool quit_; + std::unordered_map entry_; + uint32_t next_schedule_id_{1}; + std::deque scheduled_; +}; + +} // namespace + +std::unique_ptr Looper::create() { + return std::make_unique(); +} diff --git a/src/mime_types.cc b/src/mime_types.cc new file mode 100644 index 0000000..1332f5a --- /dev/null +++ b/src/mime_types.cc @@ -0,0 +1,29 @@ +#include "common.hh" + +#include "mime_types.hh" + +#include + +namespace mime_types { + +namespace { + +std::unordered_map kExtensionMap({ + { "css", "text/css" }, + { "jpeg", "image/jpeg" }, + { "jpg", "image/jpeg" }, + { "js", "text/javascript" }, + { "png", "image/png" }, + { "webp", "image/webp" }, +}); + +} // namespace + +std::string_view from_extension(std::string_view ext) { + auto it = kExtensionMap.find(ext); + if (it == kExtensionMap.end()) + return std::string_view(); + return it->second; +} + +} // namespace mime_types diff --git a/src/mime_types.hh b/src/mime_types.hh new file mode 100644 index 0000000..c68a7f9 --- /dev/null +++ b/src/mime_types.hh @@ -0,0 +1,12 @@ +#ifndef MIME_TYPES_HH +#define MIME_TYPES_HH + +#include + +namespace mime_types { + +std::string_view from_extension(std::string_view ext); + +} // namespace mime_types + +#endif // MIME_TYPES_HH diff --git a/src/observer_list.hh b/src/observer_list.hh new file mode 100644 index 0000000..86cdc03 --- /dev/null +++ b/src/observer_list.hh @@ -0,0 +1,148 @@ +#ifndef OBSERVER_LIST_HH +#define OBSERVER_LIST_HH + +#include "common.hh" + +#include +#include + +template +class ObserverList { +public: + class iterator { + public: + iterator() + : list_(nullptr), index_(0), end_(0) {} + + ~iterator() { + if (list_) + list_->release(); + } + + iterator(iterator const& it) + : list_(it.list_), index_(it.index_), end_(it.end_) { + if (list_) { + list_->aquire(); + while (index_ < end_ && !list_->observers_[index_]) + ++index_; + } + } + + iterator& operator=(iterator const& it) { + if (list_ != it.list_) { + if (list_) + list_->release(); + list_ = it.list_; + if (list_) + list_->aquire(); + } + index_ = it.index_; + end_ = it.end_; + return *this; + } + + explicit operator bool() { + return index_ < end_; + } + + T& operator*() { + return *list_->observers_[index_]; + } + + T* operator->() { + return list_->observers_[index_]; + } + + iterator operator++(int) { + iterator ret(*this); + ++(*this); + return ret; + } + + iterator& operator++() { + if (index_ < end_) { + do { + ++index_; + } while (index_ < end_ && !list_->observers_[index_]); + } + return *this; + } + + private: + friend class ObserverList; + + iterator(ObserverList* list, size_t index, size_t end) + : list_(list), index_(index), end_(end) { + list_->aquire(); + while (index_ < end_ && !list_->observers_[index_]) + ++index_; + } + + ObserverList* list_; + size_t index_; + size_t end_; + }; + + ObserverList() = default; + ~ObserverList() { + assert(active_ == 0); + } + + bool empty() const { + return observers_.empty(); + } + + void add(T* observer) { + assert(std::find(observers_.begin(), observers_.end(), observer) + == observers_.end()); + observers_.push_back(observer); + } + + void remove(T* observer) { + auto it = std::find(observers_.begin(), observers_.end(), observer); + if (it != observers_.end()) { + if (active_) { + *it = nullptr; + ++deleted_; + } else { + observers_.erase(it); + } + } else { + assert(false); + } + } + + iterator notify() { + return iterator(this, 0, observers_.size()); + } + +private: + void aquire() { + ++active_; + } + + void release() { + assert(active_ > 0); + --active_; + if (active_ == 0 && deleted_) + cleanup(); + } + + void cleanup() { + size_t i = observers_.size(); + while (deleted_ && i > 0) { + --i; + if (!observers_[i]) { + --deleted_; + observers_.erase(observers_.begin() + i); + } + } + assert(deleted_ == 0); + } + + std::vector observers_; + unsigned active_{0}; + unsigned deleted_{0}; +}; + +#endif // OBSERVER_LIST_HH diff --git a/src/pathutil.cc b/src/pathutil.cc new file mode 100644 index 0000000..bb2b8e8 --- /dev/null +++ b/src/pathutil.cc @@ -0,0 +1,44 @@ +#include "common.hh" + +#include "pathutil.hh" +#include "strutil.hh" + +#include + +namespace path { + +std::string cleanup(std::string_view path) { + auto trimmed_path = str::trim(path); + bool trailing = !trimmed_path.empty() && trimmed_path.back() == '/'; + auto parts = str::split(trimmed_path, '/'); + auto it = parts.begin(); + while (it != parts.end()) { + if (it->empty() || *it == ".") { + if (it + 1 == parts.end()) + trailing = true; + it = parts.erase(it); + } else if (*it == "..") { + if (it + 1 == parts.end()) + trailing = true; + if (it > parts.begin()) { + it = parts.erase(it - 1, it); + } else { + it = parts.erase(it); + } + } else { + ++it; + } + } + if (parts.empty()) + return "/"; + std::string ret; + for (auto const& part : parts) { + ret.push_back('/'); + ret.append(part); + } + if (trailing) + ret.push_back('/'); + return ret; +} + +} // namespace path diff --git a/src/pathutil.hh b/src/pathutil.hh new file mode 100644 index 0000000..af2f6fb --- /dev/null +++ b/src/pathutil.hh @@ -0,0 +1,13 @@ +#ifndef PATHUTIL_HH +#define PATHUTIL_HH + +#include +#include + +namespace path { + +std::string cleanup(std::string_view path); + +} // namespace path + +#endif // PATHUTIL_HH diff --git a/src/ro_buffer.cc b/src/ro_buffer.cc new file mode 100644 index 0000000..ee6208e --- /dev/null +++ b/src/ro_buffer.cc @@ -0,0 +1,29 @@ +#include "common.hh" + +#include "ro_buffer.hh" + +#include + +size_t RoBuffer::read(RoBuffer* buf, void* data, size_t len) { + assert(buf); + assert(data); + if (len == 0) + return 0; + auto* d = reinterpret_cast(data); + size_t got = 0; + while (true) { + size_t avail; + auto want = len - got; + auto* ptr = buf->rbuf(want, avail); + if (avail == 0) + return got; + if (avail >= want) { + std::copy_n(ptr, want, d + got); + buf->rcommit(want); + return len; + } + std::copy_n(ptr, avail, d + got); + buf->rcommit(avail); + got += avail; + } +} diff --git a/src/ro_buffer.hh b/src/ro_buffer.hh new file mode 100644 index 0000000..a538f78 --- /dev/null +++ b/src/ro_buffer.hh @@ -0,0 +1,23 @@ +#ifndef RO_BUFFER_HH +#define RO_BUFFER_HH + +#include + +class RoBuffer { +public: + virtual ~RoBuffer() = default; + + virtual bool empty() const = 0; + + virtual char const* rbuf(size_t want, size_t& avail) = 0; + virtual void rcommit(size_t bytes) = 0; + + static size_t read(RoBuffer* buf, void* data, size_t len); + +protected: + RoBuffer() = default; + RoBuffer(RoBuffer const&) = delete; + RoBuffer& operator=(RoBuffer const&) = delete; +}; + +#endif // RO_BUFFER_HH diff --git a/src/rotation.hh b/src/rotation.hh new file mode 100644 index 0000000..2646b29 --- /dev/null +++ b/src/rotation.hh @@ -0,0 +1,16 @@ +#ifndef ROTATION_HH +#define ROTATION_HH + +enum class Rotation { + UNKNOWN, + NONE, + MIRRORED, + ROTATED_180, + ROTATED_180_MIRRORED, + ROTATED_90, + ROTATED_90_MIRRORED, + ROTATED_270, + ROTATED_270_MIRRORED, +}; + +#endif // ROTATION_HH diff --git a/src/send_file.cc b/src/send_file.cc new file mode 100644 index 0000000..8611b6f --- /dev/null +++ b/src/send_file.cc @@ -0,0 +1,45 @@ +#include "common.hh" + +#include "config.hh" +#include "send_file.hh" + +namespace { + +class SendFileImpl : public SendFile { +public: + bool setup(Logger*, Config* config, + std::string_view sendfile_header_name, + std::string_view sendfile_path_name) override { + header_ = config->get(std::string(sendfile_header_name), ""); + path_ = config->get(std::string(sendfile_path_name), ""); + return true; + } + + std::unique_ptr create_ok_file( + Transport* transport, + std::filesystem::path const& full_path, std::string_view relative_path, + std::string_view etag, std::optional size) override { + if (header_.empty()) { + auto resp = transport->create_ok_file(full_path); + if (!etag.empty()) + resp->add_header("ETag", std::string(etag)); + if (size.has_value()) + resp->add_header("Content-Length", std::to_string(size.value())); + return resp; + } + + auto resp = transport->create_ok_data(""); + resp->add_header(header_, path_ + std::string(relative_path)); + return resp; + } + +private: + std::string header_; + std::string path_; +}; + +} // namespace + +std::unique_ptr SendFile::create() { + return std::make_unique(); +} diff --git a/src/send_file.hh b/src/send_file.hh new file mode 100644 index 0000000..aca21b6 --- /dev/null +++ b/src/send_file.hh @@ -0,0 +1,35 @@ +#ifndef SEND_FILE_HH +#define SEND_FILE_HH + +#include "transport.hh" + +#include +#include +#include +#include + +class Logger; +class Config; + +class SendFile { +public: + virtual ~SendFile() = default; + + static std::unique_ptr create(); + + virtual bool setup(Logger* logger, Config* config, + std::string_view sendfile_header_name, + std::string_view sendfile_path_name) = 0; + + virtual std::unique_ptr create_ok_file( + Transport* transport, std::filesystem::path const& full_path, + std::string_view relative_path, std::string_view etag, + std::optional size) = 0; + +protected: + SendFile() = default; + SendFile(SendFile const&) = delete; + SendFile& operator=(SendFile const&) = delete; +}; + +#endif // SEND_FILE_HH diff --git a/src/server.cc b/src/server.cc new file mode 100644 index 0000000..b2d03fa --- /dev/null +++ b/src/server.cc @@ -0,0 +1,256 @@ +#include "common.hh" + +#include "args.hh" +#include "config.hh" +#include "inet.hh" +#include "logger.hh" +#include "looper.hh" +#include "signal_handler.hh" +#include "site.hh" +#include "task_runner.hh" +#include "transport.hh" +#include "transport_fastcgi.hh" +#include "transport_http.hh" +#include "travel.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef VERSION +# warning VERSION not set +# define VERSION "" +#endif + +namespace { + +class Server { +public: + ~Server() { + for (auto& fd : listen_) + looper_->remove(fd.get()); + } + + bool setup(Logger* logger, Option const* config_arg, Option const* log_arg, + std::function()> default_logger_factory) { + auto config = Config::create(logger, config_arg->is_set() + ? config_arg->arg() : "travel3.conf"); + if (!config) + return false; + + { + std::filesystem::path log_file; + if (log_arg->is_set()) { + log_file = log_arg->arg(); + } else { + log_file = config->get_path("log_file", ""); + } + if (!log_file.empty()) { + logger_ = Logger::create_file(log_file, logger); + // If a log_file was requested and we can't append to it, treat as + // fatal error instead of falling back to default. + if (!logger_) + return false; + } else { + logger_ = default_logger_factory(); + } + } + + // EPIPE is handled so SIGPIPE is no help. + signal(SIGPIPE, SIG_IGN); + + looper_ = Looper::create(); + runner_ = TaskRunner::create(looper_); + travel_ = Travel::create(logger_, runner_); + if (!travel_->setup(logger, config.get())) + return false; + site_ = Site::create(logger_, runner_, travel_); + if (!site_->setup(logger, config.get())) + return false; + + { + std::map> + transport_factories; + transport_factories.emplace("http", create_transport_factory_http()); + transport_factories.emplace("fastcgi", + create_transport_factory_fastcgi()); + + auto* transport_factory_name = config->get("transport", "http"); + auto it = transport_factories.find(transport_factory_name); + if (it == transport_factories.end()) { + logger->err("Unknown or unsupported transport: `%s'", + transport_factory_name); + return false; + } + handler_ = Transport::create_default_handler(logger_, site_->handler()); + transport_ = it->second->create(logger_, looper_, runner_, + logger, config.get(), handler_.get()); + + if (!transport_) + return false; + } + + if (!inet::bind_and_listen(logger, + config->get("bind", ""), + config->get("port", "5555"), + &listen_)) + return false; + + assert(!listen_.empty()); + + for (auto& fd : listen_) + looper_->add(fd.get(), Looper::EVENT_READ, + std::bind(&Server::accept, this, + fd.get(), std::placeholders::_1)); + + return true; + } + + bool run() { + assert(logger_); + assert(looper_); + assert(transport_); + + if (listen_.empty()) + return true; + + auto int_handler = SignalHandler::create( + looper_, SignalHandler::Signal::INT, + std::bind(&Looper::quit, looper_)); + + auto term_handler = SignalHandler::create( + looper_, SignalHandler::Signal::TERM, + std::bind(&Looper::quit, looper_)); + + auto hup_handler = SignalHandler::create( + looper_, SignalHandler::Signal::HUP, + std::bind(&Server::reload, this)); + + travel_->start(); + site_->start(); + + return looper_->run(logger_.get()); + } + +private: + void reload() { + travel_->reload(); + site_->reload(); + } + + void accept(int listen_fd, uint8_t event) { + if (event & Looper::EVENT_READ) { + auto fd = inet::accept(logger_.get(), listen_fd, true); + if (fd) { + transport_->add_client(std::move(fd)); + } + return; + } + if (event & Looper::EVENT_ERROR) { + looper_->remove(listen_fd); + auto it = std::find_if(listen_.begin(), listen_.end(), + [listen_fd] (auto& unique_fd) -> bool { + return unique_fd.get() == listen_fd; + }); + if (it == listen_.end()) { + assert(false); + } else { + listen_.erase(it); + } + if (listen_.empty()) + looper_->quit(); + return; + } + // Event should be either read or error + assert(false); + } + + std::shared_ptr logger_; + std::shared_ptr looper_; + std::shared_ptr runner_; + std::shared_ptr travel_; + std::unique_ptr site_; + std::unique_ptr handler_; + std::unique_ptr transport_; + std::vector listen_; +}; + +constexpr const char kTryMessage[] = "Try `travel3-server --help` for usage."; + +} // namespace + +int main(int argc, char** argv) { + auto args = Args::create(); + auto* help_arg = args->add_option('h', "help", "display this text and exit."); + auto* version_arg = args->add_option('V', "version", + "display version and exit."); + auto* daemon_arg = args->add_option( + 'D', "daemon", "fork a daemon process, logging to syslog per default"); + auto* log_arg = args->add_option_with_arg( + 'L', "log", "log to FILE instead of default (stdio or syslog)", "FILE"); + auto* config_arg = args->add_option_with_arg( + 'C', "config", "load config from CONFIG instead of default travel3.conf" + " in current directory.", "CONFIG"); + std::vector arguments; + if (!args->run(argc, argv, "travel3-server", std::cerr, &arguments)) { + std::cerr << kTryMessage << std::endl; + return EXIT_FAILURE; + } + if (!arguments.empty()) { + std::cerr << "Unexpected arguments.\n" + << kTryMessage << std::endl; + return EXIT_FAILURE; + } + if (help_arg->is_set()) { + std::cout << "Usage: `travel3-servers [OPTIONS...]'\n" + << "Starts the travel server to receive and handle requests.\n" + << "\n" + << "Options:\n"; + args->print_descriptions(std::cout, 80); + return EXIT_SUCCESS; + } + if (version_arg->is_set()) { + std::cout << "Travel " VERSION " written by " + "Joel Klinghed ." << std::endl; + return EXIT_SUCCESS; + } + + Server server; + + // Setup errors will always be logged to stdio to make them more visible. + auto logger = Logger::create_stdio(); + if (!server.setup(logger.get(), config_arg, log_arg, + [daemon_arg] () { + return daemon_arg->is_set() + ? Logger::create_syslog("travel3") + : Logger::create_stdio(); + })) + return EXIT_FAILURE; + + if (daemon_arg->is_set()) { + auto pid = fork(); + if (pid == -1) { + logger->err("Failed to fork(): %s", strerror(errno)); + return EXIT_FAILURE; + } + if (pid == 0) { + // Daemon process + chdir("/"); + setpgrp(); + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + return server.run() ? EXIT_SUCCESS : EXIT_FAILURE; + } else { + return EXIT_SUCCESS; + } + } else { + return server.run() ? EXIT_SUCCESS : EXIT_FAILURE; + } +} diff --git a/src/signal_handler.cc b/src/signal_handler.cc new file mode 100644 index 0000000..7d64e60 --- /dev/null +++ b/src/signal_handler.cc @@ -0,0 +1,75 @@ +#include "common.hh" + +#include "io.hh" +#include "looper.hh" +#include "signal_handler.hh" +#include "unique_pipe.hh" + +#include +#include + +namespace { + +std::unordered_map g_fds; + +int signum(SignalHandler::Signal signal) { + switch (signal) { + case SignalHandler::Signal::INT: + return SIGINT; + case SignalHandler::Signal::TERM: + return SIGTERM; + case SignalHandler::Signal::HUP: + return SIGHUP; + } + assert(false); + return 0; +} + +void signal_handler(int signum) { + auto it = g_fds.find(signum); + if (it != g_fds.end()) { + char c = 1; + io::write(it->second, &c, 1); + } +} + +class SignalHandlerImpl : public SignalHandler { +public: + SignalHandlerImpl(std::shared_ptr looper, Signal signal, + std::function callback) + : looper_(looper), signal_(signum(signal)), callback_(std::move(callback)) { + looper_->add(pipe_.reader(), Looper::EVENT_READ, + std::bind(&SignalHandlerImpl::call, this, + std::placeholders::_1)); + g_fds[signal_] = pipe_.writer(); + + ::signal(signal_, signal_handler); + } + + ~SignalHandlerImpl() { + signal(signal_, SIG_DFL); + looper_->remove(pipe_.reader()); + g_fds.erase(signal_); + } + +private: + void call(uint8_t) { + char buf[10]; + io::read(pipe_.reader(), buf, 10); + callback_(); + } + + unique_pipe pipe_; + std::shared_ptr looper_; + int signal_; + std::function callback_; +}; + +} // namespace + +std::unique_ptr SignalHandler::create( + std::shared_ptr looper, Signal signal, + std::function callback) { + return std::make_unique( + std::move(looper), signal, std::move(callback)); +} diff --git a/src/signal_handler.hh b/src/signal_handler.hh new file mode 100644 index 0000000..dbbe6d4 --- /dev/null +++ b/src/signal_handler.hh @@ -0,0 +1,29 @@ +#ifndef SIGNAL_HANDLER_HH +#define SIGNAL_HANDLER_HH + +#include +#include + +class Looper; + +class SignalHandler { +public: + virtual ~SignalHandler() = default; + + enum class Signal { + INT, + TERM, + HUP, + }; + + static std::unique_ptr create(std::shared_ptr looper, + Signal signal, + std::function callback); + +protected: + SignalHandler() = default; + SignalHandler(SignalHandler const&) = delete; + SignalHandler& operator=(SignalHandler const&) = delete; +}; + +#endif // SIGNAL_HANDLER_HH diff --git a/src/site.cc b/src/site.cc new file mode 100644 index 0000000..e1111de --- /dev/null +++ b/src/site.cc @@ -0,0 +1,488 @@ +#include "common.hh" + +#include "config.hh" +#include "document.hh" +#include "hasher.hh" +#include "jsutil.hh" +#include "logger.hh" +#include "send_file.hh" +#include "site.hh" +#include "static_files.hh" +#include "strutil.hh" +#include "travel.hh" +#include "urlutil.hh" +#include "weak_ptr.hh" + +#include +#include + +namespace { + +const std::string kEmptyGif = std::string( + "GIF89a\x01\x00\x01\x00\x00\x00\x00\x21\xf9\x04" + "\x01\x0a\x00\x01\x00\x2c\x00\x00\x00\x00\x01\x00" + "\x01\x00\x00\x02\x02\x4c\x01\x00\x3b", 0x25); + +class SiteImpl : public Site, public Transport::Handler { +public: + SiteImpl(std::shared_ptr logger, std::shared_ptr runner, + std::shared_ptr travel) + : logger_(std::move(logger)), runner_(std::move(runner)), + travel_(std::move(travel)), media_sendfile_(SendFile::create()), + static_sendfile_(SendFile::create()), weak_ptr_owner_(this) {} + + bool setup(Logger* logger, Config* config) override { + trips_title_ = config->get("site.title", "Travels"); + trips_index_ = Document::create(trips_title_); + trips_index_->add_style("style/base.css"); + preload_trips_index(); + static_root_ = config->get_path("site.static_root", ""); + if (static_root_.empty()) { + logger->err( + "site.static_root must be set to directory with static files"); + return false; + } + auto threads = config->get("site.hasher_threads", 4); + if (!threads.has_value()) { + logger->err("site.hasher_threads is not a number: %s", + config->get("site.hasher_threads", "")); + return false; + } + if (threads.value() <= 0) { + logger->err("site.hasher_threads must be > 0"); + return false; + } + threads_ = threads.value(); + if (!media_sendfile_->setup(logger, config, + "site.sendfile.header", + "site.sendfile.path")) + return false; + if (!static_sendfile_->setup(logger, config, + "site.sendfile.header", + "site.sendfile.static_path")) + return false; + return true; + } + + void start() override { + assert(instance_ == 0); + + media_hasher_ = Hasher::create(logger_, runner_, threads_); + + do_start(); + } + + void reload() override { + preload_trips_index(); + + do_start(); + } + + Transport::Handler* handler() override { + return this; + } + + std::unique_ptr request( + Transport* transport, + Transport::Request const* request) override { + if (request->path() == "/") + return trips_index_->build(transport); + auto slash = request->path().find('/', 1); + if (slash == std::string::npos) { + auto trip_id = request->path().substr(1); + if (trip_.count(std::string(trip_id))) + return transport->create_redirect(std::string(request->path()) + "/", + false); + } else { + auto trip_id = std::string(request->path().substr(1, slash - 1)); + auto trip_it = trip_.find(trip_id); + if (trip_it != trip_.end()) { + auto media_id = std::string(request->path().substr(slash + 1)); + if (media_id.empty()) + return trip_it->second.index_->build(transport); + if (media_id == "viewer") + return trip_it->second.viewer_->build(transport); + if (media_id == "thumbnail") { + auto idx = str::parse_uint64( + std::string(request->query("media"))); + if (idx.has_value()) { + auto thumb_it = trip_it->second.thumbnail_.find(idx.value()); + if (thumb_it != trip_it->second.thumbnail_.end()) { + std::unique_ptr ret; + switch (thumb_it->second.type_) { + case Travel::Thumbnail::ThumbType::FILE: + ret = transport->create_ok_file(thumb_it->second.path_); + break; + case Travel::Thumbnail::ThumbType::EXIF: + ret = transport->create_ok_exif_thumbnail( + thumb_it->second.path_); + break; + } + if (ret) { + ret->add_header("Content-Type", + std::string(thumb_it->second.mime_type_)); + if (!thumb_it->second.etag_.empty()) + ret->add_header("ETag", thumb_it->second.etag_); + ret->add_header("Content-Length", + std::to_string(thumb_it->second.size_)); + return ret; + } + } + return create_empty_image(transport); + } + return transport->create_not_found(); + } + + auto media_it = trip_it->second.media_.find(media_id); + if (media_it != trip_it->second.media_.end()) { + std::string relative = trip_id + "/" + media_id; + return media_sendfile_->create_ok_file(transport, + media_it->second.path_, + relative, + media_it->second.etag_, + media_it->second.size_); + } + } + } + + auto resp = static_->request(transport, request->path()); + if (resp) + return resp; + return transport->create_not_found(); + } + +private: + struct Thumbnail { + Travel::Thumbnail::ThumbType type_; + std::filesystem::path path_; + std::string_view mime_type_; + std::string etag_; + uint64_t size_; + + explicit Thumbnail(Travel::Thumbnail const* thumbnail) + : type_(thumbnail->thumb_type()), path_(thumbnail->path()), mime_type_( + thumbnail->mime_type()), size_(thumbnail->size()) {} + }; + + struct Media { + std::filesystem::path path_; + std::string etag_; + std::optional size_; + + explicit Media(std::filesystem::path path) + : path_(std::move(path)) {} + }; + + struct Trip { + std::unique_ptr index_; + std::unique_ptr viewer_; + std::unordered_map media_; + std::unordered_map thumbnail_; + }; + + static void weak_loaded(std::shared_ptr> weak_ptr, + uint16_t instance) { + auto* ptr = weak_ptr->get(); + if (ptr) + ptr->loaded(instance); + } + + static void weak_hashed_media(std::shared_ptr> weak_ptr, + std::string const& trip_id, + std::string const& media_id, + size_t thumbnail_index, + std::string const& hash, uint64_t size) { + auto* ptr = weak_ptr->get(); + if (ptr) + ptr->hashed_media(trip_id, media_id, thumbnail_index, hash, size); + } + + void do_start() { + uint16_t instance = ++instance_; + + static_ = StaticFiles::create(logger_, runner_, static_sendfile_, + static_root_, threads_); + travel_->call_when_loaded( + std::bind(&SiteImpl::weak_loaded, weak_ptr_owner_.get(), + instance)); + } + + void preload_trips_index() { + auto* body = trips_index_->body(); + body->clear_content(); + body->add_tag("p", "Loading, please wait..."); + } + + void loaded(uint16_t instance) { + if (instance != instance_) + return; + + load_trips_index(); + + load_trips(); + } + + void load_trips_index() { + auto* body = trips_index_->body(); + body->clear_content(); + + body->add_tag("h1", trips_title_); + + auto* list = body->add_tag("p")->add_tag("ul")->attr("class", "trips"); + for (size_t i = 0; i < travel_->trips(); ++i) { + auto& trip = travel_->trip(i); + auto* item = list->add_tag("li")->attr("class", "trip"); + item->add_tag( + "a", + std::string(trip.title()) + " - " + std::to_string(trip.year())) + ->attr("href", url::escape(trip.id(), + url::EscapeFlags::KEEP_SLASH) + "/"); + item->add_tag("br"); + std::string extra; + if (trip.images() > 0) { + if (trip.images() > 1) + extra += std::to_string(trip.images()) + " images"; + else + extra += "1 image"; + } + if (trip.videos() > 0) { + if (!extra.empty()) + extra += ", "; + if (trip.videos() > 1) + extra += std::to_string(trip.videos()) + " videos"; + else + extra += "1 video"; + } + item->add_tag("span", extra)->attr("class", "subtitle"); + if (!trip.location().empty()) { + item->add(" "); + item->add_tag("a", "Map") + ->attr("class", "maps") + ->attr("href", maps_url(trip.location())) + ->attr("target", "_blank"); + } + } + } + + void load_trips() { + trip_.clear(); + + for (size_t i = 0; i < travel_->trips(); ++i) { + load_trip(travel_->trip(i)); + } + } + + void load_trip_index(Travel::Trip const& trip, Trip& site_trip) { + site_trip.index_ = Document::create(std::string(trip.title())); + site_trip.index_->add_style("../style/base.css"); + + auto* body = site_trip.index_->body(); + + body->add_tag("h1", std::string(trip.title())); + + auto* list = body->add_tag("p")->add_tag("ul")->attr("class", "days"); + for (size_t i = 0; i < trip.day_count(); ++i) { + auto& day = trip.day(i); + auto* item = list->add_tag("li")->attr("class", "day"); + item->add_tag("h4", day.date().to_format("%d/%m")); + for (size_t j = day.first(); j <= day.last(); ++j) { + auto& media = trip.media(j); + auto* link = item->add_tag("a"); + link->attr("href", "viewer?media=" + std::to_string(j)); + link->attr("title", media.date().to_format("%H:%M")); + std::string type; + switch (media.type()) { + case Travel::Media::Type::IMAGE: + type = "image"; + break; + case Travel::Media::Type::VIDEO: + type = "video"; + break; + } + auto* img = link->add_tag("img")->attr("class", "thumbnail " + type) + ->attr("width", "64")->attr("height", "64"); + if (media.thumbnail()) + img->attr("src", "thumbnail?media=" + std::to_string(j)); + item->add(" "); + } + } + } + + void load_trip_viewer(Travel::Trip const& trip, Trip& site_trip) { + site_trip.viewer_ = Document::create(std::string(trip.title())); + site_trip.viewer_->add_style("../style/base.css"); + site_trip.viewer_->add_style("../style/viewer.css"); + site_trip.viewer_->add_script("../js/media.js"); + + { + auto script = Tag::create("script"); + script->attr("type", "text/javascript"); + std::string content = "\n"; + for (size_t i = 0; i < trip.media_count(); ++i) { + auto& media = trip.media(i); + std::vector args; + site_trip.media_.emplace(media.id(), media.path()); + if (media.thumbnail()) + site_trip.thumbnail_.emplace(i, media.thumbnail()); + media_hasher_->hash(media.path(), + std::bind(&SiteImpl::weak_hashed_media, + weak_ptr_owner_.get(), + std::string(trip.id()), + std::string(media.id()), + i, + std::placeholders::_1, + std::placeholders::_2)); + args.push_back(js::quote(url::escape(media.id(), + url::EscapeFlags::KEEP_SLASH))); + args.push_back(std::to_string(media.width())); + args.push_back(std::to_string(media.height())); + if (media.location().empty()) + args.push_back("[]"); + else + args.push_back("[" + std::to_string(media.location().lat) + "," + + std::to_string(media.location().lng) + "]"); + args.push_back( + "new Date(" + js::quote( + media.date().to_format("%Y, %d, %m, %H, %M, %S")) + ")"); + switch (media.type()) { + case Travel::Media::Type::IMAGE: + switch (media.rotation()) { + case Rotation::UNKNOWN: + case Rotation::NONE: + args.push_back(js::quote("")); + break; + case Rotation::MIRRORED: + args.push_back(js::quote("scaleX(-1)")); + break; + case Rotation::ROTATED_90: + args.push_back(js::quote("scaleX(-1) rotate(-90deg)")); + break; + case Rotation::ROTATED_90_MIRRORED: + args.push_back(js::quote("rotate(-90deg)")); + break; + case Rotation::ROTATED_180: + args.push_back(js::quote("scaleY(-1)")); + break; + case Rotation::ROTATED_180_MIRRORED: + args.push_back(js::quote("scaleX(-1) scaleY(-1)")); + break; + case Rotation::ROTATED_270: + args.push_back(js::quote("scaleX(-1) rotate(90deg)")); + break; + case Rotation::ROTATED_270_MIRRORED: + args.push_back(js::quote("rotate(90deg)")); + break; + } + content += "media.push(new Image("; + str::join(args, ", ", content); + content += "));\n"; + break; + case Travel::Media::Type::VIDEO: + args.push_back(std::to_string(media.length())); + content += "media.push(new Video("; + str::join(args, ", ", content); + content += "));\n"; + break; + } + } + script->add(std::move(content)); + site_trip.viewer_->add_script(std::move(script)); + } + + auto* body = site_trip.viewer_->body(); + + auto* nav = body->add_tag("div")->attr("id", "header")->add_tag("ul"); + { + auto* li = nav->add_tag("li"); + li->add_tag("a", "D") + ->attr("id", "download") + ->attr("download", "") + ->attr("title", "Download [Shift-D]"); + li->add_tag("a", "P")->attr("id", "location") + ->attr("title", "Show location"); + } + nav->add_tag("li")->add_tag("a", "<") + ->attr("id", "previous") + ->attr("title", "Previous [Left Arrow]"); + nav->add_tag("li")->attr("id", "number"); + nav->add_tag("li")->add_tag("a", ">") + ->attr("id", "next") + ->attr("title", "Next [Right Arrow]"); + nav->add_tag("li")->add_tag("a", "X") + ->attr("id", "close") + ->attr("title", "Close [Escape]"); + body->add_tag("img")->attr("class", "content"); + body->add_tag("video")->attr("class", "content")->attr("controls", ""); + } + + void load_trip(Travel::Trip const& trip) { + Trip site_trip; + + load_trip_index(trip, site_trip); + load_trip_viewer(trip, site_trip); + + trip_.emplace(trip.id(), std::move(site_trip)); + } + + static std::string maps_url(Location location) { + std::string ret("https://google.com/maps?q="); + ret += std::to_string(location.lat); + ret.push_back(','); + ret += std::to_string(location.lng); + return ret; + } + + void hashed_media(std::string const& trip_id, + std::string const& media_id, + size_t thumbnail_index, + std::string const& hash, uint64_t size) { + if (hash.empty()) + return; + + auto trip_it = trip_.find(trip_id); + if (trip_it == trip_.end()) + return; + auto media_it = trip_it->second.media_.find(media_id); + if (media_it == trip_it->second.media_.end()) + return; + media_it->second.etag_ = "\"" + hash + "\""; + media_it->second.size_ = size; + auto thumbnail_it = trip_it->second.thumbnail_.find(thumbnail_index); + if (thumbnail_it == trip_it->second.thumbnail_.end()) + return; + thumbnail_it->second.etag_ = "\"" + hash + "-exif-thumb\""; + } + + std::unique_ptr create_empty_image( + Transport* transport) { + auto ret = transport->create_ok_data(kEmptyGif); + ret->add_header("Content-Type", "image/gif"); + ret->add_header("ETag", "\"bb229a48bee31f5d54ca12dc9bd960c63a671f" + "0d4be86a054c1d324a44499d96\""); + return ret; + } + + std::shared_ptr logger_; + std::shared_ptr runner_; + std::shared_ptr travel_; + size_t threads_{1}; + uint16_t instance_{0}; + std::string trips_title_; + std::unique_ptr trips_index_; + std::unordered_map trip_; + std::shared_ptr media_sendfile_; + std::unique_ptr media_hasher_; + std::filesystem::path static_root_; + std::shared_ptr static_sendfile_; + std::unique_ptr static_; + WeakPtrOwner weak_ptr_owner_; +}; + +} // namespace + +std::unique_ptr Site::create(std::shared_ptr logger, + std::shared_ptr runner, + std::shared_ptr travel) { + return std::make_unique(std::move(logger), std::move(runner), + std::move(travel)); +} diff --git a/src/site.hh b/src/site.hh new file mode 100644 index 0000000..b6a32ff --- /dev/null +++ b/src/site.hh @@ -0,0 +1,35 @@ +#ifndef SITE_HH +#define SITE_HH + +#include "transport.hh" + +#include + +class Config; +class Logger; +class TaskRunner; +class Travel; + +class Site { +public: + virtual ~Site() = default; + + static std::unique_ptr create(std::shared_ptr logger, + std::shared_ptr runner, + std::shared_ptr travel); + + virtual bool setup(Logger* logger, Config* config) = 0; + + virtual void start() = 0; + + virtual void reload() = 0; + + virtual Transport::Handler* handler() = 0; + +protected: + Site() = default; + Site(Site const&) = delete; + Site& operator=(Site const&) = delete; +}; + +#endif // SITE_HH diff --git a/src/static_files.cc b/src/static_files.cc new file mode 100644 index 0000000..7125ee0 --- /dev/null +++ b/src/static_files.cc @@ -0,0 +1,113 @@ +#include "common.hh" + +#include "files_finder.hh" +#include "hasher.hh" +#include "mime_types.hh" +#include "send_file.hh" +#include "static_files.hh" +#include "weak_ptr.hh" + +#include + +namespace { + +class StaticFilesImpl : public StaticFiles, public FilesFinder::Delegate { +public: + StaticFilesImpl( + std::shared_ptr logger, + std::shared_ptr runner, + std::shared_ptr send_file, + std::filesystem::path path, + size_t threads) + : root_(std::move(path)), send_file_(std::move(send_file)), + weak_ptr_owner_(this) { + finder_ = FilesFinder::create(logger, runner, root_, this, threads); + hasher_ = Hasher::create(logger, runner, threads); + } + + std::unique_ptr request( + Transport* transport, std::string_view path) override { + auto it = files_.find(std::string(path)); + if (it == files_.end()) + return nullptr; + auto resp = send_file_->create_ok_file(transport, it->second.path_, + it->first, it->second.etag_, + it->second.size_); + if (!it->second.mime_type_.empty()) + resp->add_header("Content-Type", it->second.mime_type_); + return resp; + } + + void file(std::filesystem::path path) override { + std::string name("/"); + name.append(path.filename()); + auto parent = path.parent_path(); + while (true) { + if (parent == root_ || !parent.has_parent_path()) + break; + name.insert(0, parent.filename()); + name.insert(0, "/"); + parent = parent.parent_path(); + } + files_[name].path_ = path; + if (path.has_extension()) + files_[name].mime_type_ = mime_types::from_extension( + std::string(path.extension()).substr(1)); + hasher_->hash(path, std::bind(&StaticFilesImpl::weak_hashed, + weak_ptr_owner_.get(), name, + std::placeholders::_1, + std::placeholders::_2)); + } + + void done() override { + finder_.reset(); + } + +private: + struct File { + std::filesystem::path path_; + std::string mime_type_; + std::string etag_; + std::optional size_; + }; + + static void weak_hashed(std::shared_ptr> weak_ptr, + std::string const& name, + std::string hash, + uint64_t size) { + auto* ptr = weak_ptr->get(); + if (ptr) + ptr->hashed(name, std::move(hash), size); + } + + void hashed(std::string const& name, std::string hash, uint64_t size) { + if (hash.empty()) + return; + + hash.insert(0, "\""); + hash.push_back('"'); + files_[name].etag_ = std::move(hash); + files_[name].size_ = size; + } + + std::filesystem::path root_; + std::shared_ptr send_file_; + std::unordered_map files_; + std::unique_ptr finder_; + std::unique_ptr hasher_; + + WeakPtrOwner weak_ptr_owner_; +}; + +} // namespace + +std::unique_ptr StaticFiles::create( + std::shared_ptr logger, + std::shared_ptr runner, + std::shared_ptr send_file, + std::filesystem::path path, + size_t threads) { + return std::make_unique( + std::move(logger), std::move(runner), std::move(send_file), + std::move(path), threads); +} diff --git a/src/static_files.hh b/src/static_files.hh new file mode 100644 index 0000000..ecff987 --- /dev/null +++ b/src/static_files.hh @@ -0,0 +1,33 @@ +#ifndef STATIC_FILES_HH +#define STATIC_FILES_HH + +#include "transport.hh" + +#include +#include + +class Logger; +class SendFile; +class TaskRunner; + +class StaticFiles { +public: + virtual ~StaticFiles() = default; + + static std::unique_ptr create( + std::shared_ptr logger, + std::shared_ptr runner, + std::shared_ptr send_file, + std::filesystem::path path, + size_t threads = 1); + + virtual std::unique_ptr request( + Transport* transport, std::string_view path) = 0; + +protected: + StaticFiles() = default; + StaticFiles(StaticFiles const&) = delete; + StaticFiles& operator=(StaticFiles const&) = delete; +}; + +#endif // STATIC_FILES_HH diff --git a/src/str_buffer.cc b/src/str_buffer.cc new file mode 100644 index 0000000..6771cad --- /dev/null +++ b/src/str_buffer.cc @@ -0,0 +1,92 @@ +#include "common.hh" + +#include "str_buffer.hh" + +#include + +namespace { + +template +class StringBuffer : public RoBuffer { +public: + explicit StringBuffer(T str) + : str_(std::move(str)), offset_(0) {} + + bool empty() const override { + return offset_ >= str_.size(); + } + + char const* rbuf(size_t /* want */, size_t& avail) override { + avail = str_.size() - offset_; + return str_.data() + offset_; + } + + void rcommit(size_t bytes) override { + assert(str_.size() - offset_ >= bytes); + offset_ += bytes; + } + +private: + T str_; + size_t offset_; +}; + +class SharedStringBuffer : public Buffer { +public: + explicit SharedStringBuffer(std::shared_ptr content) + : content_(std::move(content)), read_ptr_(0), write_ptr_(content_->size()) { + } + + bool empty() const override { + return read_ptr_ >= content_->size(); + } + + char const* rbuf(size_t /* want */, size_t& avail) override { + avail = content_->size() - read_ptr_; + return content_->data() + read_ptr_; + } + + void rcommit(size_t bytes) override { + assert(content_->size() - read_ptr_ >= bytes); + read_ptr_ += bytes; + } + + bool full() const override { + return false; + } + + void clear() override { + content_->clear(); + read_ptr_ = write_ptr_ = 0; + } + + char* wbuf(size_t request, size_t& avail) override { + avail = request; + content_->resize(write_ptr_ + request); + return content_->data() + write_ptr_; + } + + void wcommit(size_t bytes) override { + assert(content_->size() - write_ptr_ >= bytes); + write_ptr_ += bytes; + } + +private: + std::shared_ptr content_; + size_t read_ptr_; + size_t write_ptr_; +}; + +} // namespace + +std::unique_ptr make_strbuffer(std::string content) { + return std::make_unique>(std::move(content)); +} + +std::unique_ptr make_strbuffer(std::string_view content) { + return std::make_unique>(content); +} + +std::unique_ptr make_strbuffer(std::shared_ptr content) { + return std::make_unique(std::move(content)); +} diff --git a/src/str_buffer.hh b/src/str_buffer.hh new file mode 100644 index 0000000..b791918 --- /dev/null +++ b/src/str_buffer.hh @@ -0,0 +1,14 @@ +#ifndef STR_BUFFER_HH +#define STR_BUFFER_HH + +#include "buffer.hh" + +#include +#include +#include + +std::unique_ptr make_strbuffer(std::string content); +std::unique_ptr make_strbuffer(std::string_view content); +std::unique_ptr make_strbuffer(std::shared_ptr content); + +#endif // STR_BUFFER_HH diff --git a/src/strutil.cc b/src/strutil.cc new file mode 100644 index 0000000..adee769 --- /dev/null +++ b/src/strutil.cc @@ -0,0 +1,211 @@ +#include "common.hh" + +#include "strutil.hh" + +#include +#include +#include + +namespace str { + +namespace { + +// Sadly strtoul(l) doesn't treat negative values an error but instead returns +// ULONG_MAX - value which is indistinguable from ULONG_MAX - value the positive +// way. +bool is_negative(std::string const& str) { + for (auto i = str.begin(); i != str.end(); ++i) { + if (!isspace(*i)) { + return *i == '-'; + } + } + return false; +} + +} // namespace + +std::optional parse_uint16(std::string const& str) { + static_assert(sizeof(unsigned long) >= sizeof(uint16_t), + "Need unsigned long to be >= uint16_t"); + if (str.empty() || is_negative(str)) + return std::nullopt; + char* end = nullptr; + errno = 0; + auto tmp = strtoul(str.c_str(), &end, 10); + if (errno || end != str.c_str() + str.size() || + tmp > std::numeric_limits::max()) + return std::nullopt; + return static_cast(tmp); +} + +std::optional parse_uint32(std::string const& str) { + static_assert(sizeof(unsigned long long) >= sizeof(uint32_t), + "Need unsigned long long to be >= uint32_t"); + if (str.empty() || is_negative(str)) + return std::nullopt; + char* end = nullptr; + errno = 0; + auto tmp = strtoull(str.c_str(), &end, 10); + if (errno || end != str.c_str() + str.size() || + tmp > std::numeric_limits::max()) + return std::nullopt; + return static_cast(tmp); +} + +std::optional parse_uint64(std::string const& str) { + static_assert(sizeof(unsigned long long) >= sizeof(uint64_t), + "Need unsigned long long to be >= uint64_t"); + if (str.empty() || is_negative(str)) + return std::nullopt; + char* end = nullptr; + errno = 0; + auto tmp = strtoull(str.c_str(), &end, 10); + if (errno || end != str.c_str() + str.size() || + tmp > std::numeric_limits::max()) + return std::nullopt; + return static_cast(tmp); +} + +std::vector split(std::string_view str, char delim) { + std::vector ret; + size_t last = 0; + while (true) { + size_t next = str.find(delim, last); + if (next == std::string::npos) + break; + if (next > last) + ret.push_back(str.substr(last, next - last)); + last = next + 1; + } + if (last < str.size() || ret.empty()) + ret.push_back(str.substr(last)); + return ret; +} + +std::vector split(std::string const& str, char delim) { + std::vector ret; + size_t last = 0; + while (true) { + size_t next = str.find(delim, last); + if (next == std::string::npos) + break; + if (next > last) + ret.push_back(str.substr(last, next - last)); + last = next + 1; + } + if (last < str.size() || ret.empty()) + ret.push_back(str.substr(last)); + return ret; +} + +std::string join(std::vector const& in, char delim) { + std::string ret; + join(in, delim, ret); + return ret; +} + +std::string join(std::vector const& in, std::string_view delim) { + std::string ret; + join(in, delim, ret); + return ret; +} + +void join(std::vector const& in, char delim, std::string& out) { + join(in, std::string_view(&delim, 1), out); +} + +void join(std::vector const& in, std::string_view delim, + std::string& out) { + auto it = in.begin(); + if (it == in.end()) + return; + out.append(*it); + for (++it; it != in.end(); ++it) { + out.append(delim); + out.append(*it); + } +} + +std::string join(std::vector const& in, char delim) { + std::string ret; + join(in, delim, ret); + return ret; +} + +std::string join(std::vector const& in, + std::string_view delim) { + std::string ret; + join(in, delim, ret); + return ret; +} + +void join(std::vector const& in, char delim, + std::string& out) { + join(in, std::string_view(&delim, 1), out); +} + +void join(std::vector const& in, std::string_view delim, + std::string& out) { + auto it = in.begin(); + if (it == in.end()) + return; + out.append(*it); + for (++it; it != in.end(); ++it) { + out.append(delim); + out.append(*it); + } +} + +bool starts_with(std::string_view str, std::string_view prefix) { + return str.size() >= prefix.size() && + str.compare(0, prefix.size(), prefix) == 0; +} + +bool ends_with(std::string_view str, std::string_view suffix) { + return str.size() >= suffix.size() && + str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +std::string_view trim(std::string_view str) { + return ltrim(rtrim(str)); +} + +std::string_view ltrim(std::string_view str) { + size_t start = 0; + while (start < str.size() && str[start] == ' ') + ++start; + return str.substr(start); +} + +std::string_view rtrim(std::string_view str) { + size_t end = str.size(); + while (end > 0 && str[end - 1] == ' ') + --end; + return str.substr(0, end); +} + +std::string trim(std::string const& str) { + size_t start = 0; + while (start < str.size() && str[start] == ' ') + ++start; + size_t end = str.size(); + while (end > start && str[end - 1] == ' ') + --end; + return str.substr(start, end - start); +} + +std::string ltrim(std::string const& str) { + size_t start = 0; + while (start < str.size() && str[start] == ' ') + ++start; + return str.substr(start); +} + +std::string rtrim(std::string const& str) { + size_t end = str.size(); + while (end > 0 && str[end - 1] == ' ') + --end; + return str.substr(0, end); +} + +} // namespace str diff --git a/src/strutil.hh b/src/strutil.hh new file mode 100644 index 0000000..28bbf55 --- /dev/null +++ b/src/strutil.hh @@ -0,0 +1,51 @@ +#ifndef STRUTIL_HH +#define STRUTIL_HH + +#include +#include +#include +#include +#include + +namespace str { + +std::optional parse_uint16(std::string const& str); +std::optional parse_uint32(std::string const& str); +std::optional parse_uint64(std::string const& str); + +// Empty substrings are ignored but out will always be at least one entry +std::vector split(std::string_view str, char delim = ' '); + +std::vector split(std::string const& str, char delim = ' '); + +std::string join(std::vector const& in, char delim); +std::string join(std::vector const& in, std::string_view delim); + +void join(std::vector const& in, char delim, std::string& out); +void join(std::vector const& in, std::string_view delim, + std::string& out); + +std::string join(std::vector const& in, char delim); +std::string join(std::vector const& in, + std::string_view delim); + +void join(std::vector const& in, char delim, + std::string& out); +void join(std::vector const& in, std::string_view delim, + std::string& out); + +[[nodiscard]] std::string_view trim(std::string_view str); +[[nodiscard]] std::string trim(std::string const& str); + +[[nodiscard]] std::string_view ltrim(std::string_view str); +[[nodiscard]] std::string ltrim(std::string const& str); + +[[nodiscard]] std::string_view rtrim(std::string_view str); +[[nodiscard]] std::string rtrim(std::string const& str); + +[[nodiscard]] bool starts_with(std::string_view str, std::string_view prefix); +[[nodiscard]] bool ends_with(std::string_view str, std::string_view suffix); + +} // namespace str + +#endif // STRUTIL_HH diff --git a/src/tag.cc b/src/tag.cc new file mode 100644 index 0000000..a563b9d --- /dev/null +++ b/src/tag.cc @@ -0,0 +1,139 @@ +#include "common.hh" + +#include "htmlutil.hh" +#include "tag.hh" + +#include +#include + +namespace { + +class TextRenderable : public virtual Renderable { +public: + explicit TextRenderable(std::string content) + : content_(std::move(content)) {} + + void render(std::string* out) const override { + html::escape(content_, out); + } + +private: + std::string content_; +}; + +class ScriptRenderable : public virtual Renderable { +public: + explicit ScriptRenderable(std::string content) + : content_(std::move(content)) {} + + void render(std::string* out) const override { + out->append(content_); + } + +private: + std::string content_; +}; + +class TagImpl : public Tag { +public: + TagImpl(std::string name, std::string content) + : name_(std::move(name)) { + add(std::move(content)); + assert(html::escape(name_) == name_); + } + + std::string_view name() const override { + return name_; + } + + bool empty() const override { + return child_.empty(); + } + + bool has_attr(std::string const& name) const override { + return attr_.count(name); + } + + Tag* clear_content() override { + child_.clear(); + return this; + } + + Tag* attr(std::string name, std::string value) override { + assert(html::escape(name) == name); + attr_[name] = std::move(value); + return this; + } + + Tag* add(std::string content) override { + if (!content.empty()) { + if (name_ == "script") { + child_.push_back( + std::make_unique(std::move(content))); + } else { + child_.push_back(std::make_unique(std::move(content))); + } + } + return this; + } + + Tag* add(std::unique_ptr tag) override { + auto* ret = tag.get(); + child_.push_back(std::move(tag)); + return ret; + } + + void render(std::string* out) const override { + size_t need = 1 + name_.size(); + for (auto const& pair : attr_) + need += 1 + pair.first.size() + 2 + pair.second.size() + 1; + if (empty()) + ++need; + out->reserve(need); + out->push_back('<'); + out->append(name_); + for (auto const& pair : attr_) { + out->push_back(' '); + out->append(pair.first); + out->append("=\""); + html::escape(pair.second, out, html::EscapeTarget::ATTRIBUTE); + out->push_back('"'); + } + if (empty()) { + if (name_ == "script") { + // Some browsers don't allow + out->append(">"); + } else { + // There are tags, like
where the / is optional but why botter. + out->append("/>"); + } + return; + } + out->push_back('>'); + + for (auto& child : child_) { + child->render(out); + } + + out->reserve(name_.size() + 3); + out->append("append(name_); + out->push_back('>'); + } + +private: + std::string name_; + std::map attr_; + std::vector> child_; +}; + +} // namespace + +Tag* Tag::add_tag(std::string name, std::string content) { + return add(create(std::move(name), std::move(content))); +} + +std::unique_ptr Tag::create(std::string name, std::string content) { + return std::make_unique(std::move(name), std::move(content)); +} diff --git a/src/tag.hh b/src/tag.hh new file mode 100644 index 0000000..ce7f149 --- /dev/null +++ b/src/tag.hh @@ -0,0 +1,42 @@ +#ifndef TAG_HH +#define TAG_HH + +#include +#include +#include + +class Renderable { +public: + virtual ~Renderable() = default; + + virtual void render(std::string* out) const = 0; + +protected: + Renderable() = default; +}; + +class Tag : public virtual Renderable { +public: + static std::unique_ptr create(std::string name, + std::string content = std::string()); + + virtual std::string_view name() const = 0; + virtual bool empty() const = 0; + virtual bool has_attr(std::string const& name) const = 0; + + virtual Tag* attr(std::string name, std::string value) = 0; + virtual Tag* add(std::string content) = 0; + // Does not clear attributes. + virtual Tag* clear_content() = 0; + + // These return the new child tag, not this. + virtual Tag* add(std::unique_ptr tag) = 0; + virtual Tag* add_tag(std::string name, std::string content = std::string()); + +protected: + Tag() = default; + Tag(Tag const&) = delete; + Tag& operator=(Tag const&) = delete; +}; + +#endif // TAG_HH diff --git a/src/task_runner.hh b/src/task_runner.hh new file mode 100644 index 0000000..d1c79d6 --- /dev/null +++ b/src/task_runner.hh @@ -0,0 +1,24 @@ +#ifndef TASK_RUNNER_HH +#define TASK_RUNNER_HH + +#include +#include + +class Looper; + +class TaskRunner { +public: + virtual ~TaskRunner() = default; + + static std::unique_ptr create(std::shared_ptr looper); + static std::unique_ptr create(size_t threads = 1); + + virtual void post(std::function callback) = 0; + +protected: + TaskRunner() = default; + TaskRunner(TaskRunner const&) = delete; + TaskRunner& operator=(TaskRunner const&) = delete; +}; + +#endif // TASK_RUNNER_HH diff --git a/src/task_runner_looper.cc b/src/task_runner_looper.cc new file mode 100644 index 0000000..4c1292b --- /dev/null +++ b/src/task_runner_looper.cc @@ -0,0 +1,75 @@ +#include "common.hh" + +#include +#include + +#include "io.hh" +#include "looper.hh" +#include "task_runner.hh" +#include "unique_pipe.hh" + +namespace { + +constexpr char kMessage = 1; + +class TaskRunnerLooper : public TaskRunner { +public: + explicit TaskRunnerLooper(std::shared_ptr looper) + : looper_(looper), pipe_(true, false) { + looper_->add(pipe_.reader(), Looper::EVENT_READ, + std::bind(&TaskRunnerLooper::run, this, + std::placeholders::_1)); + } + + ~TaskRunnerLooper() override { + looper_->remove(pipe_.reader()); + } + + void post(std::function callback) override { + bool notify; + { + std::lock_guard lock(mutex_); + notify = queue_.empty(); + queue_.push_back(std::move(callback)); + } + if (notify) + io::write_all(pipe_.writer(), &kMessage, 1); + } + +private: + void run(uint8_t event) { + if (event & Looper::EVENT_READ) { + char in[1]; + io::read_all(pipe_.reader(), &in, 1); + } + if (event & Looper::EVENT_ERROR) { + assert(false); + looper_->quit(); + } + bool more; + do { + std::function callback; + { + std::lock_guard lock(mutex_); + if (queue_.empty()) + break; + callback = std::move(queue_.front()); + queue_.pop_front(); + more = !queue_.empty(); + } + callback(); + } while (more); + } + + std::shared_ptr looper_; + unique_pipe pipe_; + std::mutex mutex_; + std::deque> queue_; +}; + +} // namespace + +std::unique_ptr TaskRunner::create(std::shared_ptr looper) { + return std::make_unique(looper); +} + diff --git a/src/task_runner_reply.hh b/src/task_runner_reply.hh new file mode 100644 index 0000000..1ca65a0 --- /dev/null +++ b/src/task_runner_reply.hh @@ -0,0 +1,19 @@ +#ifndef TASK_RUNNER_REPLY_HH +#define TASK_RUNNER_REPLY_HH + +#include "task_runner.hh" + +#include + +template +void post_and_reply(TaskRunner* callback_runner, + std::function callback, + std::shared_ptr reply_runner, + std::function reply) { + callback_runner->post([callback, reply, reply_runner] () { + auto r = callback(); + reply_runner->post(std::bind(std::move(reply), std::move(r))); + }); +} + +#endif // TASK_RUNNER_REPLY_HH diff --git a/src/task_runner_thread.cc b/src/task_runner_thread.cc new file mode 100644 index 0000000..6e1a06f --- /dev/null +++ b/src/task_runner_thread.cc @@ -0,0 +1,73 @@ +#include "common.hh" + +#include +#include +#include +#include +#include + +#include "task_runner.hh" + +namespace { + +class TaskRunnerThread : public TaskRunner { +public: + explicit TaskRunnerThread(size_t threads) + : threads_(std::max(1, threads)) { + thread_ = std::make_unique(threads_); + for (size_t i = 0; i < threads_; ++i) + thread_[i] = std::thread(&TaskRunnerThread::thread, this); + } + + ~TaskRunnerThread() override { + { + std::lock_guard lock(mutex_); + quit_ = true; + } + cond_.notify_all(); + for (size_t i = 0; i < threads_; ++i) + thread_[i].join(); + } + + void post(std::function callback) override { + { + std::lock_guard lock(mutex_); + queue_.push_back(std::move(callback)); + } + cond_.notify_one(); + } + +private: + void thread() { + while (true) { + std::function callback; + while (true) { + std::unique_lock lock(mutex_); + if (queue_.empty()) { + if (quit_) + return; + cond_.wait(lock); + } else { + callback = std::move(queue_.front()); + queue_.pop_front(); + break; + } + } + + callback(); + } + } + + size_t const threads_; + bool quit_{false}; + std::condition_variable cond_; + std::mutex mutex_; + std::deque> queue_; + std::unique_ptr thread_; +}; + +} // namespace + +std::unique_ptr TaskRunner::create(size_t threads) { + return std::make_unique(threads); +} diff --git a/src/timezone.cc b/src/timezone.cc new file mode 100644 index 0000000..ca0f501 --- /dev/null +++ b/src/timezone.cc @@ -0,0 +1,38 @@ +#include "common.hh" + +#include "geo_json.hh" +#include "timezone.hh" +#include "tz_info.hh" + +namespace { + +class TimezoneImpl : public Timezone { +public: + TimezoneImpl(std::shared_ptr logger, + std::filesystem::path geojsondb, + std::filesystem::path tzinfo_dir) + : geojson_(GeoJson::create(logger, std::move(geojsondb))), + tzinfo_(TzInfo::create(logger, std::move(tzinfo_dir))) { + } + + std::optional get_local_time(double lat, double lng, + time_t utc_time) const override { + auto tzid = geojson_->get_data(lat, lng, "tzid"); + if (tzid.has_value()) + return tzinfo_->get_local_time(tzid.value(), utc_time); + return std::nullopt; + } + +private: + std::unique_ptr geojson_; + std::unique_ptr tzinfo_; +}; + +} // namespace + +std::unique_ptr Timezone::create(std::shared_ptr logger, + std::filesystem::path geojsondb, + std::filesystem::path tzinfo_dir) { + return std::make_unique(std::move(logger), std::move(geojsondb), + std::move(tzinfo_dir)); +} diff --git a/src/timezone.hh b/src/timezone.hh new file mode 100644 index 0000000..3d33eb8 --- /dev/null +++ b/src/timezone.hh @@ -0,0 +1,27 @@ +#ifndef TIMEZONE_HH +#define TIMEZONE_HH + +#include +#include +#include + +class Logger; + +class Timezone { +public: + virtual ~Timezone() = default; + + static std::unique_ptr create(std::shared_ptr logger, + std::filesystem::path geojsondb, + std::filesystem::path tzinfo_dir); + + virtual std::optional get_local_time(double lat, double lng, + time_t utc_time) const = 0; + +protected: + Timezone() = default; + Timezone(Timezone const&) = delete; + Timezone& operator=(Timezone const&) = delete; +}; + +#endif // TIMEZONE_HH diff --git a/src/transport.cc b/src/transport.cc new file mode 100644 index 0000000..8195db7 --- /dev/null +++ b/src/transport.cc @@ -0,0 +1,195 @@ +#include "common.hh" + +#include "pathutil.hh" +#include "strutil.hh" +#include "transport.hh" + +#include + +namespace { + +class NoContentInput : public Transport::Input { +public: + Return fill(Buffer*, size_t) override { + return Return::END; + } + + void wait_once(std::shared_ptr, std::function callback) + override { + assert(false); + callback(); + } +}; + +class NoContentResponse : public Transport::Response { +public: + explicit NoContentResponse(std::unique_ptr response) + : response_(std::move(response)) { + } + + uint16_t code() const override { + return response_->code(); + } + + std::vector> const& + headers() const override { + return response_->headers(); + } + + std::unique_ptr open_content() override { + return std::make_unique(); + } + + void add_header(std::string name, std::string value) override { + response_->add_header(std::move(name), std::move(value)); + } + +private: + std::unique_ptr response_; +}; + +void copy_headers(Transport::Response const* src, Transport::Response* dst, + std::string_view name) { + for (auto const& header_pair : src->headers()) { + if (header_pair.first == name) { + dst->add_header(std::string(header_pair.first), + std::string(header_pair.second)); + } + } +} + +std::optional get_first_header( + Transport::Response const* resp, std::string_view name) { + for (auto const& header_pair : resp->headers()) { + if (header_pair.first == name) { + return header_pair.second; + } + } + return std::nullopt; +} + +class DefaultHandler : public Transport::Handler { +public: + DefaultHandler(std::shared_ptr logger, Transport::Handler* handler) + : logger_(logger), handler_(handler) {} + + std::unique_ptr request( + Transport* transport, + Transport::Request const* request) override { + bool include_content; + if (request->method() == "GET") { + include_content = true; + } else if (request->method() == "HEAD") { + include_content = false; + } else { + return transport->create_data(405, ""); + } + + auto clean_path = path::cleanup(request->path()); + if (clean_path != request->path()) { + auto response = transport->create_redirect(clean_path, false); + return response; + } + + auto response = handler_->request(transport, request); + + if (!response) + return nullptr; + + bool not_modified = false; + + { + auto values = request->header_all("if-none-match"); + auto etag = get_first_header(response.get(), "ETag"); + + bool match = false; + for (auto const& value : values) { + if (value == "*") { + match = true; + break; + } + if (!etag) + continue; + if (str::starts_with(value, "W/") || str::starts_with(value, "w/")) { + if (value.compare(2, value.size() - 2, etag.value()) == 0) { + match = true; + break; + } + } else { + if (value == etag.value()) { + match = true; + break; + } + } + } + + if (match) + not_modified = true; + } + + // TODO: Add If-Modified-Since? Not useful right now tho as we have + // no content that returns Date: headers. + + if (not_modified) { + auto new_response = transport->create_data(304, ""); + copy_headers(response.get(), new_response.get(), "Cache-Control"); + copy_headers(response.get(), new_response.get(), "Content-Location"); + copy_headers(response.get(), new_response.get(), "Date"); + copy_headers(response.get(), new_response.get(), "ETag"); + copy_headers(response.get(), new_response.get(), "Expires"); + copy_headers(response.get(), new_response.get(), "Vary"); + return new_response; + } + + return include_content + ? std::move(response) + : std::make_unique(std::move(response)); + } + +private: + std::shared_ptr logger_; + Transport::Handler* const handler_; +}; + +} // namespace + +void Transport::Response::open_content_async( + std::shared_ptr, + std::function)> callback) { + assert(false); + callback(open_content()); +} + +std::unique_ptr Transport::create_ok_data( + std::string data) { + if (data.empty()) + return create_data(204, std::move(data)); + return create_data(200, std::move(data)); +} + +std::unique_ptr Transport::create_ok_file( + std::filesystem::path path) { + return create_file(200, std::move(path)); +} + +std::unique_ptr Transport::create_ok_exif_thumbnail( + std::filesystem::path path) { + return create_exif_thumbnail(200, std::move(path)); +} + +std::unique_ptr Transport::create_not_found() { + return create_data(404, std::string()); +} + +std::unique_ptr Transport::create_redirect( + std::string location, bool temporary) { + auto ret = create_data(temporary ? 302 : 301, std::string()); + ret->add_header("Location", std::move(location)); + return ret; +} + +std::unique_ptr Transport::create_default_handler( + std::shared_ptr logger, + Handler* handler) { + return std::make_unique(logger, handler); +} diff --git a/src/transport.hh b/src/transport.hh new file mode 100644 index 0000000..5e6733f --- /dev/null +++ b/src/transport.hh @@ -0,0 +1,147 @@ +#ifndef TRANSPORT_HH +#define TRANSPORT_HH + +#include "unique_fd.hh" + +#include +#include +#include +#include +#include +#include +#include + +class Buffer; +class Config; +class Logger; +class Looper; +class TaskRunner; + +class Transport { +public: + class Request { + public: + virtual ~Request() = default; + + virtual std::string_view method() const = 0; + virtual std::string_view path() const = 0; + virtual std::string_view query(std::string_view name) const = 0; + virtual std::optional header_one( + std::string_view name) const = 0; + virtual std::vector header_all( + std::string_view name) const = 0; + }; + + class Input { + public: + virtual ~Input() = default; + + enum class Return { + OK, // At least one byte was added to buffer. + FULL, // Zero bytes added to buffer as it is full. + END, // Zero or more bytes added to buffer but there are no more bytes + // available because input has ended. + ERR, // No bytes added to buffer and there was an fatal error reading + // any more bytes. + WAIT, // Zero or more bytes added to buffer but wait before calling + // fill again (see wait_once). + }; + + virtual Return fill(Buffer* buffer, size_t buf_request_size = 1) = 0; + + // Setup callback to be called when Input is ready to be read again using + // looper. Callback will only be called once. Callback might be called + // before wait returns if Input is already ready. If Input is destroyed + // before getting ready the callback will never be called. + virtual void wait_once(std::shared_ptr looper, + std::function callback) = 0; + + protected: + Input() = default; + }; + + class Response { + public: + virtual ~Response() = default; + + virtual uint16_t code() const = 0; + virtual std::vector> const& + headers() const = 0; + // Only call this once. If it returns null, call open_content_async instead. + virtual std::unique_ptr open_content() = 0; + // Only call this once and only if open_content() first returned nullptr. + // If response is destroyed before callback is posted the callback will + // never be posted. But if callback is already posted then it will run, + // and the input will still be valid. + virtual void open_content_async( + std::shared_ptr runner, + std::function)> callback); + + virtual void add_header(std::string name, std::string value) = 0; + + protected: + Response() = default; + }; + + std::unique_ptr create_ok_data(std::string data); + std::unique_ptr create_ok_file(std::filesystem::path path); + std::unique_ptr create_ok_exif_thumbnail( + std::filesystem::path path); + std::unique_ptr create_not_found(); + std::unique_ptr create_redirect(std::string target, + bool temporary = true); + virtual std::unique_ptr create_data( + uint16_t code, std::string data) = 0; + virtual std::unique_ptr create_file( + uint16_t code, std::filesystem::path data) = 0; + virtual std::unique_ptr create_exif_thumbnail( + uint16_t code, std::filesystem::path data) = 0; + + class Handler { + public: + virtual ~Handler() = default; + + virtual std::unique_ptr request(Transport* transport, + Request const* request) = 0; + + protected: + Handler() = default; + }; + + // Takes care of GET/HEAD, optional cache headers and general housekeeping. + // Also removes // and similar from paths. + static std::unique_ptr create_default_handler( + std::shared_ptr logger, + Handler* handler); + + class Factory { + public: + virtual ~Factory() = default; + + virtual std::unique_ptr create( + std::shared_ptr logger, + std::shared_ptr looper, + std::shared_ptr runner, + // config_logger is used to write any errors during create + // when reading the config. No reference kept as with logger. + Logger* config_logger, + Config const* config, + Handler* handler) = 0; + + protected: + Factory() = default; + Factory(Factory const&) = delete; + Factory& operator=(Factory const&) = delete; + }; + + virtual ~Transport() = default; + + virtual void add_client(unique_fd&& fd) = 0; + +protected: + Transport() = default; + Transport(Transport const&) = delete; + Transport& operator=(Transport const&) = delete; +}; + +#endif // TRANSPORT_HH diff --git a/src/transport_base.cc b/src/transport_base.cc new file mode 100644 index 0000000..00d582b --- /dev/null +++ b/src/transport_base.cc @@ -0,0 +1,677 @@ +#include "common.hh" + +#include "config.hh" +#include "file_opener.hh" +#include "image.hh" +#include "io.hh" +#include "logger.hh" +#include "looper.hh" +#include "task_runner.hh" +#include "transport_base.hh" +#include "urlutil.hh" + +namespace { + +class ResponseImpl : public Transport::Response { +public: + uint16_t code() const override { + return code_; + } + + std::vector> const& headers() + const override { + return headers_; + } + + void add_header(std::string name, std::string value) override { + headers_.emplace_back(std::move(name), std::move(value)); + } + +protected: + explicit ResponseImpl(uint16_t code) + : code_(code) {} + +private: + uint16_t const code_; + std::vector> headers_; +}; + +class DataInput : public Transport::Input { +public: + explicit DataInput(std::string data) + : data_(std::move(data)) {} + + Return fill(Buffer* buffer, size_t) override { + if (offset_ >= data_.size()) + return Return::END; + auto bytes = Buffer::write(buffer,data_.data() + offset_, + data_.size() - offset_); + if (bytes > 0) { + offset_ += bytes; + return Return::OK; + } + return Return::FULL; + } + + void wait_once(std::shared_ptr, + std::function callback) override { + assert(false); + callback(); + } + +private: + std::string data_; + size_t offset_{0}; +}; + +class ResponseData : public ResponseImpl { +public: + ResponseData(uint16_t code, std::string data) + : ResponseImpl(code), data_(std::move(data)) { + add_header("Content-Length", std::to_string(data_.size())); + } + + std::unique_ptr open_content() override { + return std::make_unique(std::move(data_)); + } + +private: + std::string data_; +}; + +class FileInput : public Transport::Input { +public: + explicit FileInput(unique_fd&& fd) + : fd_(std::move(fd)) {} + + ~FileInput() override { + if (looper_) + looper_->remove(fd_.get()); + } + + Return fill(Buffer* buffer, size_t buf_request_size) override { + size_t bytes; + switch (io::fill(fd_.get(), buffer, buf_request_size, &bytes)) { + case io::Return::OK: + break; + case io::Return::ERR: + return Return::ERR; + case io::Return::CLOSED: + return Return::END; + } + + if (bytes > 0) + return Return::OK; + if (buffer->full()) + return Return::FULL; + return Return::WAIT; + } + + void wait_once(std::shared_ptr looper, + std::function callback) override { + if (looper_) { + assert(false); + looper_->remove(fd_.get()); + } + looper_ = looper; + waiting_callback_ = std::move(callback); + looper_->add(fd_.get(), Looper::EVENT_READ, + std::bind(&FileInput::event, this, std::placeholders::_1)); + } + +private: + void event(uint8_t) { + looper_->remove(fd_.get()); + looper_.reset(); + auto callback = std::move(waiting_callback_); + callback(); + } + + unique_fd fd_; + std::shared_ptr looper_; + std::function waiting_callback_; +}; + +class ErrorInput : public Transport::Input { +public: + Return fill(Buffer*, size_t) override { + return Return::ERR; + } + + void wait_once(std::shared_ptr, + std::function callback) override { + assert(false); + callback(); + } +}; + +class ResponseFile : public ResponseImpl { +public: + ResponseFile(uint16_t code, std::shared_ptr file_opener, + std::filesystem::path path) + : ResponseImpl(code), opener_(std::move(file_opener)), + open_id_(opener_->open(std::move(path), + std::bind(&ResponseFile::opened, this, + std::placeholders::_1, + std::placeholders::_2))) { + } + + ~ResponseFile() { + if (open_id_) + opener_->cancel(open_id_); + } + + std::unique_ptr open_content() override { + if (open_id_) + return nullptr; + if (fd_) + return create_input(std::move(fd_)); + return std::make_unique(); + } + + void open_content_async( + std::shared_ptr runner, + std::function)> callback) + override { + if (open_id_) { + waiting_ = std::make_unique(std::move(callback)); + waiting_runner_ = std::move(runner); + } else { + callback(open_content()); + } + } + +protected: + virtual std::unique_ptr create_input(unique_fd&& fd) { + return std::make_unique(std::move(fd)); + } + +private: + class WaitingCallback { + public: + explicit WaitingCallback( + std::function)> callback) + : callback_(std::move(callback)) {} + + void input(std::unique_ptr input) { + assert(!input_); + input_ = std::move(input); + } + + void call() { + assert(input_); + callback_(std::move(input_)); + } + + private: + std::function)> callback_; + std::unique_ptr input_; + }; + + void opened(uint32_t id, unique_fd fd) { + assert(open_id_ == id); + open_id_ = 0; + opener_.reset(); + fd_ = std::move(fd); + + if (waiting_) { + waiting_->input(open_content()); + waiting_runner_->post(std::bind(&WaitingCallback::call, + waiting_)); + waiting_.reset(); + waiting_runner_.reset(); + } + } + + std::shared_ptr opener_; + uint32_t open_id_; + unique_fd fd_; + std::shared_ptr waiting_; + std::shared_ptr waiting_runner_; +}; + +class ExifThumbnailInput : public FileInput { +public: + explicit ExifThumbnailInput(unique_fd&& fd) + : FileInput(std::move(fd)), reader_(ThumbnailReader::create()) {} + + Return fill(Buffer* buffer, size_t buf_request_size) override { + if (buf_) { + auto file_ret = FileInput::fill(buf_.get(), 1); + switch (reader_->drain(buf_.get())) { + case ThumbnailReader::Return::NEED_MORE: + break; + case ThumbnailReader::Return::DONE: + buf_.reset(); + return fill_with_data(buffer, buf_request_size); + case ThumbnailReader::Return::ERR: + return Return::ERR; + } + switch (file_ret) { + case Return::OK: + case Return::ERR: + case Return::WAIT: + return file_ret; + case Return::FULL: + // ThumbnailReader should drain more than this + assert(false); + return Return::ERR; + case Return::END: + return Return::ERR; + } + } else { + return fill_with_data(buffer, buf_request_size); + } + } + +private: + Return fill_with_data(Buffer* buffer, size_t buf_request_size) { + if (offset_ >= reader_->data().size()) + return Return::END; + size_t avail; + auto* ptr = buffer->wbuf(buf_request_size, avail); + if (avail == 0) + return Return::FULL; + auto got = reader_->data().size() - offset_; + if (avail > got) + avail = got; + std::copy_n(reader_->data().data() + offset_, avail, ptr); + buffer->wcommit(avail); + offset_ += avail; + return Return::OK; + } + + std::unique_ptr reader_; + std::unique_ptr buf_{Buffer::fixed(10 * 1024)}; + size_t offset_{0}; +}; + +class ResponseExifThumbnail : public ResponseFile { +public: + ResponseExifThumbnail(uint16_t code, std::shared_ptr file_opener, + std::filesystem::path path) + : ResponseFile(code, std::move(file_opener), std::move(path)) {} + +protected: + std::unique_ptr create_input(unique_fd&& fd) override { + return std::make_unique(std::move(fd)); + } +}; + +} // namespace + +TransportBase::TransportBase(std::shared_ptr logger, + std::shared_ptr looper, + std::shared_ptr runner, + Handler* handler) + : logger_(logger), looper_(looper), runner_(runner), handler_(handler) { +} + +TransportBase::~TransportBase() { + // Clear these before calling client_abort to not cause any + // unnecessary client_new. + client_wait_.clear(); + + for (auto& client : client_) + client_abort(&client); +} + +uint64_t TransportBase::default_client_input_buffer_size() const { + // No POST/PUT support, really shouldn't be that big. + return 100 * 1024; +} + +uint64_t TransportBase::default_client_output_buffer_size() const { + // Might return actual files, but that is async so whole file + // doesn't need to fit. + return 1 * 1024 * 1024; +} + +std::unique_ptr TransportBase::create_data( + uint16_t code, std::string data) { + return std::make_unique(code, std::move(data)); +} + +std::unique_ptr TransportBase::create_file( + uint16_t code, std::filesystem::path path) { + return std::make_unique(code, file_opener_, std::move(path)); +} + +std::unique_ptr TransportBase::create_exif_thumbnail( + uint16_t code, std::filesystem::path path) { + return std::make_unique(code, file_opener_, + std::move(path)); +} + +bool TransportBase::setup(Logger* logger, Config const* config) { + client_.clear(); + auto clients = config->get("transport.max_clients", 10); + if (!clients.has_value()) { + logger->err("transport.max_clients is unknown value: '%s'", + config->get("transport.max_clients", nullptr)); + return false; + } + if (clients.value() < 1) { + logger->err("transport.max_clients must be > 0"); + return false; + } + for (size_t i = 0; i < clients.value(); ++i) + client_.emplace_back(i); + auto in_buffer_size = config->get_size("client.input_buffer_size", + default_client_input_buffer_size()); + if (!in_buffer_size.has_value()) { + logger->err("client.input_buffer_size is unknown size: `%s'", + config->get("client.input_buffer_size", nullptr)); + return false; + } + if (in_buffer_size.value() < 1) { + logger->err("client.input_buffer_size must be > 0"); + return false; + } + auto out_buffer_size = config->get_size("client.output_buffer_size", + default_client_output_buffer_size()); + if (!out_buffer_size.has_value()) { + logger->err("client.output_buffer_size is unknown size: `%s'", + config->get("client.output_buffer_size", nullptr)); + return false; + } + if (out_buffer_size.value() < 1) { + logger->err("client.output_buffer_size must be > 0"); + return false; + } + for (auto& client : client_) { + client.in_ = Buffer::fixed(in_buffer_size.value()); + client.out_ = Buffer::fixed(out_buffer_size.value()); + } + auto timeout = config->get_duration("client.timeout", 30.0); + if (!timeout.has_value()) { + logger->err("client.timeout is unknown duration: `%s'", + config->get("client.timeout", nullptr)); + return false; + } + if (timeout.value() <= 0.0) { + logger->err("client.timeout must be > 0"); + return false; + } + client_timeout_ = timeout.value(); + auto file_opener_threads = config->get("transport.workers", 1); + if (!file_opener_threads.has_value()) { + logger->err("transport.workers is unknown value: '%s'", + config->get("transport.workers", nullptr)); + return false; + } + if (file_opener_threads.value() <= 0) { + logger->err("transport.workers must be > 0"); + return false; + } + file_opener_ = FileOpener::create(runner_, file_opener_threads.value()); + return true; +} + +void TransportBase::add_client(unique_fd&& fd) { + if (!client_full_) { + auto const start = next_avail_client_; + do { + auto& client = client_[next_avail_client_++]; + if (next_avail_client_ == client_.size()) + next_avail_client_ = 0; + if (!client.fd_) { + client.fd_ = std::move(fd); + client_new(&client); + + // Assume there is data available directly to speed up responses + // in the common case. + client_event(&client, Looper::EVENT_READ); + return; + } + } while (next_avail_client_ != start); + client_full_ = true; + } + client_wait_.push_back(std::move(fd)); +} + +void TransportBase::client_new(Client* client) { + assert(client->fd_); + looper_->add(client->fd_.get(), Looper::EVENT_READ, + std::bind(&TransportBase::client_event, this, client, + std::placeholders::_1)); + client->last_event_ = std::chrono::steady_clock::now(); + client->timeout_ = looper_->schedule( + client_timeout_, + std::bind(&TransportBase::client_timeout, this, client, + std::placeholders::_1)); +} + +void TransportBase::client_timeout(Client* client, uint32_t id) { + assert(client->timeout_ == id); + client->timeout_ = 0; + + std::chrono::duration delay = + std::chrono::steady_clock::now() - client->last_event_; + + if (delay.count() < client_timeout_) { + client->timeout_ = looper_->schedule( + client_timeout_ - delay.count(), + std::bind(&TransportBase::client_timeout, this, client, + std::placeholders::_1)); + } else { + logger_->dbg("Client timeout %zu", client->index_); + client_abort(client); + } +} + +void TransportBase::client_event(Client* client, uint8_t event) { + client->last_event_ = std::chrono::steady_clock::now(); + + bool call_handle = false; + + if (event & Looper::EVENT_READ) { + size_t bytes = 0; + switch (io::fill(client->fd_.get(), client->in_.get(), + client->expect_in_, &bytes)) { + case io::Return::OK: + if (bytes > 0) + call_handle = true; + break; + case io::Return::ERR: + logger_->dbg("Error reading from client %zu", client->index_); + client_abort(client); + return; + case io::Return::CLOSED: + if (!client->in_closed_) + call_handle = true; + client->in_closed_ = true; + break; + } + } + if (event & Looper::EVENT_WRITE) { + size_t bytes = 0; + if (!io::drain(client->out_.get(), client->fd_.get(), &bytes)) { + logger_->dbg("Error writing to client %zu", client->index_); + client_abort(client); + return; + } + if (bytes > 0) + call_handle = true; + } + if (event & Looper::EVENT_ERROR) { + logger_->dbg("Looper error on client %zu", client->index_); + client_abort(client); + return; + } + + if (call_handle) { + if (!client_handle(client)) { + client_abort(client); + return; + } + } + + client_update_event(client); +} + +void TransportBase::client_update_event(Client* client) { + uint8_t events = 0; + if (!client->in_closed_ && client->expect_in_ > 0 && !client->in_->full()) + events |= Looper::EVENT_READ; + if (!client->out_->empty()) + events |= Looper::EVENT_WRITE; + looper_->update(client->fd_.get(), events); +} + +bool TransportBase::client_flush(Client* client) { + size_t bytes; + const bool was_full = client->out_->full(); + if (io::drain(client->out_.get(), client->fd_.get(), &bytes)) { + if (bytes > 0) { + client->last_event_ = std::chrono::steady_clock::now(); + } + + if (!client->out_->empty()) { + // Make sure to add EVENT_WRITE + client_update_event(client); + } + + if (bytes > 0 && was_full) { + for (auto& pair : client->responses_) { + if (pair.second.response_ && pair.second.content_) { + if (!client_response_content(client, pair.first)) + return false; + } + } + } + return true; + } + logger_->dbg("Error writing to client %zu", client->index_); + client_abort(client); + return false; +} + +void TransportBase::client_abort(Client* client) { + if (!client->fd_) + return; + + looper_->remove(client->fd_.get()); + if (client->timeout_) { + looper_->cancel(client->timeout_); + client->timeout_ = 0; + } + client->fd_.reset(); + client->in_->clear(); + client->out_->clear(); + client->responses_.clear(); + client->in_closed_ = false; + client->expect_in_ = 1; + + next_avail_client_ = client->index_; + client_full_ = false; +} + +bool TransportBase::client_response(Client* client, uint32_t id, + std::unique_ptr response) { + auto ret = client->responses_.emplace(id, std::move(response)); + assert(ret.second); + if (!client_response_header(client, id)) + return false; + assert(client->responses_.count(id)); + auto& cli_response = client->responses_[id]; + cli_response.content_ = cli_response.response_->open_content(); + if (cli_response.content_) + return client_response_content(client, id); + + cli_response.response_->open_content_async( + runner_, std::bind(&TransportBase::client_response_open, this, client, + id, std::placeholders::_1)); + return true; +} + +void TransportBase::client_response_open(Client* client, uint32_t id, + std::unique_ptr input) { + assert(client->responses_.count(id)); + auto& cli_response = client->responses_[id]; + cli_response.content_ = std::move(input); + client_response_content(client, id); +} + +bool TransportBase::client_response_header(Client*, uint32_t) { + return true; +} + +bool TransportBase::client_response_content(Client* client, uint32_t id) { + return client_response_content(client, id, client->out_.get()); +} + +bool TransportBase::client_response_content(Client* client, uint32_t id, + Buffer* out) { + assert(client->responses_.count(id)); + auto& cli_response = client->responses_[id]; + switch (cli_response.content_->fill(out)) { + case Input::Return::OK: + if (!client_flush(client)) + return false; + return client->responses_.count(id) == 0 || + client_response_content(client, id); + case Input::Return::FULL: + return client_flush(client); + case Input::Return::END: + return client_response_end(client, id); + case Input::Return::ERR: + logger_->warn("Input error for client %zu", client->index_); + client_abort(client); + return false; + case Input::Return::WAIT: + cli_response.content_->wait_once( + looper_, + std::bind(&TransportBase::client_response_content_wait, this, + client, id)); + return true; + } + assert(false); + return true; +} + +void TransportBase::client_response_content_wait(Client* client, uint32_t id) { + client_response_content(client, id); +} + +bool TransportBase::client_response_end(Client* client, uint32_t id) { + if (!client_response_footer(client, id)) + return false; + client->responses_.erase(id); + if (!client_handle(client)) + return false; + client_update_event(client); + return true; +} + +bool TransportBase::client_response_footer(Client*, uint32_t) { + return true; +} + +bool TransportBase::client_request(Client* client, uint32_t id, + std::unique_ptr request) { + auto response = handler_->request(this, request.get()); + if (response) { + return client_response(client, id, std::move(response)); + } else { + return client_response(client, id, create_data(500, "")); + } +} + +std::string_view TransportBase::UrlRequest::path() const { + split_url_if_needed(); + return path_; +} + +std::string_view TransportBase::UrlRequest::query(std::string_view name) const { + split_url_if_needed(); + auto it = query_.find(std::string(name)); + if (it == query_.end()) + return std::string_view(); + return it->second; +} + +void TransportBase::UrlRequest::split_url_if_needed() const { + if (path_.empty()) + url::split_and_unescape_path_and_query(url(), path_, query_); +} diff --git a/src/transport_base.hh b/src/transport_base.hh new file mode 100644 index 0000000..61d7601 --- /dev/null +++ b/src/transport_base.hh @@ -0,0 +1,113 @@ +#ifndef TRANSPORT_BASE_HH +#define TRANSPORT_BASE_HH + +#include "buffer.hh" +#include "transport.hh" + +#include +#include + +class FileOpener; + +class TransportBase : public Transport { +public: + ~TransportBase() override; + + std::unique_ptr create_data( + uint16_t code, std::string data) override; + + std::unique_ptr create_file( + uint16_t code, std::filesystem::path data) override; + + std::unique_ptr create_exif_thumbnail( + uint16_t code, std::filesystem::path data) override; + + void add_client(unique_fd&& fd) override; + + virtual bool setup(Logger* logger, Config const* config); + +protected: + struct ClientResponse { + std::unique_ptr response_; + std::unique_ptr content_; + + ClientResponse() = default; + explicit ClientResponse(std::unique_ptr response) + : response_(std::move(response)) {} + }; + + struct Client { + size_t const index_; + unique_fd fd_; + std::unique_ptr in_; + std::unique_ptr out_; + std::unordered_map responses_; + + bool in_closed_{false}; + size_t expect_in_{1}; + uint32_t timeout_{0}; + std::chrono::steady_clock::time_point last_event_; + + explicit Client(size_t index) + : index_(index) {} + }; + + class UrlRequest : public Request { + public: + std::string_view path() const override; + std::string_view query(std::string_view name) const override; + + protected: + virtual std::string_view url() const = 0; + + private: + void split_url_if_needed() const; + + std::string mutable path_; + std::unordered_map mutable query_; + }; + + TransportBase(std::shared_ptr logger, std::shared_ptr looper, + std::shared_ptr runner, Handler* handler); + + virtual uint64_t default_client_input_buffer_size() const; + virtual uint64_t default_client_output_buffer_size() const; + + virtual void client_new(Client* client); + virtual void client_event(Client* client, uint8_t event); + virtual void client_abort(Client* client); + virtual bool client_flush(Client* client); + virtual bool client_handle(Client* client) = 0; + virtual void client_timeout(Client* client, uint32_t id); + virtual bool client_response(Client* client, uint32_t id, + std::unique_ptr response); + virtual bool client_response_header(Client* client, uint32_t id); + virtual bool client_response_content(Client* client, uint32_t id); + virtual bool client_response_footer(Client* client, uint32_t id); + virtual bool client_response_end(Client* client, uint32_t id); + void client_update_event(Client* client); + bool client_request(Client* client, uint32_t id, + std::unique_ptr request); + bool client_response_content(Client* client, uint32_t id, Buffer* out); + + size_t clients() const { return client_.size(); } + + std::shared_ptr logger_; + std::shared_ptr looper_; + std::shared_ptr runner_; + Handler* const handler_; + std::shared_ptr file_opener_; + +private: + void client_response_content_wait(Client* client, uint32_t response_id); + void client_response_open(Client* client, uint32_t response_id, + std::unique_ptr input); + + std::vector client_; + bool client_full_{false}; + size_t next_avail_client_{0}; + std::vector client_wait_; + double client_timeout_; +}; + +#endif // TRANSPORT_BASE_HH diff --git a/src/transport_fastcgi.cc b/src/transport_fastcgi.cc new file mode 100644 index 0000000..8a5c3cc --- /dev/null +++ b/src/transport_fastcgi.cc @@ -0,0 +1,707 @@ +#include "common.hh" + +#include "config.hh" +#include "fcgi_protocol.hh" +#include "http_protocol.hh" +#include "logger.hh" +#include "strutil.hh" +#include "transport_base.hh" +#include "transport_fastcgi.hh" + +#include +#include + +namespace { + +class FastCgiTransport : public TransportBase { +public: + FastCgiTransport(std::shared_ptr logger, + std::shared_ptr looper, + std::shared_ptr runner, + Transport::Handler* handler) + : TransportBase(logger, looper, runner, handler) { + } + + bool setup(Logger* logger, Config const* config) override { + if (!TransportBase::setup(logger, config)) + return false; + + auto max_requests = config->get("transport.fcgi.max_requests", 20); + if (!max_requests.has_value()) { + logger->err("transport.fcgi.max_requests is unknown value: '%s'", + config->get("transport.fcgi.max_requests", nullptr)); + return false; + } + if (max_requests.value() < 1) { + logger->err("transport.fcgi.max_requests must be > 0"); + return false; + } + + max_requests_ = max_requests.value(); + + extra_.resize(clients()); + for (auto& extra : extra_) { + extra.requests.resize(max_requests_); + } + + return true; + } + + bool client_handle(Client* client) override { + auto& extra = extra_[client->index_]; + + if (extra.close_connection) { + client->expect_in_ = 0; + if (client->out_->empty()) { + client_abort(client); + return false; + } + return true; + } + + // A fastcgi records can be quite small, handle as many as possible + // before returning. + while (true) { + if (!extra.record) { + extra.record = fcgi::Record::parse(client->in_.get()); + if (!extra.record) { + // Need more input + size_t avail; + client->in_->rbuf(8, avail); + assert(avail < 8); // if avail >= 8 parse should not return nullptr + client->expect_in_ = avail >= 8 ? 1 : 8 - avail; + if (client->in_closed_) { + if (client->out_->empty()) { + client_abort(client); + return false; + } + // Wait for output to be sent (or timeout, whichever is first). + } + return true; + } + if (!extra.record->good()) { + logger_->warn("Bad record sent by client: %zu", client->index_); + client_abort(client); + return false; + } + extra.content_offset = 0; + } + + if (extra.record->request_id() != 0 && + extra.record->type() != fcgi::RecordType::BeginRequest && + extra.request_map.count(extra.record->request_id()) == 0) { + logger_->dbg("Ignoring message for unknown request for client: %zu", + client->index_); + // Ignoring messages for unknown requests + if (!client_consume_content(client)) + return false; + } else { + switch (extra.record->type()) { + case fcgi::RecordType::GetValues: + if (extra.record->request_id() == 0) { + if (!client_handle_get_values(client)) + return false; + } else { + if (!client_handle_unknown(client)) + return false; + } + break; + case fcgi::RecordType::GetValuesResult: + case fcgi::RecordType::EndRequest: + case fcgi::RecordType::Stdout: + case fcgi::RecordType::Stderr: + logger_->warn("Client %zu sending application record type", + client->index_); + client_abort(client); + return false; + case fcgi::RecordType::BeginRequest: + if (extra.record->request_id() == 0) { + logger_->warn("Client %zu sending BeginRequest for 0", + client->index_); + client_abort(client); + return false; + } + if (!client_handle_begin_request(client)) + return false; + break; + case fcgi::RecordType::AbortRequest: + if (extra.record->request_id() == 0) { + logger_->warn("Client %zu sending AbortRequest for 0", + client->index_); + client_abort(client); + return false; + } + if (!client_handle_abort_request(client)) + return false; + break; + case fcgi::RecordType::Params: + if (extra.record->request_id() == 0) { + logger_->warn("Client %zu sending Params for 0", client->index_); + client_abort(client); + return false; + } + if (!client_handle_params(client)) + return false; + break; + case fcgi::RecordType::Stdin: + if (extra.record->request_id() == 0) { + logger_->warn("Client %zu sending Stdin for 0", client->index_); + client_abort(client); + return false; + } + if (!client_handle_stdin(client)) + return false; + break; + default: + if (!client_handle_unknown(client)) + return false; + } + } + + if (extra.record) { + // Need more data + return true; + } + } + } + + void client_new(Client* client) override { + TransportBase::client_new(client); + + auto& extra = extra_[client->index_]; + extra.request_map.clear(); + extra.record.reset(); + extra.stream.reset(); + extra.pair.reset(); + extra.content_offset = 0; + extra.values.clear(); + extra.close_connection = false; + + for (auto& request : extra.requests) { + request.active = false; + request.stream.reset(); + request.pair.reset(); + request.params.clear(); + } + } + +private: + class StdoutBuffer : public Buffer { + public: + void use(uint32_t request_id, Buffer* out) { + assert(!ptr_); + id_ = request_id; + out_ = out; + good_ = true; + } + + bool good() const { + return good_; + } + + bool full() const override { + assert(out_); + return out_->full(); + } + + void clear() override { + assert(false); + } + + char* wbuf(size_t request, size_t& avail) override { + assert(!ptr_); + assert(out_); + auto ptr = out_->wbuf(request + 16, avail); + if (avail >= 16) { + avail -= 16; + if (avail > std::numeric_limits::max()) + avail = std::numeric_limits::max(); + ptr_ = ptr; + return ptr + 8; + } else { + avail = 0; + ptr_ = nullptr; + return nullptr; + } + } + + void wcommit(size_t bytes) override { + assert(out_); + if (bytes > 0) { + assert(ptr_); + assert(bytes <= std::numeric_limits::max()); + auto builder = fcgi::RecordBuilder::create(fcgi::RecordType::Stdout, + id_, bytes); + bool result = builder->build(ptr_, 8) && + builder->padding(ptr_ + 8 + bytes, 8); + if (!result) + good_ = false; + out_->wcommit(builder->size()); + } + ptr_ = nullptr; + } + + bool empty() const override { + assert(out_); + return out_->empty(); + } + + char const* rbuf(size_t, size_t& avail) override { + assert(false); + avail = 0; + return nullptr; + } + + void rcommit(size_t) override { + assert(false); + } + + private: + uint32_t id_ = 0; + Buffer* out_ = nullptr; + char* ptr_ = nullptr; + bool good_ = true; + }; + + class FastCgiRequest : public UrlRequest { + public: + explicit FastCgiRequest(std::vector> + params) + : params_(std::move(params)) { + } + + std::string_view method() const override { + return find("REQUEST_METHOD", "GET"); + } + + std::string_view url() const override { + return find("REQUEST_URI", "/"); + } + + std::optional header_one( + std::string_view name) const override { + for (auto const& pair : params_) { + if (lower_case_equal(pair.first, name)) + return pair.second; + } + return std::nullopt; + } + + std::vector header_all(std::string_view name) const override { + std::vector ret; + for (auto const& pair : params_) { + if (lower_case_equal(pair.first, name)) { + auto tmp = str::split(pair.second, ','); + for (auto str : tmp) + ret.push_back(std::string(str::trim(str))); + } + } + return ret; + } + + private: + static bool lower_case_equal(std::string_view a, std::string_view b) { + if (a.size() != b.size()) + return false; + for (size_t i = 0; i < a.size(); ++i) { + if (lower_case(a[i]) != lower_case(b[i])) + return false; + } + return true; + } + + static bool lower_case(char c) { + return (c >= 'A' && c <= 'Z') ? (c | 0x20) : c; + } + + std::string_view find(std::string_view name, + std::string_view fallback) const { + for (auto const& pair : params_) { + if (pair.first == name) + return pair.second; + } + return fallback; + } + + std::vector> params_; + }; + + struct Request { + bool keep_conn{false}; + bool active{false}; + std::unique_ptr stream; + std::unique_ptr pair; + bool add_to_stream; + std::vector> params; + std::unique_ptr stdin; + bool add_to_stdin; + std::optional app_state; + }; + + struct Extra { + std::unordered_map request_map; + std::vector requests; + std::unique_ptr record; + std::unique_ptr stream; + std::unique_ptr pair; + size_t content_offset; + std::vector values; + bool close_connection; + }; + + bool client_handle_unknown(Client* client) { + auto& extra = extra_[client->index_]; + if (extra.record->request_id() == 0) { + if (extra.content_offset == 0) + logger_->dbg("Client %zu got unknown maintainence record.", + client->index_); + auto type = extra.record->type(); + if (!client_consume_content(client)) + return false; + if (extra.record) + return true; + auto builder = fcgi::RecordBuilder::create_unknown_type(type); + return client_send(client, std::move(builder)); + } else { + if (extra.content_offset == 0) + logger_->info("Client %zu got unknown record type for request: %u", + client->index_, extra.record->type()); + return client_consume_content(client); + } + } + + bool client_send(Client* client, + std::unique_ptr builder) { + if (!builder->build(client->out_.get())) { + logger_->warn("Client output buffer full: %zu", client->index_); + client_abort(client); + return false; + } + return client_flush(client); + } + + bool client_handle_get_values(Client* client) { + auto& extra = extra_[client->index_]; + assert(extra.record); + assert(extra.record->type() == fcgi::RecordType::GetValues); + bool need_more; + if (!extra.stream) + extra.stream = fcgi::RecordStream::create_single(extra.record.get()); + if (!extra.pair) { + extra.pair = fcgi::Pair::start(extra.stream.get(), client->in_.get()); + need_more = !extra.pair; + } else { + need_more = !extra.pair->next(extra.stream.get(), client->in_.get()); + } + while (!need_more) { + if (!extra.pair->good()) { + logger_->warn("Client %zu sent invalid GetValues", client->index_); + client_abort(client); + return false; + } + extra.values.push_back(extra.pair->name()); + need_more = !extra.pair->next(extra.stream.get(), client->in_.get()); + } + if (extra.stream->end_of_stream()) + return client_handle_end_of_get_values(client); + // Need more data + return true; + } + + bool client_handle_end_of_get_values(Client* client) { + auto& extra = extra_[client->index_]; + extra.pair.reset(); + extra.stream.reset(); + extra.record.reset(); + auto pair_builder = fcgi::PairBuilder::create(); + for (auto& value : extra.values) { + if (value == "FCGI_MAX_CONNS") { + pair_builder->add(std::move(value), std::to_string(clients())); + } else if (value == "FCGI_MAX_REQS") { + pair_builder->add(std::move(value), std::to_string(max_requests_)); + } else if (value == "FCGI_MPXS_CONNS") { + pair_builder->add(std::move(value), "1"); + } else { + logger_->dbg("Unknown value `%s` ignored by client: %zu", + value.c_str(), client->index_); + } + } + extra.values.clear(); + auto builder = + fcgi::RecordBuilder::create(fcgi::RecordType::GetValuesResult, + 0, + pair_builder->size()); + if (!builder->build(client->out_.get()) || + !pair_builder->build(client->out_.get()) || + !builder->padding(client->out_.get())) { + logger_->warn("Client output buffer full: %zu", client->index_); + client_abort(client); + return false; + } + return client_flush(client); + } + + bool client_handle_begin_request(Client* client) { + auto& extra = extra_[client->index_]; + assert(extra.record); + assert(extra.record->type() == fcgi::RecordType::BeginRequest); + auto body = fcgi::BeginRequestBody::parse(extra.record.get(), + client->in_.get()); + if (!body) + return true; // Need more data + if (!body->good()) { + logger_->warn("Client sent invalid begin request: %zu", client->index_); + client_abort(client); + return false; + } + auto request_id = extra.record->request_id(); + extra.record.reset(); + if (body->role() != fcgi::Role::Responder) { + return client_send(client, + fcgi::RecordBuilder::create_end_request( + request_id, 1, + fcgi::ProtocolStatus::UnknownRole)); + } + size_t i = 0; + for (; i < max_requests_; ++i) + if (!extra.requests[i].active) + break; + if (i == max_requests_) { + return client_send(client, + fcgi::RecordBuilder::create_end_request( + request_id, 1, + fcgi::ProtocolStatus::Overloaded)); + } + auto ret = extra.request_map.emplace(request_id, i); + if (!ret.second) { + logger_->warn("Client sent double begin request: %zu", client->index_); + client_abort(client); + return false; + } + extra.requests[i].active = true; + extra.requests[i].keep_conn = body->flags() & fcgi::Flags::KeepConn; + return true; + } + + bool client_handle_abort_request(Client* client) { + auto& extra = extra_[client->index_]; + assert(extra.record); + assert(extra.record->type() == fcgi::RecordType::AbortRequest); + auto request_id = extra.record->request_id(); + if (!client_consume_content(client)) + return false; + if (extra.record) + return true; // Need more data + return client_end_request(client, request_id, 1); + } + + bool client_end_request(Client* client, uint16_t request_id, + uint32_t app_state) { + auto& extra = extra_[client->index_]; + auto it = extra.request_map.find(request_id); + if (it == extra.request_map.end()) { + assert(false); + return true; + } + auto& request = extra.requests[it->second]; + request.params.clear(); + if (!request.stdin || !request.stdin->end_of_stream()) { + request.app_state = app_state; + return true; + } + auto keep_conn = request.keep_conn; + request.active = false; + request.app_state.reset(); + request.stream.reset(); + request.stdin.reset(); + request.pair.reset(); + extra.request_map.erase(it); + if (!client_send(client, + fcgi::RecordBuilder::create_end_request( + request_id, app_state, + fcgi::ProtocolStatus::RequestComplete))) + return false; + if (keep_conn) + return true; + + // TODO: Should this check that no other requests are active? + extra.close_connection = true; + client->expect_in_ = 0; + if (client->out_->empty()) { + client_abort(client); + return false; + } + return true; + } + + bool client_handle_params(Client* client) { + auto& extra = extra_[client->index_]; + assert(extra.record); + assert(extra.record->type() == fcgi::RecordType::Params); + auto& request = extra.requests.at( + extra.request_map.at(extra.record->request_id())); + if (!request.stream) { + request.stream = fcgi::RecordStream::create_stream(extra.record.get()); + request.add_to_stream = false; + } else { + if (request.add_to_stream) { + request.stream->add(extra.record.get()); + request.add_to_stream = false; + } + } + bool need_more; + if (!request.pair) { + request.pair = fcgi::Pair::start(request.stream.get(), client->in_.get()); + need_more = !request.pair; + } else { + need_more = !request.pair->next(request.stream.get(), client->in_.get()); + } + while (!need_more) { + if (!request.pair->good()) { + logger_->warn("Client %zu sending invalid params", client->index_); + client_abort(client); + return false; + } + request.params.emplace_back(request.pair->name(), request.pair->value()); + need_more = !request.pair->next(request.stream.get(), client->in_.get()); + } + if (request.stream->end_of_stream()) { + auto id = extra.record->request_id(); + extra.record.reset(); + request.stream.reset(); + request.pair.reset(); + return TransportBase::client_request( + client, id, + std::make_unique(std::move(request.params))); + } + if (request.stream->end_of_record()) { + extra.record.reset(); + request.add_to_stream = true; + } + return true; + } + + bool client_handle_stdin(Client* client) { + auto& extra = extra_[client->index_]; + assert(extra.record); + assert(extra.record->type() == fcgi::RecordType::Stdin); + auto& request = extra.requests.at( + extra.request_map.at(extra.record->request_id())); + if (!request.stdin) { + request.stdin = fcgi::RecordStream::create_stream(extra.record.get()); + request.add_to_stdin = false; + } else { + if (request.add_to_stdin) { + request.stdin->add(extra.record.get()); + request.add_to_stdin = false; + } + } + // Just consume the stdin stream data, it isn't used for anything + while (true) { + size_t avail; + request.stdin->rbuf(client->in_.get(), 1, avail); + if (avail == 0) + break; + request.stdin->rcommit(client->in_.get(), avail); + } + if (request.stdin->end_of_stream()) { + auto id = extra.record->request_id(); + extra.record.reset(); + if (request.app_state) + return client_end_request(client, id, *request.app_state); + } else if (request.stdin->end_of_record()) { + extra.record.reset(); + request.add_to_stdin = true; + } + return true; + } + + bool client_consume_content(Client* client) { + auto& extra = extra_[client->index_]; + assert(extra.record); + size_t content_size = static_cast(extra.record->content_length()) + + extra.record->padding_length(); + if (extra.content_offset >= content_size) { + extra.record.reset(); + return true; + } + size_t need = content_size - extra.content_offset; + size_t avail; + client->in_->rbuf(need, avail); + if (avail >= need) { + client->in_->rcommit(need); + extra.record.reset(); + return true; + } + extra.content_offset += avail; + client->in_->rcommit(avail); + return true; + } + + bool client_response_header(Client* client, uint32_t id) override { + assert(client->responses_.count(id)); + auto& cli_response = client->responses_[id]; + auto content_builder = + CgiResponseBuilder::create(cli_response.response_->code()); + for (auto const& pair : cli_response.response_->headers()) + content_builder->add_header(pair.first, pair.second); + auto builder = fcgi::RecordBuilder::create(fcgi::RecordType::Stdout, + id, content_builder->size()); + if (!builder->build(client->out_.get()) || + !content_builder->build(client->out_.get()) || + !builder->padding(client->out_.get())) { + logger_->warn("Output buffer full for client: %zu", client->index_); + client_abort(client); + return false; + } + return client_flush(client); + } + + bool client_response_content(Client* client, uint32_t id) override { + stdout_.use(id, client->out_.get()); + if (!TransportBase::client_response_content(client, id, &stdout_)) + return false; + return stdout_.good(); + } + + bool client_response_footer(Client* client, uint32_t id) override { + assert(client->responses_.count(id)); + auto builder = fcgi::RecordBuilder::create(fcgi::RecordType::Stdout, id, + std::string()); + if (!builder->build(client->out_.get())) { + logger_->warn("Output buffer full for client: %zu", client->index_); + client_abort(client); + return false; + } + if (!client_flush(client)) + return false; + return client_end_request(client, id, 0); + } + + std::vector extra_; + size_t max_requests_{0}; + StdoutBuffer stdout_; +}; + +class FastCgiFactory : public Transport::Factory { +public: + std::unique_ptr create( + std::shared_ptr logger, + std::shared_ptr looper, + std::shared_ptr runner, + Logger* config_logger, + Config const* config, + Transport::Handler* handler) { + auto transport = + std::make_unique(logger, looper, runner, handler); + if (transport->setup(config_logger, config)) + return transport; + return nullptr; + } +}; + +} // namespace + +std::unique_ptr create_transport_factory_fastcgi() { + return std::make_unique(); +} diff --git a/src/transport_fastcgi.hh b/src/transport_fastcgi.hh new file mode 100644 index 0000000..d574bd5 --- /dev/null +++ b/src/transport_fastcgi.hh @@ -0,0 +1,8 @@ +#ifndef TRANSPORT_FASTCGI_HH +#define TRANSPORT_FASTCGI_HH + +#include "transport.hh" + +std::unique_ptr create_transport_factory_fastcgi(); + +#endif // TRANSPORT_FASTCGI_HH diff --git a/src/transport_http.cc b/src/transport_http.cc new file mode 100644 index 0000000..98e7fed --- /dev/null +++ b/src/transport_http.cc @@ -0,0 +1,193 @@ +#include "common.hh" + +#include "http_protocol.hh" +#include "logger.hh" +#include "strutil.hh" +#include "transport_base.hh" +#include "transport_http.hh" + +namespace { + +class HttpTransport : public TransportBase { +public: + HttpTransport(std::shared_ptr logger, + std::shared_ptr looper, + std::shared_ptr runner, + Transport::Handler* handler) + : TransportBase(logger, looper, runner, handler) { + } + + bool setup(Logger* logger, Config const* config) override { + if (!TransportBase::setup(logger, config)) + return false; + + extra_.resize(clients()); + return true; + } + + bool client_handle(Client* client) override { + auto& extra = extra_[client->index_]; + auto req = HttpRequest::parse(client->in_.get()); + if (!req) { + client->expect_in_ = 1; // Don't know how big the request will be + if (client->in_closed_ || extra.close_connection_) { + if (client->out_->empty()) { + client_abort(client); + return false; + } + // Wait for output to be sent (or timeout, whichever is first). + } + return true; + } + + if (req->good()) { + if (supported_request(req.get())) { + extra.close_connection_ = close_connection(req.get()); + extra.version_ = req->proto_version(); + // Stop reading in_ buffer until request is handled. + client->expect_in_ = 0; + return TransportBase::client_request( + client, 0, std::make_unique(std::move(req))); + } else { + return client_fatal_response(client, 505); + } + } else { + return client_fatal_response(client, 400); + } + } + + static bool supported_request(HttpRequest const* request) { + if (request->proto() != "HTTP") + return false; + auto version = request->proto_version(); + return version.major == 1; + } + + static bool close_connection(HttpRequest const* request) { + if (request->proto_version().major == 1 && + request->proto_version().minor == 1) { + return request->first_header("connection") == "close"; + } + return true; + } + + void client_new(Client* client) override { + TransportBase::client_new(client); + + auto& extra = extra_[client->index_]; + extra.close_connection_ = true; + extra.version_.major = 1; + extra.version_.minor = 0; + } + + bool client_fatal_response(Client* client, uint16_t status_code) { + auto& extra = extra_[client->index_]; + extra.close_connection_ = true; + client->expect_in_ = 0; + client->in_->clear(); + return client_response(client, 0, create_data(status_code, "")); + } + + bool client_response_header(Client* client, uint32_t id) override { + auto& extra = extra_[client->index_]; + assert(client->responses_.count(id)); + auto& cli_response = client->responses_[id]; + auto status_code = cli_response.response_->code(); + auto builder = HttpResponseBuilder::create( + "HTTP", extra.version_, + status_code, + std::string(http_standard_message(status_code))); + bool have_content_length = false; + for (auto const& pair : cli_response.response_->headers()) { + if (!have_content_length && pair.first == "Content-Length") + have_content_length = true; + builder->add_header(pair.first, pair.second); + } + if (!have_content_length) + extra.close_connection_ = true; + if (extra.close_connection_) + builder->add_header("Connection", "close"); + if (!builder->build(client->out_.get())) { + logger_->warn("Output buffer full for client: %zu", client->index_); + client_abort(client); + return false; + } + return client_flush(client); + } + + bool client_response_footer(Client* client, uint32_t) override { + auto const& extra = extra_[client->index_]; + if (extra.close_connection_ && client->out_->empty()) { + client_abort(client); + return false; + } + return true; + } + +private: + class WrapRequest : public UrlRequest { + public: + explicit WrapRequest(std::unique_ptr req) + : req_(std::move(req)) {} + + std::string_view method() const override { + return req_->method(); + } + + std::string_view url() const override { + return req_->url(); + } + + std::optional header_one( + std::string_view name) const override { + auto it = req_->header(name); + if (it->valid()) + return it->value(); + return std::nullopt; + } + + std::vector header_all( + std::string_view name) const override { + std::vector ret; + for (auto it = req_->header(name); it->valid(); it->next()) { + auto tmp = str::split(it->value(), ','); + for (auto str : tmp) + ret.push_back(std::string(str::trim(str))); + } + return ret; + } + + private: + std::unique_ptr req_; + }; + + struct Extra { + bool close_connection_; + Version version_; + }; + + std::vector extra_; +}; + +class HttpFactory : public Transport::Factory { +public: + std::unique_ptr create( + std::shared_ptr logger, + std::shared_ptr looper, + std::shared_ptr runner, + Logger* config_logger, + Config const* config, + Transport::Handler* handler) { + auto transport = std::make_unique( + logger, looper, runner, handler); + if (transport->setup(config_logger, config)) + return transport; + return nullptr; + } +}; + +} // namespace + +std::unique_ptr create_transport_factory_http() { + return std::make_unique(); +} diff --git a/src/transport_http.hh b/src/transport_http.hh new file mode 100644 index 0000000..386a67b --- /dev/null +++ b/src/transport_http.hh @@ -0,0 +1,8 @@ +#ifndef TRANSPORT_HTTP_HH +#define TRANSPORT_HTTP_HH + +#include "transport.hh" + +std::unique_ptr create_transport_factory_http(); + +#endif // TRANSPORT_HTTP_HH diff --git a/src/travel.cc b/src/travel.cc new file mode 100644 index 0000000..f8adf50 --- /dev/null +++ b/src/travel.cc @@ -0,0 +1,556 @@ +#include "common.hh" + +#include "config.hh" +#include "files_finder.hh" +#include "image.hh" +#include "logger.hh" +#include "rotation.hh" +#include "strutil.hh" +#include "task_runner.hh" +#include "timezone.hh" +#include "travel.hh" +#include "video.hh" +#include "weak_ptr.hh" + +#include +#include +#include +#include +#include + +namespace { + +class TravelImpl : public Travel, public FilesFinder::Delegate { +public: + TravelImpl(std::shared_ptr logger, + std::shared_ptr runner) + : logger_(std::move(logger)), runner_(std::move(runner)), + weak_ptr_owner_(this) {} + + bool setup(Logger* logger, Config* config) override { + root_ = config->get_path("site.root", ""); + if (root_.empty()) { + logger->err("site.root must be set to the root directory for travels."); + return false; + } + auto travel_files_threads = config->get("workers", 4); + if (!travel_files_threads.has_value()) { + logger->err("workers is unknown value: '%s'", + config->get("workers", nullptr)); + return false; + } + if (travel_files_threads.value() <= 0) { + logger->err("workers must be > 0"); + return false; + } + timezone_ = Timezone::create(logger_, + config->get_path("geojson.database", ""), + config->get_path("zoneinfo.root", + "/usr/share/zoneinfo")); + worker_threads_ = travel_files_threads.value(); + return true; + } + + void start() override { + workers_ = TaskRunner::create(worker_threads_); + finder_ = FilesFinder::create(logger_, workers_, root_, this, + worker_threads_); + } + + void reload() override { + // Make implementation simpler by ignoring calls to reload() while + // previoys start() or reload() is still in progress. + if (finder_) { + logger_->warn("Reload called while still loading, ignored."); + return; + } + + workers_.reset(); + trips_.clear(); + trip_index_.clear(); + + start(); + } + + size_t trips() const override { + return trips_.size(); + } + + Trip const& trip(size_t i) const override { + assert(i < trips_.size()); + return trips_[i]; + } + + void call_when_loaded(std::function callback) override { + if (finder_) { + call_when_loaded_.push_back(std::move(callback)); + } else { + callback(); + } + } + + // Called on any thread. + bool include_dir(std::string_view name, uint16_t depth) const override { + if (depth > 0) + return FilesFinder::Delegate::include_dir(name, depth); + return valid_trip_id(name); + } + + // Runs on workers + void file(std::filesystem::path path) override { + std::string media_id; + std::string trip_id = get_trip_id(path, &media_id); + if (trip_id.empty()) { + logger_->warn("Ignoring %s because no trip id found", path.c_str()); + return; + } + auto image = get_image_info(media_id, path); + if (image) { + if (image.value().date_.empty()) { + logger_->dbg("Ignoring %s, image without a date.", path.c_str()); + return; + } + runner_->post(std::bind(&TravelImpl::weak_finder_image, + weak_ptr_owner_.get(), + trip_id, + image.value())); + return; + } + auto video = get_video_info(media_id, path, timezone_.get()); + if (video) { + if (video.value().date_.empty()) { + logger_->dbg("Ignoring %s, video without a date.", path.c_str()); + return; + } + runner_->post(std::bind(&TravelImpl::weak_finder_video, + weak_ptr_owner_.get(), + trip_id, + video.value())); + return; + } + logger_->dbg("Ignoring %s, not an image or a video.", path.c_str()); + } + + // Runs on workers + void done() override { + runner_->post(std::bind(&TravelImpl::weak_finder_done, + weak_ptr_owner_.get())); + } + +private: + struct Info { + std::string id_; + std::filesystem::path path_; + uint64_t width_; + uint64_t height_; + Location location_; + Date date_; + + Info(std::string id, std::filesystem::path path, + uint64_t width, uint64_t height, Location location, Date date) + : id_(std::move(id)), path_(std::move(path)), + width_(width), height_(height), + location_(location), date_(date) {} + }; + + struct ImageInfo : Info { + Rotation rotation_; + std::string thumbnail_mime_type_; + uint64_t thumbnail_size_; + + ImageInfo(std::string id, std::filesystem::path path, + uint64_t width, uint64_t height, + Location location, Date date, Rotation rotation, + std::string thumbnail_mime_type, uint64_t thumbnail_size) + : Info(std::move(id), std::move(path), width, height, location, + date), rotation_(rotation), + thumbnail_mime_type_(std::move(thumbnail_mime_type)), + thumbnail_size_(thumbnail_size) {} + }; + + struct VideoInfo : Info { + double length_; + + VideoInfo(std::string id, std::filesystem::path path, + uint64_t width, uint64_t height, + Location location, Date date, double length) + : Info(std::move(id), std::move(path), width, height, location, + date), length_(length) {} + }; + + class MediaImpl : public virtual Media, public virtual Thumbnail { + public: + explicit MediaImpl(ImageInfo info) + : image_(std::move(info)) {} + + explicit MediaImpl(VideoInfo info) + : video_(std::move(info)) {} + + std::string_view id() const override { + return image_ ? image_->id_ : video_->id_; + } + + std::filesystem::path const& path() const override { + return image_ ? image_->path_ : video_->path_; + } + + Type type() const override { + return image_ ? Type::IMAGE : Type::VIDEO; + } + + uint64_t width() const override { + return image_ ? image_->width_ : video_->width_; + } + uint64_t height() const override { + return image_ ? image_->height_ : video_->height_; + } + + Location location() const override { + return image_ ? image_->location_ : video_->location_; + } + + Date date() const override { + return image_ ? image_->date_ : video_->date_; + } + + double length() const override { + return image_ ? 0.0 : video_->length_; + } + + Rotation rotation() const override { + return image_ ? image_->rotation_ : Rotation::UNKNOWN; + } + + Thumbnail const* thumbnail() const override { + return image_ && !image_->thumbnail_mime_type_.empty() && + image_->thumbnail_size_ > 0 ? this : nullptr; + } + + ThumbType thumb_type() const override { + return ThumbType::EXIF; + } + + std::string_view mime_type() const override { + return image_->thumbnail_mime_type_; + } + + uint64_t size() const override { + return image_->thumbnail_size_; + } + + private: + std::optional image_; + std::optional video_; + }; + + class DayImpl : public Day { + public: + DayImpl(Date day, size_t first) + : day_(day), first_(first), last_(first) { + } + + Date date() const override { + return day_; + } + + size_t first() const override { + return first_; + } + + size_t last() const override { + return last_; + } + + void increment_last() { + ++last_; + } + + private: + Date const day_; + size_t const first_; + size_t last_; + }; + + class TripImpl : public Trip { + public: + TripImpl(std::string id, std::string name, uint16_t year) + : id_(std::move(id)), name_(std::move(name)), year_(year) {} + + std::string_view id() const override { + return id_; + } + + std::string_view title() const override { + return name_; + } + + uint16_t year() const override { + return year_; + } + + Location location() const override { + return location_; + } + + uint64_t images() const override { + return images_; + } + + uint64_t videos() const override { + return videos_; + } + + size_t media_count() const override { + return media_.size(); + } + + Media const& media(size_t i) const override { + assert(i < media_.size()); + return media_[i]; + } + + size_t day_count() const override { + return day_.size(); + } + + Day const& day(size_t i) const override { + assert(i < day_.size()); + return day_[i]; + } + + void add_image(ImageInfo info) { + ++images_; + media_.emplace_back(std::move(info)); + } + + void add_video(VideoInfo info) { + ++videos_; + media_.emplace_back(std::move(info)); + } + + void sort_media() { + std::sort(media_.begin(), media_.end(), + [] (MediaImpl const& a, MediaImpl const& b) { + return a.date() < b.date(); + }); + } + + void setup_days() { + for (size_t i = 0; i < media_.size(); ++i) { + auto day = media_[i].date().day(); + if (day_.empty() || day != day_.back().date()) { + assert(day_.empty() || day > day_.back().date()); + day_.emplace_back(day, i); + } else { + assert(i == day_.back().last() + 1); + day_.back().increment_last(); + } + } + } + + void set_location(Location location) { + location_ = location; + } + + private: + std::string id_; + std::string name_; + uint16_t year_; + Location location_; + uint64_t images_{0}; + uint64_t videos_{0}; + std::vector media_; + std::vector day_; + }; + + // Called from workers + std::string get_trip_id(std::filesystem::path const& path, + std::string* out_child_id) const { + std::string child_id; + std::filesystem::path tmp = path; + while (true) { + if (!tmp.has_parent_path()) + break; + auto parent = tmp.parent_path(); + if (parent == root_) { + if (out_child_id) + *out_child_id = std::move(child_id); + return tmp.filename(); + } + if (!child_id.empty()) + child_id.insert(0, "/"); + child_id.insert(0, tmp.filename()); + tmp = parent; + } + return std::string(); + } + + static void weak_finder_image(std::shared_ptr> weak_ptr, + std::string const& trip_id, + ImageInfo const& info) { + auto* ptr = weak_ptr->get(); + if (ptr) + ptr->finder_image(trip_id, info); + } + + static void weak_finder_video(std::shared_ptr> weak_ptr, + std::string const& trip_id, + VideoInfo const& info) { + auto* ptr = weak_ptr->get(); + if (ptr) + ptr->finder_video(trip_id, info); + } + + static void weak_finder_done(std::shared_ptr> weak_ptr) { + auto* ptr = weak_ptr->get(); + if (ptr) + ptr->finder_done(); + } + + TripImpl* get_trip(std::string const& trip_id) { + auto it = trip_index_.find(trip_id); + if (it != trip_index_.end()) + return &trips_[it->second]; + + std::string name; + uint16_t year; + if (parse_trip_id(trip_id, &name, &year)) { + auto index = trips_.size(); + trips_.emplace_back(trip_id, std::move(name), year); + trip_index_[trip_id] = index; + return &trips_[index]; + } else { + // include_dir should make sure of this doesn't happen. + assert(false); + return nullptr; + } + } + + void finder_image(std::string const& trip_id, + ImageInfo const& info) { + auto* trip_ptr = get_trip(trip_id); + if (trip_ptr) + trip_ptr->add_image(info); + } + + void finder_video(std::string const& trip_id, + VideoInfo const& info) { + auto* trip_ptr = get_trip(trip_id); + if (trip_ptr) + trip_ptr->add_video(info); + } + + void finder_done() { + finder_.reset(); + + for (auto& trip_impl : trips_) + cleanup_trip(trip_impl); + + logger_->info("Trips all loaded."); + + for (auto& callback : call_when_loaded_) { + callback(); + } + call_when_loaded_.clear(); + } + + void cleanup_trip(TripImpl& trip_impl) { + // Sort by date + trip_impl.sort_media(); + + trip_impl.setup_days(); + + // TODO: Remove outliers + // TODO: Use some weighted median instead of average + Location loc; + size_t count = 0; + for (size_t i = 0; i < trip_impl.media_count(); ++i) { + auto& media = trip_impl.media(i); + if (media.location().empty()) + continue; + if (loc.empty()) { + loc = media.location(); + count = 1; + } else { + loc.lat += media.location().lat; + loc.lng += media.location().lng; + ++count; + } + } + loc.lat /= count; + loc.lng /= count; + trip_impl.set_location(loc); + } + + static std::optional get_image_info(std::string id, + std::filesystem::path path) { + auto image = Image::load(path); + if (image) { + auto* thumb = image->thumbnail(); + return ImageInfo(std::move(id), std::move(path), + image->width(), image->height(), + image->location(), image->date(), image->rotation(), + thumb ? std::string(thumb->mime_type()) : std::string(), + thumb ? thumb->size() : 0); + } + return std::nullopt; + } + + static std::optional get_video_info(std::string id, + std::filesystem::path path, + Timezone const* timezone) { + auto video = Video::load(path, timezone); + if (video) + return VideoInfo(std::move(id), std::move(path), + video->width(), video->height(), + video->location(), video->date(), video->length()); + return std::nullopt; + } + + static bool valid_trip_id(std::string_view id) { + return parse_trip_id(id, nullptr, nullptr); + } + + static bool parse_trip_id(std::string_view id, + std::string* name, uint16_t* year) { + auto it = id.find('-'); + if (it == std::string_view::npos) + return false; + if (name) + name->assign(str::trim(id.substr(0, it))); + auto tmp = str::parse_uint16(std::string(id, it + 1, std::string::npos)); + if (!tmp) + return false; + if (*tmp < 1950) + return false; + if (year) + *year = *tmp; + return true; + } + + std::shared_ptr logger_; + std::shared_ptr runner_; + std::unique_ptr timezone_; + size_t worker_threads_{0}; + std::filesystem::path root_; + std::vector trips_; + std::unordered_map trip_index_; + std::vector> call_when_loaded_; + + std::mutex worker_mutex_; + + // It is important that workers_ is (next to) last as it blocks leftover + // workers in destructor so should be destroyed first. + std::shared_ptr workers_; + // finder depends on workers so must be destroyed before. + std::unique_ptr finder_; + WeakPtrOwner weak_ptr_owner_; +}; + +} // namespace + +std::unique_ptr Travel::create(std::shared_ptr logger, + std::shared_ptr runner) { + return std::make_unique(std::move(logger), std::move(runner)); +} + diff --git a/src/travel.hh b/src/travel.hh new file mode 100644 index 0000000..c9cc891 --- /dev/null +++ b/src/travel.hh @@ -0,0 +1,137 @@ +#ifndef TRAVEL_HH +#define TRAVEL_HH + +#include "date.hh" +#include "location.hh" +#include "rotation.hh" + +#include +#include +#include +#include + +class Config; +class Logger; +class TaskRunner; + +class Travel { +public: + class Thumbnail { + public: + enum class ThumbType { + FILE, + EXIF, + }; + + virtual ~Thumbnail() = default; + + virtual std::filesystem::path const& path() const = 0; + + virtual ThumbType thumb_type() const = 0; + + virtual std::string_view mime_type() const = 0; + + virtual uint64_t size() const = 0; + + protected: + Thumbnail() = default; + }; + + class Media { + public: + enum class Type { + IMAGE, + VIDEO, + }; + + virtual ~Media() = default; + + virtual std::string_view id() const = 0; + + virtual std::filesystem::path const& path() const = 0; + + virtual Type type() const = 0; + + virtual uint64_t width() const = 0; + virtual uint64_t height() const = 0; + + virtual Location location() const = 0; + + virtual Date date() const = 0; + + // Only ever > 0.0 for videos + virtual double length() const = 0; + + // Only ever != UNKNOWN for images + virtual Rotation rotation() const = 0; + + // Returns nullptr if no thumbnail + virtual Thumbnail const* thumbnail() const = 0; + + protected: + Media() = default; + }; + + class Day { + public: + virtual ~Day() = default; + + // Returns 00:00:00 of the day in question. + virtual Date date() const = 0; + + // Index in media array for first media for this day. + virtual size_t first() const = 0; + + // Index in media array for the last media for this day. + // Days can't be empty, if there is just one media of the day then + // first() == last(). + virtual size_t last() const = 0; + + protected: + Day() = default; + }; + + class Trip { + public: + virtual ~Trip() = default; + + virtual std::string_view id() const = 0; + virtual std::string_view title() const = 0; + virtual uint16_t year() const = 0; + virtual Location location() const = 0; + virtual uint64_t images() const = 0; + virtual uint64_t videos() const = 0; + + virtual size_t media_count() const = 0; + virtual Media const& media(size_t i) const = 0; + + virtual size_t day_count() const = 0; + virtual Day const& day(size_t i) const = 0; + + protected: + Trip() = default; + }; + + virtual ~Travel() = default; + + static std::unique_ptr create(std::shared_ptr logger, + std::shared_ptr runner); + + virtual bool setup(Logger* logger, Config* config) = 0; + + virtual void start() = 0; + + virtual void reload() = 0; + + virtual void call_when_loaded(std::function callback) = 0; + + virtual size_t trips() const = 0; + virtual Trip const& trip(size_t i) const = 0; + +protected: + Travel() = default; + Travel(Travel const&) = delete; + Travel& operator=(Travel const&) = delete; +}; + +#endif // TRAVEL_HH diff --git a/src/tz_info.cc b/src/tz_info.cc new file mode 100644 index 0000000..7c3361e --- /dev/null +++ b/src/tz_info.cc @@ -0,0 +1,329 @@ +#include "common.hh" + +#include "io.hh" +#include "logger.hh" +#include "tz_info.hh" +#include "tz_str.hh" + +#include +#include +#include +#include +#include + +namespace { + +struct Header { + char magic[4]; + char ver; + char unused[15]; + uint32_t isutcnt; + uint32_t isstdcnt; + uint32_t leapcnt; + uint32_t timecnt; + uint32_t typecnt; + uint32_t charcnt; +}; + +#if !HAVE_ATTRIBUTE_PACKED +#pragma pack(push) +#endif +struct +#if HAVE_ATTRIBUTE_PACKED +__attribute__((packed)) +#endif +LocalTimeType { + int32_t utoff; + uint8_t dst; + uint8_t idx; +}; + +struct +#if HAVE_ATTRIBUTE_PACKED +__attribute__((packed)) +#endif +LeapSecond { + int64_t occur; + int32_t corr; +}; +#if !HAVE_ATTRIBUTE_PACKED +#pragma pack(pop) +#endif + +struct Data { + std::vector transition_times; + std::vector transition_types; + std::vector local_time_type_records; + std::vector time_zone_designations; + std::vector leap_second_records; + std::vector standard_or_wall_indicators; + std::vector ut_local_indicators; +}; + +inline uint32_t ntoh(uint32_t value) { +#ifdef WORDS_BIGENDIAN + return value; +#else + return bswap_32(value); +#endif +} + +inline int32_t ntoh(int32_t value) { +#ifdef WORDS_BIGENDIAN + return value; +#else + return bswap_32(value); +#endif +} + +inline int64_t ntoh(int64_t value) { +#ifdef WORDS_BIGENDIAN + return value; +#else + return bswap_64(value); +#endif +} + +class TzInfoImpl : public TzInfo { +public: + TzInfoImpl(std::shared_ptr logger, + std::filesystem::path tzinfo_dir) + : logger_(std::move(logger)), base_(std::move(tzinfo_dir)) { + static_assert(sizeof(Header) == 44, "Header must be packed"); + } + + std::optional get_local_time(std::string_view tzname, + time_t utc_time) const override { + if (!base_.empty()) { + std::filesystem::path zone = base_ / tzname; + auto fd = io::open(zone, io::open_flags::rdonly); + if (fd) { + return read(zone, fd.get(), utc_time); + } else { + logger_->warn("Unable to open %s for reading: %s.", zone.c_str(), + strerror(errno)); + } + } + return std::nullopt; + } + +private: + std::optional read(std::filesystem::path const& zone, int fd, + time_t utc_time) const { + Header header; + if (!io::read_all(fd, &header, sizeof(header))) { + logger_->warn("%s: Error reading: %s.", zone.c_str(), + strerror(errno)); + return std::nullopt; + } + if (!check_and_fix_header(header)) { + logger_->warn("%s: Not a TZinfo file.", zone.c_str()); + return std::nullopt; + } + // Skip V1, 32bit time stamps are just so 90's. + auto ret = lseek(fd, data_size(header, '\0'), SEEK_CUR); + if (ret == -1) { + logger_->warn("%s: Error seeking: %s.", zone.c_str(), strerror(errno)); + return std::nullopt; + } + if (!io::read_all(fd, &header, sizeof(header))) { + logger_->warn("%s: Error reading: %s.", zone.c_str(), + strerror(errno)); + return std::nullopt; + } + if (header.ver < '2' || !check_and_fix_header(header)) { + logger_->warn("%s: Not a TZinfo V2+ file.", zone.c_str()); + return std::nullopt; + } + Data data; + if (!read_data(fd, header, data)) { + logger_->warn("%s: Error reading: %s.", zone.c_str(), + strerror(errno)); + return std::nullopt; + } + std::string footer; + // Note that read_footer might read past the footer, which is currently + // fine (as it is a footer) but good to know for future developers. + if (!read_footer(fd, footer)) { + logger_->warn("%s: Error reading (4): %s.", zone.c_str(), + strerror(errno)); + return std::nullopt; + } + + return utc_to_local(data, footer, utc_time); + } + + static std::optional utc_to_local(Data const& data, + std::string const& footer, + time_t utc) { + if (data.transition_times.empty()) + return utc_to_local(footer, utc); + if (utc < data.transition_times.front()) + return std::nullopt; + size_t i = 0; + while (i < data.transition_times.size() && utc > data.transition_times[i]) + ++i; + + if (i == 0) + return std::nullopt; + if (i == data.transition_times.size() && !footer.empty()) + return utc_to_local(footer, utc); + --i; + + return utc_to_local(data.local_time_type_records[ + data.transition_types[i]], utc); + } + + static std::optional utc_to_local(LocalTimeType const& local_time, + time_t utc) { + return utc += local_time.utoff; + } + + static std::optional utc_to_local(std::string const& tz, time_t utc) { + return tz::get_local_time(tz, utc); + } + + static size_t data_size(Header const& header, char version) { + auto time_size = version >= '2' ? 8 : 4; + return header.timecnt * time_size + header.timecnt + header.typecnt * 6 + + header.charcnt + header.leapcnt * (time_size + 4) + header.isstdcnt + + header.isutcnt; + } + + static bool read_data(int fd, Header const& header, Data& data) { + assert(header.ver >= '2'); + data.transition_times.resize(header.timecnt); + data.transition_types.resize(header.timecnt); + static_assert(sizeof(LocalTimeType) == 6, "LocalTimeType must be packed"); + data.local_time_type_records.resize(header.typecnt); + data.time_zone_designations.resize(header.charcnt); + static_assert(sizeof(LeapSecond) == 12, "LeapSecond must be packed"); + data.leap_second_records.resize(header.leapcnt); + data.standard_or_wall_indicators.resize(header.isstdcnt); + data.ut_local_indicators.resize(header.isutcnt); + if (!io::read_all(fd, data.transition_times.data(), + header.timecnt * sizeof(int64_t)) || + !io::read_all(fd, data.transition_types.data(), header.timecnt) || + !io::read_all(fd, data.local_time_type_records.data(), + header.typecnt * sizeof(LocalTimeType)) || + !io::read_all(fd, data.time_zone_designations.data(), header.charcnt) || + !io::read_all(fd, data.leap_second_records.data(), + header.leapcnt * sizeof(LeapSecond)) || + !io::read_all(fd, data.standard_or_wall_indicators.data(), + header.isstdcnt) || + !io::read_all(fd, data.ut_local_indicators.data(), header.isutcnt)) + return false; + + { + int64_t last = std::numeric_limits::min(); + for (auto& time : data.transition_times) { + time = ntoh(time); + if (time <= last) + return false; + last = time; + } + } + + for (auto const& type : data.transition_types) + if (type >= header.typecnt) + return false; + + for (auto& local : data.local_time_type_records) { + local.utoff = ntoh(local.utoff); + if (local.utoff == -2147483648) + return false; + if (local.dst != 0 && local.dst != 1) + return false; + if (local.idx >= header.charcnt) + return false; + } + + if (data.time_zone_designations.empty() || + data.time_zone_designations.back() != '\0') + return false; + + { + LeapSecond last; + last.occur = -1; + last.corr = 0; + for (auto& leap : data.leap_second_records) { + leap.occur = ntoh(leap.occur); + leap.corr = ntoh(leap.corr); + if (last.occur == -1) { + if (leap.occur < 0) + return false; + } else { + if (leap.occur <= last.occur) + return false; + } + auto diff = last.corr - leap.corr; + if (diff != 1 && diff != -1) + return false; + last.occur = leap.occur + 2419199; + last.corr = leap.corr; + } + } + + for (auto const& standard_or_wall : data.standard_or_wall_indicators) + if (standard_or_wall != 1 && standard_or_wall != 0) + return false; + + for (auto const& ut_or_local : data.ut_local_indicators) + if (ut_or_local != 1 && ut_or_local != 0) + return false; + + return true; + } + + static bool read_footer(int fd, std::string& footer) { + size_t offset = 0; + footer.resize(128); + while (true) { + auto got = io::read(fd, footer.data() + offset, footer.size() - offset); + if (got <= 0) + return false; + if (footer.front() != '\n') + return false; + auto end = footer.substr(0, got).find('\n', std::max(1lu, offset)); + if (end != std::string::npos) { + footer = footer.substr(1, end - 1); + return true; + } + offset += got; + footer.resize(offset + 128); + } + } + + static bool check_and_fix_header(Header& header) { + if (memcmp(header.magic, "TZif", 4)) + return false; + header.isutcnt = ntoh(header.isutcnt); + header.isstdcnt = ntoh(header.isstdcnt); + header.leapcnt = ntoh(header.leapcnt); + header.timecnt = ntoh(header.timecnt); + header.typecnt = ntoh(header.typecnt); + header.charcnt = ntoh(header.charcnt); + + if (header.isutcnt != 0 && header.isutcnt != header.typecnt) + return false; + if (header.isstdcnt != 0 && header.isstdcnt != header.typecnt) + return false; + if (header.typecnt == 0) + return false; + if (header.charcnt == 0) + return false; + + return true; + } + + std::shared_ptr logger_; + std::filesystem::path base_; +}; + +} // namespace + +std::unique_ptr TzInfo::create(std::shared_ptr logger, + std::filesystem::path tzinfo_dir) { + return std::make_unique(std::move(logger), std::move(tzinfo_dir)); +} + diff --git a/src/tz_info.hh b/src/tz_info.hh new file mode 100644 index 0000000..eb44e58 --- /dev/null +++ b/src/tz_info.hh @@ -0,0 +1,28 @@ +#ifndef TZ_INFO_HH +#define TZ_INFO_HH + +#include +#include +#include +#include + +class Logger; + +class TzInfo { +public: + virtual ~TzInfo() = default; + + static std::unique_ptr create(std::shared_ptr logger, + std::filesystem::path tzinfo_dir); + + virtual std::optional get_local_time(std::string_view tzname, + time_t utc_time) const = 0; + + +protected: + TzInfo() = default; + TzInfo(TzInfo const&) = delete; + TzInfo& operator=(TzInfo const&) = delete; +}; + +#endif // TZ_INFO_HH diff --git a/src/tz_str.cc b/src/tz_str.cc new file mode 100644 index 0000000..207f15d --- /dev/null +++ b/src/tz_str.cc @@ -0,0 +1,265 @@ +#include "common.hh" + +#include "tz_str.hh" + +#include + +namespace tz { + +namespace { + +inline bool is_alpha(char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); +} + +inline bool is_digit(char c) { + return c >= '0' && c <= '9'; +} + +inline bool is_alphnumeric(char c) { + return is_alpha(c) || is_digit(c); +} + +std::optional read_abbr(std::string_view str, + size_t& offset) { + if (offset >= str.size()) + return std::nullopt; + auto const start = offset; + std::string_view ret; + if (str[offset] == '<') { + size_t end = str.find('>', offset + 1); + if (end == std::string_view::npos) + return std::nullopt; + offset = end + 1; + ret = str.substr(start + 1, end - (start + 1)); + for (auto const& c : ret) { + if (!(is_alphnumeric(c) || c == '+' || c == '-')) + return std::nullopt; + } + } else { + while (offset < str.size() && is_alpha(str[offset])) + ++offset; + ret = str.substr(start, offset - start); + } + if (ret.size() < 3) + return std::nullopt; + return ret; +} + +std::optional read_num(std::string_view str, + size_t& offset) { + if (offset >= str.size() || !is_digit(str[offset])) + return std::nullopt; + uint32_t ret = str[offset++] - '0'; + while (offset < str.size() && is_digit(str[offset])) { + auto value = ret * 10 + (str[offset++] - '0'); + if (value < ret) + return std::nullopt; + ret = value; + } + return ret; +} + +std::optional read_time(std::string_view str, + size_t& offset) { + auto hh = read_num(str, offset); + if (!hh) + return std::nullopt; + if (hh.value() > 24) + return std::nullopt; + time_t ret = hh.value() * 60 * 60; + if (offset < str.size() && str[offset] == ':') { + ++offset; + auto mm = read_num(str, offset); + if (!mm) + return std::nullopt; + if (mm.value() > 59) + return std::nullopt; + ret += mm.value() * 60; + if (offset < str.size() && str[offset] == ':') { + ++offset; + auto ss = read_num(str, offset); + if (!ss) + return std::nullopt; + if (ss.value() > 59) + return std::nullopt; + ret += ss.value(); + } + } + return ret; +} + +std::optional read_offset(std::string_view str, + size_t& offset) { + bool negative; + if (offset < str.size() && (str[offset] == '+' || str[offset] == '-')) + negative = str[offset++] == '+'; // Yes, this is correct. ('-' is east) + else + negative = true; // Yes, this is correct. (default is west) + auto ret = read_time(str, offset); + if (!ret) + return std::nullopt; + return negative ? -ret.value() : ret.value(); +} + +inline bool leap_year(int32_t local_year) { + return (local_year % 4) == 0 && + ((local_year % 100) || ((local_year % 400) == 0)); +} + +uint8_t week_day_for_day_of_month(int32_t year, uint8_t month, + uint8_t day_of_month) { + auto k = static_cast(day_of_month); + auto m = month >= 3 ? month - 2 : 10 + month; + auto C = year % 100; + auto Y = year / 100; + if (m > 10) + --Y; + return abs((k + static_cast((2.6 * m - 0.2)) + - 2 * C + Y + (Y / 4) + (C / 4))) % 7; +} + +std::optional read_date_and_time(std::string_view str, + int32_t local_year, + size_t& offset) { + if (offset >= str.size()) + return std::nullopt; + + time_t day_of_year; + if (str[offset] == 'J') { + ++offset; + auto julian_day = read_num(str, offset); + if (!julian_day || julian_day.value() < 1 || julian_day.value() > 365) + return std::nullopt; + + day_of_year = julian_day.value() - 1; + if (leap_year(local_year) && julian_day.value() >= 60) + ++day_of_year; + } else if (str[offset] == 'M') { + ++offset; + auto month = read_num(str, offset); + if (!month || month.value() < 1 || month.value() > 12) + return std::nullopt; + + if (offset >= str.size() || str[offset] != '.') + return std::nullopt; + + ++offset; + auto week_of_month = read_num(str, offset); + if (!week_of_month || week_of_month.value() < 1 || + week_of_month.value() > 5) + return std::nullopt; + + if (offset >= str.size() || str[offset] != '.') + return std::nullopt; + + ++offset; + auto day_of_week = read_num(str, offset); + if (!day_of_week || day_of_week.value() > 6) + return std::nullopt; + + day_of_year = 0; + for (size_t i = 1; i < month; ++i) { + day_of_year += (i <= 7) + ? ((i == 2) ? (leap_year(local_year) ? 29 : 28) : (i % 2 ? 31 : 30)) + : (i % 2 ? 30 : 31); + } + auto week_day_for_first_day_of_month = week_day_for_day_of_month( + local_year, month.value(), 1); + for (size_t i = 1; i < week_of_month; ++i) + day_of_year += 7; + if (day_of_week.value() < week_day_for_first_day_of_month) + day_of_year += 7 - week_day_for_first_day_of_month - day_of_week.value(); + else + day_of_year += day_of_week.value() - week_day_for_first_day_of_month; + } else { + auto julian_day = read_num(str, offset); + if (!julian_day || julian_day.value() > 365) + return std::nullopt; + + day_of_year = julian_day.value(); + } + + time_t ret = day_of_year * 24 * 60 * 60; + if (offset < str.size() && str[offset] == '/') { + ++offset; + auto time = read_time(str, offset); + if (!time) + return std::nullopt; + + ret += time.value(); + } else { + ret += 2 * 60 * 60; + } + return ret; +} + +} // namespace + +std::optional get_local_time(std::string_view tz_str, + time_t utc_time) { + size_t offset = 0; + auto std = read_abbr(tz_str, offset); + if (!std) + return std::nullopt; + + auto std_offset = read_offset(tz_str, offset); + if (!std_offset) + return std::nullopt; + + auto local_time = utc_time + std_offset.value(); + if (offset == tz_str.size()) + return local_time; + auto dst = read_abbr(tz_str, offset); + if (!dst) + return std::nullopt; + + std::optional dst_offset; + if (offset == tz_str.size() || tz_str[offset] == ',') { + dst_offset = std_offset.value() + 60 * 60; + } else { + dst_offset = read_offset(tz_str, offset); + if (!dst_offset) + return std::nullopt; + } + if (offset == tz_str.size()) { + // TODO: Can't figure out what the spec actually says about this. + // They are clearly optional but it doesn't specify what the default + // are. Assume no DST. + return local_time; + } + if (tz_str[offset] != ',') + return std::nullopt; + ++offset; + // TODO: This can't be 100% correct + auto local_year = 1970 + local_time / (365.25 * 24 * 60 * 60); + auto start = read_date_and_time(tz_str, local_year, offset); + if (!start) + return std::nullopt; + + if (tz_str[offset] != ',') + return std::nullopt; + + ++offset; + auto end = read_date_and_time(tz_str, local_year, offset); + if (!end) + return std::nullopt; + + // TODO: If local_year isn't correct this is definitly not. + auto local_time_in_year = + local_time % static_cast((365.25 * 24 * 60 * 60)); + if (start.value() <= end.value()) { + if (start.value() < local_time_in_year && + local_time_in_year < end.value()) { + return utc_time + dst_offset.value(); + } + } else { + if (!(end.value() < local_time_in_year && + local_time_in_year < start.value())) { + return utc_time + dst_offset.value(); + } + } + return local_time; +} + +} // namespace tz diff --git a/src/tz_str.hh b/src/tz_str.hh new file mode 100644 index 0000000..d706197 --- /dev/null +++ b/src/tz_str.hh @@ -0,0 +1,16 @@ +#ifndef TZ_STR_HH +#define TZ_STR_HH + +#include +#include + +#include "time.h" + +namespace tz { + +std::optional get_local_time(std::string_view tz_str, + time_t utc_time); + +} // namespace tz + +#endif // TZ_STR_HH diff --git a/src/unique_fd.cc b/src/unique_fd.cc new file mode 100644 index 0000000..eb2ed29 --- /dev/null +++ b/src/unique_fd.cc @@ -0,0 +1,10 @@ +#include "common.hh" + +#include "io.hh" +#include "unique_fd.hh" + +void unique_fd::reset(int fd) { + if (fd_ >= 0) + io::close(fd_); + fd_ = fd; +} diff --git a/src/unique_fd.hh b/src/unique_fd.hh new file mode 100644 index 0000000..dc60b3f --- /dev/null +++ b/src/unique_fd.hh @@ -0,0 +1,48 @@ +#ifndef UNIQUE_FD_HH +#define UNIQUE_FD_HH + +#include + +class unique_fd { +public: + constexpr unique_fd() noexcept + : fd_(-1) {} + constexpr unique_fd(std::nullptr_t) noexcept + : fd_(-1) {} + explicit unique_fd(int fd) noexcept + : fd_(fd) {} + unique_fd(unique_fd&& fd) noexcept + : fd_(fd.release()) {} + + ~unique_fd() { reset(); } + + unique_fd& operator=(unique_fd&& fd) noexcept { + reset(fd.release()); + return *this; + } + unique_fd& operator=(std::nullptr_t) noexcept { + reset(); + return *this; + } + + int get() const noexcept { return fd_; } + int operator*() const { return get(); } + + explicit operator bool() const noexcept { return fd_ >= 0; } + + int release() noexcept { + int ret = fd_; + fd_ = -1; + return ret; + } + + void reset(int fd = -1); + +private: + unique_fd(unique_fd const&) = delete; + unique_fd& operator=(unique_fd const&) = delete; + + int fd_; +}; + +#endif // UNIQUE_FD_HH diff --git a/src/unique_pipe.cc b/src/unique_pipe.cc new file mode 100644 index 0000000..3ab1871 --- /dev/null +++ b/src/unique_pipe.cc @@ -0,0 +1,47 @@ +#include "common.hh" + +#include "io.hh" +#include "unique_pipe.hh" + +#include +#include + +unique_pipe::unique_pipe(bool non_blocking_reader, bool non_blocking_writer) { + int fd[2]; +#if HAVE_PIPE2 + if (non_blocking_reader == non_blocking_writer) { + if (pipe2(fd, non_blocking_reader ? O_NONBLOCK : 0) == 0) { + fd_[0] = unique_fd(fd[0]); + fd_[1] = unique_fd(fd[1]); + } + return; + } +#endif + if (pipe(fd)) + return; + if (non_blocking_reader) + io::make_nonblocking(fd[0]); + if (non_blocking_writer) + io::make_nonblocking(fd[1]); + fd_[0] = unique_fd(fd[0]); + fd_[1] = unique_fd(fd[1]); +} + +unique_pipe::unique_pipe(unique_pipe&& fd) noexcept { + fd_[0] = unique_fd(fd.fd_[0].release()); + fd_[1] = unique_fd(fd.fd_[1].release()); +} + +void unique_pipe::reset() { + fd_[0].reset(); + fd_[1].reset(); +} + +unique_fd unique_pipe::release_reader() { + return std::move(fd_[0]); +} + +unique_fd unique_pipe::release_writer() { + return std::move(fd_[1]); +} + diff --git a/src/unique_pipe.hh b/src/unique_pipe.hh new file mode 100644 index 0000000..7020cdf --- /dev/null +++ b/src/unique_pipe.hh @@ -0,0 +1,51 @@ +#ifndef UNIQUE_PIPE_HH +#define UNIQUE_PIPE_HH + +#include + +#include "unique_fd.hh" + +class unique_pipe { +public: + explicit unique_pipe(bool non_blocking = false) + : unique_pipe(non_blocking, non_blocking) {} + unique_pipe(bool non_blocking_reader, + bool non_blocking_writer); + unique_pipe(unique_pipe&& fd) noexcept; + unique_pipe(std::nullptr_t) noexcept + : fd_{nullptr, nullptr} {} + + ~unique_pipe() = default; + + unique_pipe& operator=(unique_pipe&& fd) noexcept { + fd_[0].reset(fd.fd_[0].release()); + fd_[1].reset(fd.fd_[1].release()); + return *this; + } + + unique_pipe& operator=(std::nullptr_t) noexcept { + reset(); + return *this; + } + + int reader() const noexcept { return fd_[0].get(); } + int writer() const noexcept { return fd_[1].get(); } + + explicit operator bool() const noexcept { + return fd_[0] || fd_[1]; + } + + void reset(); + + unique_fd release_reader(); + unique_fd release_writer(); + +private: + unique_pipe(unique_pipe const&) = delete; + unique_pipe& operator=(unique_pipe const&) = delete; + + unique_fd fd_[2]; +}; + + +#endif // UNIQUE_PIPE_HH diff --git a/src/urlutil.cc b/src/urlutil.cc new file mode 100644 index 0000000..00ec713 --- /dev/null +++ b/src/urlutil.cc @@ -0,0 +1,138 @@ +#include "common.hh" + +#include "urlutil.hh" + +#include + +namespace url { + +namespace { + +constexpr char kHex[] = "0123456789ABCDEF"; + +std::optional unhex(char c) { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'A' && c <= 'F') + return 10 + (c - 'A'); + if (c >= 'a' && c <= 'f') + return 10 + (c - 'a'); + return std::nullopt; +} + +bool is_unreserved(char c) { + if (c >= 'A' && c <= 'Z') + return true; + if (c >= 'a' && c <= 'z') + return true; + if (c >= '0' && c <= '9') + return true; + return c == '-' || c == '_' || c == '.' || c == '~'; +} + +std::string query_unescape(std::string_view str) { + std::string ret; + size_t start = 0; + while (true) { + auto next = str.find('+', start); + if (next == std::string::npos) { + unescape(str.substr(start), ret); + break; + } + unescape(str.substr(start, next - start), ret); + ret.push_back(' '); + start = next + 1; + } + return ret; +} + +} // namespace + +std::string escape(std::string_view str, EscapeFlags flags) { + std::string out; + escape(str, out, flags); + return out; +} + +void escape(std::string_view str, std::string& out, EscapeFlags flags) { + out.reserve(out.size() + str.size()); + bool const keep_slash = + (flags & EscapeFlags::KEEP_SLASH) == EscapeFlags::KEEP_SLASH; + for (char c : str) { + if (is_unreserved(c) || (c == '/' && keep_slash)) { + out.push_back(c); + } else { + out.push_back('%'); + out.push_back(kHex[(c & 0xff) >> 4]); + out.push_back(kHex[(c & 0xff) & 0xf]); + } + } +} + +std::string unescape(std::string_view str) { + std::string ret; + unescape(str, ret); + return ret; +} + +void unescape(std::string_view str, std::string& out) { + out.reserve(out.size() + str.size()); + + size_t last = 0; + while (true) { + auto next = str.find('%', last); + if (next == std::string::npos || next + 3 > str.size()) + break; + auto a = unhex(str[next + 1]); + auto b = unhex(str[next + 2]); + if (a && b) { + out.append(str, last, next - last); + out.push_back(a.value() << 4 | b.value()); + } else { + // Keep invalid escape sequences as-is. + out.append(str, last, next + 3 - last); + } + last = next + 3; + } + out.append(str, last); +} + +void split_and_unescape_path_and_query( + std::string_view url, + std::string& path, + std::unordered_map& query) { + auto start = url.find('?'); + if (start == std::string_view::npos) { + path = unescape(url); + query.clear(); + } else { + path = unescape(url.substr(0, start)); + query = expand_and_unescape_query(url.substr(start + 1)); + } +} + +std::unordered_map expand_and_unescape_query( + std::string_view query) { + std::unordered_map ret; + size_t start = 0; + while (true) { + auto next = query.find('&', start); + auto pair = next == std::string::npos + ? query.substr(start) + : query.substr(start, next - start); + auto eq = pair.find('='); + if (eq == std::string::npos) { + if (!pair.empty()) + ret.emplace(query_unescape(pair), std::string()); + } else { + ret.emplace(query_unescape(pair.substr(0, eq)), + query_unescape(pair.substr(eq + 1))); + } + if (next == std::string::npos) + break; + start = next + 1; + } + return ret; +} + +} // namespace url diff --git a/src/urlutil.hh b/src/urlutil.hh new file mode 100644 index 0000000..5ae170c --- /dev/null +++ b/src/urlutil.hh @@ -0,0 +1,60 @@ +#ifndef URLUTIL_HH +#define URLUTIL_HH + +#include +#include +#include + +namespace url { + +enum class EscapeFlags : unsigned { + // Default encodes all non-unreserved characters + // (not the same as all reserved) to be safe. + DEFAULT = 0, + // Same as DEFAULT but doesn't encode SLASH, useful when the in data + // is a path. + KEEP_SLASH = 1, +}; + +std::string escape(std::string_view str, + EscapeFlags flags = EscapeFlags::DEFAULT); +void escape(std::string_view str, std::string& out, + EscapeFlags flags = EscapeFlags::DEFAULT); + +std::string unescape(std::string_view str); +void unescape(std::string_view str, std::string& out); + +constexpr EscapeFlags operator&(EscapeFlags a, EscapeFlags b) noexcept { + using utype = typename std::underlying_type::type; + return static_cast( + static_cast(a) & static_cast(b)); +} + +constexpr EscapeFlags operator|(EscapeFlags a, EscapeFlags b) noexcept { + using utype = typename std::underlying_type::type; + return static_cast( + static_cast(a) | static_cast(b)); +} + +constexpr EscapeFlags operator^(EscapeFlags a, EscapeFlags b) noexcept { + using utype = typename std::underlying_type::type; + return static_cast( + static_cast(a) ^ static_cast(b)); +} + +constexpr EscapeFlags operator~(EscapeFlags a) noexcept { + using utype = typename std::underlying_type::type; + return static_cast(~static_cast(a)); +} + +void split_and_unescape_path_and_query( + std::string_view url, + std::string& path, + std::unordered_map& query); + +std::unordered_map expand_and_unescape_query( + std::string_view query); + +} // namespace url + +#endif // URLUTIL_HH diff --git a/src/video.cc b/src/video.cc new file mode 100644 index 0000000..110bd4a --- /dev/null +++ b/src/video.cc @@ -0,0 +1,171 @@ +#include "common.hh" + +#include "strutil.hh" +#include "timezone.hh" +#include "video.hh" + +#if HAVE_MEDIAINFO +#include +#endif + +namespace { + +class VideoImpl : public Video { +public: + VideoImpl(uint64_t width, uint64_t height) + : width_(width), height_(height) {} + + uint64_t width() const override { + return width_; + } + + uint64_t height() const override { + return height_; + } + + double length() const override { + return length_; + } + + Location location() const override { + return location_; + } + + Date date() const override { + return date_; + } + + void set_length(double length) { + length_ = length; + } + + void set_location(Location location) { + location_ = location; + } + + void set_date(Date date) { + date_ = date; + } + +private: + uint64_t width_; + uint64_t height_; + double length_{0.0}; + Location location_; + Date date_; +}; + +#if HAVE_MEDIAINFO +bool parse_location(std::string const& str, Location* location) { + auto end = str.find('/'); + if (end == std::string::npos) + return false; + char* latend = nullptr; + location->lat = strtod(str.c_str(), &latend); + if (!latend) + return false; + char* lngend = nullptr; + location->lng = strtod(latend, &lngend); + if (!lngend) + return false; + if (lngend != str.c_str() + end) + return false; + return true; +} +#endif + +} // namespace + +std::unique_ptr