summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-10-08 00:58:42 +0200
committerJoel Klinghed <the_jk@spawned.biz>2025-10-19 00:13:47 +0200
commit86ec0b5386fc2078891a829026844d2ec21ea7db (patch)
tree5f3ed650bc2957e06fd3c8c1ecfa7c6e7fc825b6 /src
parent3a002fb9c23fc9a6384bc1b30a8e364924bb574e (diff)
Add http module and implement basic http server
Diffstat (limited to 'src')
-rw-r--r--src/http.cc724
-rw-r--r--src/http.hh177
-rw-r--r--src/logger.cc145
-rw-r--r--src/logger.hh41
-rw-r--r--src/looper.hh46
-rw-r--r--src/looper_poll.cc238
-rw-r--r--src/main.cc98
-rw-r--r--src/str.cc26
-rw-r--r--src/str.hh7
9 files changed, 1501 insertions, 1 deletions
diff --git a/src/http.cc b/src/http.cc
new file mode 100644
index 0000000..8a0a1e6
--- /dev/null
+++ b/src/http.cc
@@ -0,0 +1,724 @@
+#include "http.hh"
+
+#include "buffer.hh"
+#include "cfg.hh"
+#include "logger.hh"
+#include "looper.hh"
+#include "str.hh"
+#include "unique_fd.hh"
+
+#include <algorithm>
+#include <cassert>
+#include <cerrno>
+#include <charconv>
+#include <chrono>
+#include <cstdint>
+#include <cstring>
+#include <fcntl.h>
+#include <format>
+#include <map>
+#include <memory>
+#include <netdb.h>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <utility>
+#include <vector>
+
+namespace http {
+
+namespace {
+
+std::string_view ascii_lowercase(std::string_view input, std::string& tmp) {
+ auto it = input.begin();
+ for (; it != input.end(); ++it) {
+ if (*it >= 'A' && *it <= 'Z') {
+ tmp.resize(input.size());
+ auto out = std::copy(input.begin(), it, tmp.begin());
+ // NOLINTNEXTLINE(bugprone-narrowing-conversions)
+ *out++ = *it++ | 0x20;
+ for (; it != input.end(); ++it) {
+ if (*it >= 'A' && *it <= 'Z') {
+ // NOLINTNEXTLINE(bugprone-narrowing-conversions)
+ *out++ = *it | 0x20;
+ } else {
+ *out++ = *it;
+ }
+ }
+ return tmp;
+ }
+ }
+ return input;
+}
+
+std::string standard_header(std::string_view input) {
+ bool capitalize = true;
+ std::string ret;
+ for (auto c : input) {
+ if (c >= 'A' && c <= 'Z') {
+ if (capitalize) {
+ ret.push_back(c);
+ capitalize = false;
+ } else {
+ // NOLINTNEXTLINE(bugprone-narrowing-conversions)
+ ret.push_back(c | 0x20);
+ }
+ } else if (c >= 'a' && c <= 'z') {
+ if (capitalize) {
+ // NOLINTNEXTLINE(bugprone-narrowing-conversions)
+ ret.push_back(c & 0x20);
+ capitalize = false;
+ } else {
+ ret.push_back(c);
+ }
+ } else if (c == '-') {
+ ret.push_back(c);
+ capitalize = true;
+ } else {
+ ret.push_back(c);
+ }
+ }
+ return ret;
+}
+
+void make_nonblock(int fd) {
+ int flags = fcntl(fd, F_GETFL);
+ if (flags & O_NONBLOCK)
+ return;
+ fcntl(fd, F_SETFL, flags | O_NONBLOCK);
+}
+
+void clear(Buffer& buffer) {
+ if (buffer.empty())
+ return;
+ while (true) {
+ size_t avail;
+ buffer.rptr(avail);
+ if (avail == 0)
+ break;
+ buffer.consume(avail);
+ }
+}
+
+class MimeTypeImpl : public MimeType {
+ public:
+ MimeTypeImpl(std::string str, size_t slash, size_t subtype_len,
+ std::map<std::string_view, std::string_view> params = {})
+ : str_(std::move(str)),
+ slash_(slash),
+ subtype_len_(subtype_len),
+ params_(std::move(params)) {
+ assert(slash_ < str_.size());
+ assert(slash_ + 1 + subtype_len_ <= str_.size());
+ }
+
+ [[nodiscard]]
+ std::string_view type() const override {
+ return std::string_view(str_).substr(0, slash_);
+ }
+
+ [[nodiscard]]
+ std::string_view subtype() const override {
+ return std::string_view(str_).substr(slash_ + 1, subtype_len_);
+ }
+
+ [[nodiscard]]
+ std::optional<std::string_view> parameter(
+ std::string_view name) const override {
+ std::optional<std::string_view> ret;
+ std::string tmp;
+ auto it = params_.find(ascii_lowercase(name, tmp));
+ if (it != params_.end())
+ ret = it->second;
+ return ret;
+ }
+
+ [[nodiscard]]
+ std::string const& string() const override {
+ return str_;
+ }
+
+ private:
+ std::string const str_;
+ size_t const slash_;
+ size_t const subtype_len_;
+ std::map<std::string_view, std::string_view> const params_;
+};
+
+class MimeTypeBuilderImpl : public MimeType::Builder {
+ public:
+ MimeTypeBuilderImpl(std::string_view type, std::string_view subtype)
+ : slash_(type.size()), subtype_len_(subtype.size()) {
+ str_.reserve(type.size() + 1 + subtype.size());
+ str_.append(type);
+ str_.push_back('/');
+ str_.append(subtype);
+ }
+
+ MimeType::Builder& parameter(std::string_view name,
+ std::string_view value) override {
+ // TODO: make value a quoted string if it contains ;
+ std::string tmp;
+ auto ret = params_.emplace(ascii_lowercase(name, tmp), value);
+ if (!ret.second) {
+ ret.first->second = value;
+ }
+ return *this;
+ }
+
+ [[nodiscard]]
+ std::unique_ptr<MimeType> build() const override {
+ if (params_.empty()) {
+ return std::make_unique<MimeTypeImpl>(str_, slash_, subtype_len_);
+ }
+
+ std::string tmp(str_);
+ std::map<std::pair<size_t, size_t>, size_t> params1;
+ for (auto const& pair : params_) {
+ tmp.append("; ");
+ params1.emplace(std::make_pair(tmp.size(), pair.first.size()),
+ pair.second.size());
+ tmp.append(pair.first);
+ tmp.push_back('=');
+ tmp.append(pair.second);
+ }
+ std::map<std::string_view, std::string_view> params2;
+ std::string_view tmp_view(tmp);
+ for (auto const& pair : params1) {
+ params2.emplace(tmp_view.substr(pair.first.first, pair.first.second),
+ tmp_view.substr(pair.first.first + pair.first.second + 1,
+ pair.second));
+ }
+ return std::make_unique<MimeTypeImpl>(std::move(tmp), slash_, subtype_len_,
+ std::move(params2));
+ }
+
+ private:
+ std::string str_;
+ size_t const slash_;
+ size_t const subtype_len_;
+ std::map<std::string, std::string> params_;
+};
+
+class OpenPortImpl : public OpenPort {
+ public:
+ explicit OpenPortImpl(std::vector<unique_fd> fd) : fd_(std::move(fd)) {}
+
+ std::vector<unique_fd> release() { return std::move(fd_); }
+
+ private:
+ std::vector<unique_fd> fd_;
+};
+
+class ResponseImpl : public Response {
+ public:
+ explicit ResponseImpl(std::string data) : data_(std::move(data)) {}
+
+ bool write(Buffer& buffer) override {
+ if (offset_ >= data_.size())
+ return false;
+
+ size_t avail;
+ auto* ptr = buffer.wptr(avail);
+ if (avail == 0)
+ return true;
+
+ avail = std::min(data_.size() - offset_, avail);
+
+ std::copy_n(data_.data() + offset_, avail, reinterpret_cast<char*>(ptr));
+ offset_ += avail;
+ buffer.commit(avail);
+
+ return offset_ < data_.size();
+ }
+
+ private:
+ std::string const data_;
+ size_t offset_{0};
+};
+
+class ResponseBuilderImpl : public Response::Builder {
+ public:
+ explicit ResponseBuilderImpl(StatusCode status) : status_(status) {}
+
+ Response::Builder& content(std::string_view content) override {
+ content_ = content;
+ return *this;
+ }
+
+ Response::Builder& add_header(std::string_view name,
+ std::string_view value) override {
+ // TODO: Make sure name or value doesn't contain invalid chars
+ headers_.emplace_back(standard_header(name), value);
+ return *this;
+ }
+
+ [[nodiscard]]
+ std::unique_ptr<Response> build() const override {
+ std::string ret;
+ ret.reserve(100 + content_.size());
+ ret.append("HTTP/1.1 ");
+ {
+ char tmp[4];
+ auto [ptr, ec] =
+ std::to_chars(tmp, tmp + sizeof(tmp), std::to_underlying(status_));
+ ret.append(tmp, ptr - tmp);
+ }
+ ret.append(" ");
+ switch (status_) {
+ case StatusCode::kOK:
+ ret.append("OK");
+ break;
+ case StatusCode::kNoContent:
+ ret.append("No Content");
+ break;
+ case StatusCode::kNotFound:
+ ret.append("Not Found");
+ break;
+ case StatusCode::kMethodNotAllowed:
+ ret.append("Method Not Allowed");
+ break;
+ }
+ ret.append("\r\n");
+ bool have_content_len = false;
+ for (auto const& pair : headers_) {
+ if (!have_content_len && pair.first == "Content-Length")
+ have_content_len = true;
+ ret.append(pair.first);
+ ret.append(": ");
+ ret.append(pair.second);
+ ret.append("\r\n");
+ }
+ if (!have_content_len && status_ != StatusCode::kNoContent) {
+ char tmp[20];
+ auto [ptr, ec] = std::to_chars(tmp, tmp + sizeof(tmp), content_.size());
+ ret.append("Content-Length");
+ ret.append(": ");
+ ret.append(tmp, ptr - tmp);
+ ret.append("\r\n");
+ }
+ ret.append("\r\n");
+ if (status_ != StatusCode::kNoContent)
+ ret.append(content_);
+ return std::make_unique<ResponseImpl>(std::move(ret));
+ }
+
+ private:
+ StatusCode const status_;
+ std::string content_;
+ std::vector<std::pair<std::string, std::string>> headers_;
+};
+
+class RequestImpl : public Request {
+ public:
+ RequestImpl(std::string_view method, std::string_view path)
+ : method_(method), path_(path) {}
+
+ [[nodiscard]]
+ std::string_view method() const override {
+ return method_;
+ }
+
+ [[nodiscard]]
+ std::string_view path() const override {
+ return path_;
+ }
+
+ private:
+ std::string_view method_;
+ std::string_view path_;
+};
+
+// TODO: What is a good value?
+const int kListenBacklog = 10;
+
+class ServerImpl : public Server {
+ public:
+ ServerImpl(logger::Logger& logger, cfg::Config const& cfg,
+ looper::Looper& looper, std::unique_ptr<OpenPort> open_port,
+ Server::Delegate& delegate)
+ : logger_(logger),
+ cfg_(cfg),
+ looper_(looper),
+ delegate_(delegate),
+ listens_(static_cast<OpenPortImpl*>(open_port.get())->release()) {
+ client_timeout_ = std::chrono::duration<double>(
+ cfg_.get_uint64("http.client.timeout.seconds").value_or(60));
+ client_.resize(cfg_.get_uint64("http.max.clients").value_or(100));
+ start_to_listen();
+ }
+
+ private:
+ static const size_t kInputMinSize = 1024;
+ static const size_t kInputMaxSize = static_cast<size_t>(1) * 1024 * 1024;
+ static const size_t kOutputSize = static_cast<size_t>(512) * 1024;
+
+ struct Client {
+ unique_fd fd;
+ std::unique_ptr<Buffer> in{Buffer::dynamic(kInputMinSize, kInputMaxSize)};
+ std::unique_ptr<Buffer> out{Buffer::fixed(kOutputSize)};
+ std::unique_ptr<Response> resp;
+ std::chrono::steady_clock::time_point last;
+ uint32_t timeout{0};
+ bool read_closed_{false};
+ };
+
+ void start_to_listen() {
+ auto it = listens_.begin();
+ while (it != listens_.end()) {
+ if (listen(it->get(),
+ static_cast<int>(cfg_.get_uint64("http.listen.backlog")
+ .value_or(kListenBacklog)))) {
+ logger_.warn(
+ std::format("Error listening to socket: {}", strerror(errno)));
+ it = listens_.erase(it);
+ } else {
+ make_nonblock(it->get());
+
+ looper_.add(
+ it->get(), looper::EVENT_READ,
+ [this, fd = it->get()](auto events) { accept_client(fd, events); });
+ ++it;
+ }
+ }
+
+ if (listens_.empty()) {
+ logger_.err("No ports left to listen to, exit.");
+ looper_.quit();
+ }
+ }
+
+ void accept_client(int fd, uint8_t event) {
+ if (event & looper::EVENT_ERROR) {
+ logger_.warn("Listening port returned error, closing.");
+ looper_.remove(fd);
+ auto it = std::ranges::find_if(
+ listens_, [&fd](auto& ufd) { return ufd.get() == fd; });
+ if (it != listens_.end()) {
+ listens_.erase(it);
+ if (listens_.empty()) {
+ logger_.err("No ports left to listen to, exit.");
+ looper_.quit();
+ }
+ }
+ return;
+ }
+
+ unique_fd client_fd(accept4(fd, nullptr, nullptr, SOCK_NONBLOCK));
+ if (!client_fd) {
+ logger_.info(std::format("Accept returned error: {}", strerror(errno)));
+ return;
+ }
+
+ if (active_clients_ == client_.size()) {
+ logger_.warn("Max number of clients already.");
+ return;
+ }
+
+ for (; client_[next_client_].fd; next_client_++) {
+ if (next_client_ == client_.size())
+ next_client_ = 0;
+ }
+
+ logger_.dbg(std::format("New client: {}", next_client_));
+
+ auto& client = client_[next_client_];
+ client.fd = std::move(client_fd);
+ client.last = std::chrono::steady_clock::now();
+
+ looper_.add(
+ client.fd.get(), looper::EVENT_READ,
+ [this, id = next_client_](auto events) { client_event(id, events); });
+ client.timeout = looper_.schedule(
+ client_timeout_.count(),
+ [this, id = next_client_](auto handle) { client_timeout(id, handle); });
+
+ ++active_clients_;
+ ++next_client_;
+ // TODO: Stopping listening if active_clients_ == client_.size()
+ }
+
+ void client_event(size_t client_id, uint8_t event) {
+ if (event & looper::EVENT_ERROR) {
+ logger_.info(std::format("Client socket error: {}", client_id));
+ close_client(client_id);
+ return;
+ }
+
+ auto& client = client_[client_id];
+ if (event & looper::EVENT_READ) {
+ size_t avail;
+ auto* ptr = client.in->wptr(avail);
+ if (avail == 0) {
+ logger_.info(std::format("Client too large request: {}", client_id));
+ close_client(client_id);
+ return;
+ }
+
+ ssize_t got;
+ while (true) {
+ got = read(client.fd.get(), ptr, avail);
+ if (got < 0 && errno == EINTR)
+ continue;
+ break;
+ }
+ if (got < 0) {
+ if (errno != EWOULDBLOCK && errno != EAGAIN) {
+ logger_.info(std::format("Client read error: {}: {}", client_id,
+ strerror(errno)));
+ close_client(client_id);
+ return;
+ }
+ } else if (got == 0) {
+ client.read_closed_ = true;
+ } else {
+ client.in->commit(got);
+
+ if (!client_read(client_id))
+ return;
+ }
+ }
+
+ while (true) {
+ size_t avail;
+ auto* ptr = client.out->rptr(avail);
+ if (avail == 0) {
+ if (client.resp) {
+ if (!client.resp->write(*client.out)) {
+ client.resp.reset();
+
+ if (!client_read(client_id))
+ return;
+ }
+ continue;
+ }
+ break;
+ }
+ ssize_t got;
+ while (true) {
+ got = write(client.fd.get(), ptr, avail);
+ if (got < 0 && errno == EINTR)
+ continue;
+ break;
+ }
+ if (got < 0) {
+ if (errno != EWOULDBLOCK && errno != EAGAIN) {
+ logger_.info(std::format("Client write error: {}: {}", client_id,
+ strerror(errno)));
+ close_client(client_id);
+ return;
+ }
+ break;
+ }
+ client.out->consume(got);
+
+ if (std::cmp_less(got, avail))
+ break;
+ }
+
+ if (client.read_closed_ && client.in->empty() && client.out->empty() &&
+ !client.resp) {
+ close_client(client_id);
+ return;
+ }
+
+ bool want_read = !client.read_closed_ && !client.resp;
+ bool want_write = !client.out->empty();
+
+ looper_.update(client.fd.get(), (want_read ? looper::EVENT_READ : 0) |
+ (want_write ? looper::EVENT_WRITE : 0));
+ }
+
+ bool client_read(size_t client_id) {
+ auto& client = client_[client_id];
+ size_t avail;
+ auto* ptr = client.in->rptr(avail);
+ if (avail == 0)
+ return true;
+ std::string_view data(reinterpret_cast<char const*>(ptr), avail);
+ // TODO: Cache last search to not have to restart all the time
+ auto end = data.find("\r\n\r\n");
+ if (end == std::string_view::npos)
+ return true;
+
+ auto lines = str::split(data.substr(0, end), "\r\n", /* keep_empty */ true);
+ std::string_view method;
+ std::string_view path;
+ {
+ auto parts = str::split(lines[0], ' ', false);
+ if (parts.size() != 3) {
+ logger_.info(std::format("Client invalid request: {}", client_id));
+ close_client(client_id);
+ return false;
+ }
+ method = parts[0];
+ path = parts[1];
+ if (!parts[2].starts_with("HTTP/")) {
+ logger_.info(std::format("Client invalid request: {}", client_id));
+ close_client(client_id);
+ return false;
+ }
+ }
+ RequestImpl request(method, path);
+ client.resp = delegate_.handle(request);
+ client.in->consume(end + 4);
+ return true;
+ }
+
+ void client_timeout(size_t client_id, uint32_t id) {
+ auto now = std::chrono::steady_clock::now();
+ auto& client = client_[client_id];
+ assert(client.timeout == id);
+ if (now - client.last <
+ std::chrono::duration_cast<std::chrono::steady_clock::duration>(
+ client_timeout_)) {
+ // TODO: Reschedule for delay left, not the full one
+ client.timeout = looper_.schedule(client_timeout_.count(),
+ [this, client_id](auto handle) {
+ client_timeout(client_id, handle);
+ });
+ return;
+ }
+
+ logger_.dbg(std::format("Client timeout: {}", client_id));
+
+ client.timeout = 0;
+ close_client(client_id);
+ }
+
+ void close_client(size_t client_id) {
+ auto& client = client_[client_id];
+ if (!client.fd) {
+ assert(false);
+ return;
+ }
+
+ logger_.dbg(std::format("Drop client: {}", client_id));
+
+ looper_.remove(client.fd.get());
+ if (client.timeout)
+ looper_.cancel(client.timeout);
+ client.fd.reset();
+ clear(*client.in);
+ clear(*client.out);
+ client.resp.reset();
+ client.read_closed_ = false;
+ assert(active_clients_ > 0);
+ --active_clients_;
+ if (next_client_ == client_id + 1)
+ next_client_ = client_id;
+ }
+
+ logger::Logger& logger_;
+ cfg::Config const& cfg_;
+ looper::Looper& looper_;
+ Server::Delegate& delegate_;
+ std::chrono::duration<double> client_timeout_;
+ std::vector<unique_fd> listens_;
+ std::vector<Client> client_;
+ size_t next_client_{0};
+ size_t active_clients_{0};
+};
+
+} // namespace
+
+std::unique_ptr<MimeType> MimeType::create(std::string_view type,
+ std::string_view subtype) {
+ std::string tmp;
+ tmp.reserve(type.size() + 1 + subtype.size());
+ tmp.append(type);
+ tmp.push_back('/');
+ tmp.append(subtype);
+ return std::make_unique<MimeTypeImpl>(std::move(tmp), type.size(),
+ subtype.size());
+}
+
+std::unique_ptr<MimeType::Builder> MimeType::Builder::create(
+ std::string_view type, std::string_view subtype) {
+ return std::make_unique<MimeTypeBuilderImpl>(type, subtype);
+}
+
+std::unique_ptr<OpenPort> open_port(std::string_view host_port,
+ logger::Logger& logger) {
+ auto colon = host_port.find(':');
+ std::string host;
+ std::string port;
+ if (colon == std::string_view::npos) {
+ host = "localhost";
+ port = host_port;
+ } else {
+ host = host_port.substr(0, colon);
+ port = host_port.substr(colon + 1);
+ }
+
+ if (host == "*")
+ host = "";
+
+ struct addrinfo hints = {};
+ hints.ai_flags =
+ AI_PASSIVE // Use wildcard if host is nullptr
+ |
+ AI_ADDRCONFIG; // Only use IPv4 or IPv6 if there is at least one interface of that type
+ hints.ai_family = AF_UNSPEC; // Allow IPv4 or IPv6
+ hints.ai_socktype = SOCK_STREAM; // TCP
+ struct addrinfo* res;
+ auto ret = getaddrinfo(host.empty() ? nullptr : host.c_str(), port.c_str(),
+ &hints, &res);
+ if (ret) {
+ logger.err(
+ std::format("Unable to bind to {}: {}", host_port, gai_strerror(ret)));
+ return nullptr;
+ }
+
+ struct addrinfo* addr;
+ std::vector<unique_fd> fds;
+ for (addr = res; addr != nullptr; addr = addr->ai_next) {
+ unique_fd fd{socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol)};
+ if (!fd)
+ continue;
+ if (bind(fd.get(), addr->ai_addr, addr->ai_addrlen) == 0) {
+ fds.push_back(std::move(fd));
+ }
+ }
+ freeaddrinfo(res);
+
+ if (fds.empty()) {
+ // Assume that the last errno will be bind or socket failing
+ logger.err(
+ std::format("Unable to bind to {}: {}", host_port, strerror(errno)));
+ return nullptr;
+ }
+
+ return std::make_unique<OpenPortImpl>(std::move(fds));
+}
+
+std::unique_ptr<Response::Builder> Response::Builder::create(
+ StatusCode status_code) {
+ return std::make_unique<ResponseBuilderImpl>(status_code);
+}
+
+std::unique_ptr<Response> Response::status(StatusCode status_code) {
+ return Response::Builder::create(status_code)->build();
+}
+
+std::unique_ptr<Response> Response::content(std::string_view content,
+ MimeType const& mime_type) {
+ return Response::Builder::create(StatusCode::kOK)
+ ->content(content)
+ .add_header("Content-Type", mime_type.string())
+ .build();
+}
+
+std::unique_ptr<Server> create_server(logger::Logger& logger,
+ cfg::Config const& cfg,
+ looper::Looper& looper,
+ std::unique_ptr<OpenPort> open_port,
+ Server::Delegate& delegate) {
+ return std::make_unique<ServerImpl>(logger, cfg, looper, std::move(open_port),
+ delegate);
+}
+
+} // namespace http
diff --git a/src/http.hh b/src/http.hh
new file mode 100644
index 0000000..ca3f7d4
--- /dev/null
+++ b/src/http.hh
@@ -0,0 +1,177 @@
+#ifndef HTTP_HH
+#define HTTP_HH
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string_view>
+
+class Buffer;
+
+namespace logger {
+class Logger;
+} // namespace logger
+
+namespace looper {
+class Looper;
+} // namespace looper
+
+namespace cfg {
+class Config;
+} // namespace cfg
+
+namespace http {
+
+enum class StatusCode : uint16_t {
+ kOK = 200,
+ kNoContent = 204,
+ kNotFound = 404,
+ kMethodNotAllowed = 405,
+};
+
+class OpenPort {
+ public:
+ virtual ~OpenPort() = default;
+
+ protected:
+ OpenPort() = default;
+ OpenPort(OpenPort const&) = delete;
+ OpenPort& operator=(OpenPort const&) = delete;
+};
+
+// Returns nullptr in case of error, the error is written to logger.
+std::unique_ptr<OpenPort> open_port(std::string_view host_port,
+ logger::Logger& logger);
+
+class MimeType {
+ public:
+ virtual ~MimeType() = default;
+
+ [[nodiscard]]
+ virtual std::string_view type() const = 0;
+ [[nodiscard]]
+ virtual std::string_view subtype() const = 0;
+ [[nodiscard]]
+ virtual std::optional<std::string_view> parameter(
+ std::string_view name) const = 0;
+ [[nodiscard]]
+ virtual std::string const& string() const = 0;
+
+ class Builder {
+ public:
+ virtual ~Builder() = default;
+
+ virtual Builder& parameter(std::string_view name,
+ std::string_view value) = 0;
+
+ [[nodiscard]]
+ virtual std::unique_ptr<MimeType> build() const = 0;
+
+ [[nodiscard]]
+ static std::unique_ptr<Builder> create(std::string_view type,
+ std::string_view subtype);
+
+ protected:
+ Builder() = default;
+ Builder(Builder const&) = delete;
+ Builder& operator=(Builder const&) = delete;
+ };
+
+ // Short version of Builder::create(type, subtype).build()
+ [[nodiscard]]
+ static std::unique_ptr<MimeType> create(std::string_view type,
+ std::string_view subtype);
+
+ protected:
+ MimeType() = default;
+ MimeType(MimeType const&) = delete;
+ MimeType& operator=(MimeType const&) = delete;
+};
+
+class Request {
+ public:
+ virtual ~Request() = default;
+
+ [[nodiscard]]
+ virtual std::string_view method() const = 0;
+ [[nodiscard]]
+ virtual std::string_view path() const = 0;
+
+ protected:
+ Request() = default;
+ Request(Request const&) = delete;
+ Request& operator=(Request const&) = delete;
+};
+
+class Response {
+ public:
+ virtual ~Response() = default;
+
+ class Builder {
+ public:
+ virtual ~Builder() = default;
+
+ virtual Builder& content(std::string_view content) = 0;
+ virtual Builder& add_header(std::string_view name,
+ std::string_view value) = 0;
+
+ [[nodiscard]]
+ virtual std::unique_ptr<Response> build() const = 0;
+
+ [[nodiscard]]
+ static std::unique_ptr<Builder> create(StatusCode status_code);
+
+ protected:
+ Builder() = default;
+ Builder(Builder const&) = delete;
+ Builder& operator=(Builder const&) = delete;
+ };
+
+ // Short version of Builder::create(status_code).build()
+ [[nodiscard]]
+ static std::unique_ptr<Response> status(StatusCode status_code);
+
+ // Short version of Builder::create(StatusCode::kOK).content(content)
+ // .add_header("Content-Type", mime_type).build()
+ [[nodiscard]]
+ static std::unique_ptr<Response> content(std::string_view content,
+ MimeType const& mime_type);
+
+ // Returns true while there is more data to write.
+ virtual bool write(Buffer& buffer) = 0;
+
+ protected:
+ Response() = default;
+ Response(Response const&) = delete;
+ Response& operator=(Response const&) = delete;
+};
+
+class Server {
+ public:
+ virtual ~Server() = default;
+
+ class Delegate {
+ public:
+ virtual ~Delegate() = default;
+
+ virtual std::unique_ptr<Response> handle(Request const& request) = 0;
+
+ protected:
+ Delegate() = default;
+ };
+
+ protected:
+ Server() = default;
+ Server(Server const&) = delete;
+ Server& operator=(Server const&) = delete;
+};
+
+std::unique_ptr<Server> create_server(logger::Logger& logger,
+ cfg::Config const& cfg,
+ looper::Looper& looper,
+ std::unique_ptr<OpenPort> open_port,
+ Server::Delegate& delegate);
+
+} // namespace http
+
+#endif // HTTP_HH
diff --git a/src/logger.cc b/src/logger.cc
new file mode 100644
index 0000000..21effff
--- /dev/null
+++ b/src/logger.cc
@@ -0,0 +1,145 @@
+#include "logger.hh"
+
+#include <cstdint>
+#include <iostream>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <syslog.h>
+#include <utility>
+
+namespace logger {
+
+namespace {
+
+class BaseLogger : public Logger {
+ protected:
+ enum class Level : uint8_t {
+ kError,
+ kWarning,
+ kInfo,
+ kDebug,
+ };
+
+ public:
+ void err(std::string_view message) final { write(Level::kError, message); }
+
+ void warn(std::string_view message) final { write(Level::kWarning, message); }
+
+ void info(std::string_view message) final { write(Level::kInfo, message); }
+
+#if !defined(NDEBUG)
+ void dbg(std::string_view message) final { write(Level::kDebug, message); }
+#endif
+
+ protected:
+ BaseLogger() = default;
+
+ virtual void write(Level level, std::string_view message) = 0;
+};
+
+class NoopLogger : public BaseLogger {
+ public:
+ NoopLogger() = default;
+
+ protected:
+ void write(Level /* level */, std::string_view /* message */) override {}
+};
+
+class SyslogLogger : public BaseLogger {
+ public:
+ SyslogLogger(std::string ident, bool verbose)
+ : ident_(std::move(ident)), verbose_(verbose) {
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ openlog(ident_.c_str(), LOG_PID | LOG_CONS, LOG_DAEMON);
+ }
+
+ ~SyslogLogger() override {
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ closelog();
+ }
+
+ protected:
+ void write(Level level, std::string_view message) override {
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ int prio = LOG_ERR;
+ switch (level) {
+ case Level::kError:
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ prio = LOG_ERR;
+ break;
+ case Level::kWarning:
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ prio = LOG_WARNING;
+ break;
+ case Level::kInfo:
+ if (!verbose_)
+ return;
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ prio = LOG_INFO;
+ break;
+ case Level::kDebug:
+ if (!verbose_)
+ return;
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ prio = LOG_DEBUG;
+ break;
+ }
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ ::syslog(prio, "%*s", static_cast<int>(message.size()), message.data());
+ }
+
+ private:
+ std::string const ident_;
+ bool const verbose_;
+};
+
+class StderrLogger : public BaseLogger {
+ public:
+ explicit StderrLogger(bool verbose) : verbose_(verbose) {}
+
+ protected:
+ void write(Level level, std::string_view message) override {
+ switch (level) {
+ case Level::kError:
+ std::cerr << "Error: ";
+ break;
+ case Level::kWarning:
+ std::cerr << "Warning: ";
+ break;
+ case Level::kInfo:
+ if (!verbose_)
+ return;
+ std::cerr << "Info: ";
+ break;
+ case Level::kDebug:
+ if (!verbose_)
+ return;
+ std::cerr << "Debug: ";
+ break;
+ }
+ std::cerr << message << '\n';
+ }
+
+ private:
+ bool const verbose_;
+};
+
+} // namespace
+
+[[nodiscard]]
+std::unique_ptr<Logger> noop() {
+ return std::make_unique<NoopLogger>();
+}
+
+[[nodiscard]]
+std::unique_ptr<Logger> syslog(std::string ident, bool verbose) {
+ return std::make_unique<SyslogLogger>(std::move(ident), verbose);
+}
+
+[[nodiscard]]
+std::unique_ptr<Logger> stderr(bool verbose) {
+ return std::make_unique<StderrLogger>(verbose);
+}
+
+} // namespace logger
diff --git a/src/logger.hh b/src/logger.hh
new file mode 100644
index 0000000..5c1e599
--- /dev/null
+++ b/src/logger.hh
@@ -0,0 +1,41 @@
+#ifndef LOGGER_HH
+#define LOGGER_HH
+
+#include <memory>
+#include <string>
+#include <string_view>
+
+namespace logger {
+
+class Logger {
+ public:
+ virtual ~Logger() = default;
+
+ virtual void err(std::string_view message) = 0;
+ virtual void warn(std::string_view message) = 0;
+ virtual void info(std::string_view message) = 0;
+
+#if defined(NDEBUG)
+ void dbg(std::string_view) {}
+#else
+ virtual void dbg(std::string_view message) = 0;
+#endif
+
+ protected:
+ Logger() = default;
+ Logger(Logger const&) = delete;
+ Logger& operator=(Logger const&) = delete;
+};
+
+[[nodiscard]]
+std::unique_ptr<Logger> noop();
+
+[[nodiscard]]
+std::unique_ptr<Logger> syslog(std::string ident, bool verbose = false);
+
+[[nodiscard]]
+std::unique_ptr<Logger> stderr(bool verbose = false);
+
+} // namespace logger
+
+#endif // LOGGER_HH
diff --git a/src/looper.hh b/src/looper.hh
new file mode 100644
index 0000000..2f4c672
--- /dev/null
+++ b/src/looper.hh
@@ -0,0 +1,46 @@
+#ifndef LOOPER_HH
+#define LOOPER_HH
+
+#include <cstdint>
+#include <functional>
+#include <memory>
+
+namespace logger {
+class Logger;
+} // namespace logger
+
+namespace looper {
+
+constexpr static uint8_t EVENT_READ = 1;
+constexpr static uint8_t EVENT_WRITE = 2;
+constexpr static uint8_t EVENT_ERROR = 4;
+
+class Looper {
+ public:
+ virtual ~Looper() = default;
+
+ virtual void add(int fd, uint8_t events,
+ std::function<void(uint8_t)> 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<void(uint32_t)> callback) = 0;
+ virtual void cancel(uint32_t id) = 0;
+
+ virtual bool run(logger::Logger& logger) = 0;
+ virtual void quit() = 0;
+
+ protected:
+ Looper() = default;
+ Looper(Looper const&) = delete;
+ Looper& operator=(Looper const&) = delete;
+};
+
+[[nodiscard]]
+std::unique_ptr<Looper> create();
+
+} // namespace looper
+
+#endif // LOOPER_HH
diff --git a/src/looper_poll.cc b/src/looper_poll.cc
new file mode 100644
index 0000000..2fed7d2
--- /dev/null
+++ b/src/looper_poll.cc
@@ -0,0 +1,238 @@
+#include "logger.hh"
+#include "looper.hh"
+
+#include <algorithm>
+#include <cassert>
+#include <cerrno>
+#include <chrono>
+#include <cstdint>
+#include <cstring>
+#include <deque>
+#include <format>
+#include <functional>
+#include <limits>
+#include <memory>
+#include <poll.h>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+namespace looper {
+
+namespace {
+
+class LooperPoll : public Looper {
+ public:
+ LooperPoll() = default;
+
+ void add(int fd, uint8_t events,
+ std::function<void(uint8_t)> 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& 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<std::chrono::milliseconds>(
+ scheduled_.front().target_ - now);
+ if (delay.count() <= std::numeric_limits<int>::max())
+ timeout = static_cast<int>(delay.count());
+ else
+ timeout = std::numeric_limits<int>::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;
+ }
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ std::vector<struct pollfd> pollfd;
+ pollfd.reserve(entry_.size());
+ auto it = entry_.begin();
+ while (it != entry_.end()) {
+ if (it->second.delete_) {
+ it = entry_.erase(it);
+ } else {
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ struct pollfd tmp;
+ tmp.fd = it->first;
+ tmp.events = events_looper2poll(it->second.events_);
+ pollfd.push_back(tmp);
+ ++it;
+ }
+ }
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ int active = poll(pollfd.data(), pollfd.size(), timeout);
+ if (active < 0) {
+ if (errno == EINTR)
+ continue;
+ logger.err(std::format("Poll failed: {}", 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<void(uint32_t)> 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::steady_clock::duration>(
+ std::chrono::duration<double>(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 {
+ auto it = std::ranges::find_if(scheduled_, [id](auto const& scheduled) {
+ return scheduled.id_ == id;
+ });
+ if (it != scheduled_.end()) {
+ scheduled_.erase(it);
+ } else {
+ assert(false);
+ }
+ }
+
+ private:
+ struct Entry {
+ uint8_t events_;
+ std::function<void(uint8_t)> callback_;
+ bool delete_{false};
+
+ explicit Entry(uint8_t events) : events_(events) {}
+ };
+
+ struct Scheduled {
+ std::function<void(uint32_t)> callback_;
+ uint32_t id_;
+ std::chrono::steady_clock::time_point target_;
+
+ Scheduled(std::function<void(uint32_t)> 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)
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ ret |= POLLIN | POLLPRI;
+ if (events & EVENT_WRITE)
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ ret |= POLLOUT;
+ return ret;
+ }
+
+ static uint8_t events_poll2looper(short events) {
+ uint8_t ret = 0;
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ if (events & (POLLIN | POLLPRI | POLLHUP))
+ ret |= EVENT_READ;
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ if (events & POLLOUT)
+ ret |= EVENT_WRITE;
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ 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 = std::ranges::any_of(
+ scheduled_,
+ [ret](auto const& scheduled) { return scheduled.id_ == ret; });
+ if (!found)
+ return ret;
+ }
+ }
+ }
+
+ bool quit_{false};
+ std::unordered_map<int, Entry> entry_;
+ uint32_t next_schedule_id_{1};
+ std::deque<Scheduled> scheduled_;
+};
+
+} // namespace
+
+[[nodiscard]]
+std::unique_ptr<Looper> create() {
+ return std::make_unique<LooperPoll>();
+}
+
+} // namespace looper
diff --git a/src/main.cc b/src/main.cc
index e66f95a..54f3d0a 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -1,16 +1,91 @@
#include "args.hh"
+#include "cfg.hh"
#include "config.h"
+#include "http.hh"
+#include "logger.hh"
+#include "looper.hh"
+#include <cerrno>
+#include <cstring>
+#include <format>
#include <iostream>
+#include <memory>
+#include <unistd.h>
+#include <utility>
+#include <vector>
#ifndef VERSION
# define VERSION "unknown"
#endif
+namespace {
+
+class HttpServerDelegate : public http::Server::Delegate {
+ public:
+ HttpServerDelegate() = default;
+
+ std::unique_ptr<http::Response> handle(
+ http::Request const& request) override {
+ if (request.method() != "GET") {
+ return http::Response::status(http::StatusCode::kMethodNotAllowed);
+ }
+
+ if (request.path() == "/api/v1/status") {
+ return http::Response::content(
+ R"({ status: "OK" })",
+ *http::MimeType::create("application", "json"));
+ }
+
+ return http::Response::status(http::StatusCode::kNotFound);
+ }
+};
+
+bool run(logger::Logger& logger, cfg::Config const& cfg,
+ std::unique_ptr<http::OpenPort> port) {
+ auto looper = looper::create();
+ HttpServerDelegate delegate;
+ auto server =
+ http::create_server(logger, cfg, *looper, std::move(port), delegate);
+ return looper->run(logger);
+}
+
+bool start_daemon(logger::Logger& logger, cfg::Config const& cfg,
+ std::unique_ptr<http::OpenPort> port, bool verbose) {
+ auto pid = fork();
+ if (pid == 0) {
+ // Child process
+ // Move out of current workind directory to not keep it alive
+ chdir("/");
+ // Create a new process group
+ setpgid(0, 0);
+ // Close stdin, stdout and stderr
+ close(0);
+ close(1);
+ close(2);
+ // Use syslog logger
+ auto logger2 = logger::syslog("bluetooth-jukebox", verbose);
+ _exit(run(*logger2, cfg, std::move(port)) ? 0 : 1);
+ } else if (pid == -1) {
+ // Error
+ logger.err(std::format("Unable to fork: {}", strerror(errno)));
+ return false;
+ } else {
+ // Parent process
+ logger.info(std::format("Forked into pid {}", pid));
+ return true;
+ }
+}
+
+} // namespace
+
int main(int argc, char** argv) {
auto args = Args::create();
auto opt_help = args->option('h', "help", "display this text and exit.");
auto opt_version = args->option('V', "version", "display version and exit.");
+ auto opt_config = args->option_argument(
+ 'C', "config", "FILE", "load config from FILE instead of default.");
+ auto opt_verbose = args->option('v', "verbose", "be more verbose.");
+ auto opt_daemon = args->option('d', "daemon", "fork into background.");
if (!args->run(argc, argv)) {
args->print_error(std::cerr);
std::cerr << "Try 'bluetooth-jukebox --help' for more information.\n";
@@ -27,5 +102,26 @@ int main(int argc, char** argv) {
<< " written by Joel Klinghed <the_jk@spawned.biz>.\n";
return 0;
}
- return 0;
+ bool verbose = opt_verbose->is_set();
+ bool daemon = opt_daemon->is_set();
+ std::unique_ptr<cfg::Config> cfg;
+ std::vector<std::string> errors;
+ if (opt_config->is_set()) {
+ cfg = cfg::load_one(opt_config->argument(), errors);
+ } else {
+ cfg = cfg::load_all("bluetooth-jukebox", errors);
+ }
+
+ auto logger = logger::stderr(verbose);
+
+ // Open the port here, in case we fork() later, to fail early.
+ auto port = http::open_port(cfg->get("http.bind").value_or("localhost:5555"),
+ *logger);
+ if (!port)
+ return 1;
+
+ if (daemon)
+ return start_daemon(*logger, *cfg, std::move(port), verbose) ? 0 : 1;
+
+ return run(*logger, *cfg, std::move(port)) ? 0 : 1;
}
diff --git a/src/str.cc b/src/str.cc
index 44db3a6..e5b70ed 100644
--- a/src/str.cc
+++ b/src/str.cc
@@ -40,6 +40,32 @@ std::vector<std::string_view> split(std::string_view str, char separator,
return vec;
}
+void split(std::string_view str, std::vector<std::string_view>& out,
+ std::string_view separator, bool keep_empty) {
+ out.clear();
+
+ size_t offset = 0;
+ while (true) {
+ auto next = str.find(separator, offset);
+ if (next == std::string_view::npos) {
+ if (keep_empty || offset < str.size())
+ out.push_back(str.substr(offset));
+ break;
+ }
+ if (keep_empty || offset < next)
+ out.push_back(str.substr(offset, next - offset));
+ offset = next + separator.size();
+ }
+}
+
+std::vector<std::string_view> split(std::string_view str,
+ std::string_view separator,
+ bool keep_empty) {
+ std::vector<std::string_view> vec;
+ split(str, vec, separator, keep_empty);
+ return vec;
+}
+
std::string_view trim(std::string_view str) {
size_t s = 0;
size_t e = str.size();
diff --git a/src/str.hh b/src/str.hh
index e1ee549..1c618ee 100644
--- a/src/str.hh
+++ b/src/str.hh
@@ -13,6 +13,13 @@ void split(std::string_view str, std::vector<std::string_view>& out,
char separator = ' ',
bool keep_empty = false);
+void split(std::string_view str, std::vector<std::string_view>& out,
+ std::string_view separator, bool keep_empty = false);
+
+[[nodiscard]] std::vector<std::string_view> split(std::string_view str,
+ std::string_view separator,
+ bool keep_empty = false);
+
[[nodiscard]]
std::string_view trim(std::string_view str);