diff --git a/CMakeLists.txt b/CMakeLists.txt index 04193c4d..3d621951 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,4 +121,8 @@ endif() install(FILES docs/man/tilemaker.1 DESTINATION share/man/man1) -install(TARGETS tilemaker RUNTIME DESTINATION bin) +add_executable(tilemaker-server server/server.cpp) +target_include_directories(tilemaker-server PRIVATE include) +target_link_libraries(tilemaker-server ${THREAD_LIB} ${CMAKE_DL_LIBS} SQLite::SQLite3 Boost::filesystem Boost::program_options) + +install(TARGETS tilemaker tilemaker-server RUNTIME DESTINATION bin) diff --git a/Dockerfile b/Dockerfile index 1046e74e..8158ef0c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,12 +24,14 @@ COPY CMakeLists.txt / COPY cmake /cmake COPY src /src COPY include /include +COPY server /server WORKDIR /build RUN cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++ .. RUN cmake --build . RUN strip tilemaker +RUN strip tilemaker-server FROM debian:bullseye-slim RUN apt-get update && \ diff --git a/Makefile b/Makefile index 0b3db1de..ca6eb3e1 100644 --- a/Makefile +++ b/Makefile @@ -90,7 +90,7 @@ INC := -I$(PLATFORM_PATH)/include -isystem ./include -I./src $(LUA_CFLAGS) # Targets .PHONY: test -all: tilemaker +all: tilemaker server tilemaker: \ src/attribute_store.o \ @@ -186,6 +186,10 @@ test_pbf_reader: \ test/pbf_reader.test.o $(CXX) $(CXXFLAGS) -o test.pbf_reader $^ $(INC) $(LIB) $(LDFLAGS) && ./test.pbf_reader +server: \ + server/server.o + $(CXX) $(CXXFLAGS) -o tilemaker-server $^ $(INC) $(LIB) $(LDFLAGS) + %.o: %.cpp $(CXX) $(CXXFLAGS) -o $@ -c $< $(INC) @@ -195,10 +199,11 @@ test_pbf_reader: \ install: install -m 0755 -d $(DESTDIR)$(prefix)/bin/ install -m 0755 tilemaker $(DESTDIR)$(prefix)/bin/ + install -m 0755 tilemaker-server $(DESTDIR)$(prefix)/bin/ install -m 0755 -d ${DESTDIR}${MANPREFIX}/man1/ install docs/man/tilemaker.1 ${DESTDIR}${MANPREFIX}/man1/ clean: - rm -f tilemaker src/*.o src/external/*.o include/*.o include/*.pb.h test/*.o + rm -f tilemaker tilemaker-server src/*.o src/external/*.o include/*.o include/*.pb.h server/*.o test/*.o .PHONY: install diff --git a/server/Simple-Web-Server/LICENSE b/server/Simple-Web-Server/LICENSE new file mode 100644 index 00000000..cecca180 --- /dev/null +++ b/server/Simple-Web-Server/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014-2020 Ole Christian Eidheim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/server/Simple-Web-Server/asio_compatibility.hpp b/server/Simple-Web-Server/asio_compatibility.hpp new file mode 100644 index 00000000..5a7007de --- /dev/null +++ b/server/Simple-Web-Server/asio_compatibility.hpp @@ -0,0 +1,85 @@ +#ifndef SIMPLE_WEB_ASIO_COMPATIBILITY_HPP +#define SIMPLE_WEB_ASIO_COMPATIBILITY_HPP + +#include + +#ifdef ASIO_STANDALONE +#include +#include +namespace SimpleWeb { + namespace error = asio::error; + using error_code = std::error_code; + using errc = std::errc; + using system_error = std::system_error; + namespace make_error_code = std; +} // namespace SimpleWeb +#else +#include +#include +namespace SimpleWeb { + namespace asio = boost::asio; + namespace error = asio::error; + using error_code = boost::system::error_code; + namespace errc = boost::system::errc; + using system_error = boost::system::system_error; + namespace make_error_code = boost::system::errc; +} // namespace SimpleWeb +#endif + +namespace SimpleWeb { +#if(ASIO_STANDALONE && ASIO_VERSION >= 101300) || BOOST_ASIO_VERSION >= 101300 + using io_context = asio::io_context; + using resolver_results = asio::ip::tcp::resolver::results_type; + using async_connect_endpoint = asio::ip::tcp::endpoint; + + template + inline void post(io_context &context, handler_type &&handler) { + asio::post(context, std::forward(handler)); + } + inline void restart(io_context &context) noexcept { + context.restart(); + } + inline asio::ip::address make_address(const std::string &str) noexcept { + return asio::ip::make_address(str); + } + template + std::unique_ptr make_steady_timer(socket_type &socket, std::chrono::duration duration) { + return std::unique_ptr(new asio::steady_timer(socket.get_executor(), duration)); + } + template + void async_resolve(asio::ip::tcp::resolver &resolver, const std::pair &host_port, handler_type &&handler) { + resolver.async_resolve(host_port.first, host_port.second, std::forward(handler)); + } + inline asio::executor_work_guard make_work_guard(io_context &context) { + return asio::make_work_guard(context); + } +#else + using io_context = asio::io_service; + using resolver_results = asio::ip::tcp::resolver::iterator; + using async_connect_endpoint = asio::ip::tcp::resolver::iterator; + + template + inline void post(io_context &context, handler_type &&handler) { + context.post(std::forward(handler)); + } + inline void restart(io_context &context) noexcept { + context.reset(); + } + inline asio::ip::address make_address(const std::string &str) noexcept { + return asio::ip::address::from_string(str); + } + template + std::unique_ptr make_steady_timer(socket_type &socket, std::chrono::duration duration) { + return std::unique_ptr(new asio::steady_timer(socket.get_io_service(), duration)); + } + template + void async_resolve(asio::ip::tcp::resolver &resolver, const std::pair &host_port, handler_type &&handler) { + resolver.async_resolve(asio::ip::tcp::resolver::query(host_port.first, host_port.second), std::forward(handler)); + } + inline io_context::work make_work_guard(io_context &context) { + return io_context::work(context); + } +#endif +} // namespace SimpleWeb + +#endif /* SIMPLE_WEB_ASIO_COMPATIBILITY_HPP */ diff --git a/server/Simple-Web-Server/mutex.hpp b/server/Simple-Web-Server/mutex.hpp new file mode 100644 index 00000000..27118502 --- /dev/null +++ b/server/Simple-Web-Server/mutex.hpp @@ -0,0 +1,107 @@ +// Based on https://clang.llvm.org/docs/ThreadSafetyAnalysis.html +#ifndef SIMPLE_WEB_MUTEX_HPP +#define SIMPLE_WEB_MUTEX_HPP + +#include + +// Enable thread safety attributes only with clang. +#if defined(__clang__) && (!defined(SWIG)) +#define THREAD_ANNOTATION_ATTRIBUTE__(x) __attribute__((x)) +#else +#define THREAD_ANNOTATION_ATTRIBUTE__(x) // no-op +#endif + +#define CAPABILITY(x) \ + THREAD_ANNOTATION_ATTRIBUTE__(capability(x)) + +#define SCOPED_CAPABILITY \ + THREAD_ANNOTATION_ATTRIBUTE__(scoped_lockable) + +#define GUARDED_BY(x) \ + THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x)) + +#define PT_GUARDED_BY(x) \ + THREAD_ANNOTATION_ATTRIBUTE__(pt_guarded_by(x)) + +#define ACQUIRED_BEFORE(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(acquired_before(__VA_ARGS__)) + +#define ACQUIRED_AFTER(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(acquired_after(__VA_ARGS__)) + +#define REQUIRES(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(requires_capability(__VA_ARGS__)) + +#define REQUIRES_SHARED(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(requires_shared_capability(__VA_ARGS__)) + +#define ACQUIRE(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(acquire_capability(__VA_ARGS__)) + +#define ACQUIRE_SHARED(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(acquire_shared_capability(__VA_ARGS__)) + +#define RELEASE(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(release_capability(__VA_ARGS__)) + +#define RELEASE_SHARED(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(release_shared_capability(__VA_ARGS__)) + +#define TRY_ACQUIRE(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_capability(__VA_ARGS__)) + +#define TRY_ACQUIRE_SHARED(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_shared_capability(__VA_ARGS__)) + +#define EXCLUDES(...) \ + THREAD_ANNOTATION_ATTRIBUTE__(locks_excluded(__VA_ARGS__)) + +#define ASSERT_CAPABILITY(x) \ + THREAD_ANNOTATION_ATTRIBUTE__(assert_capability(x)) + +#define ASSERT_SHARED_CAPABILITY(x) \ + THREAD_ANNOTATION_ATTRIBUTE__(assert_shared_capability(x)) + +#define RETURN_CAPABILITY(x) \ + THREAD_ANNOTATION_ATTRIBUTE__(lock_returned(x)) + +#define NO_THREAD_SAFETY_ANALYSIS \ + THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis) + +namespace SimpleWeb { + /// Mutex class that is annotated for Clang Thread Safety Analysis. + class CAPABILITY("mutex") Mutex { + std::mutex mutex; + + public: + void lock() ACQUIRE() { + mutex.lock(); + } + + void unlock() RELEASE() { + mutex.unlock(); + } + }; + + /// Scoped mutex guard class that is annotated for Clang Thread Safety Analysis. + class SCOPED_CAPABILITY LockGuard { + Mutex &mutex; + bool locked = true; + + public: + LockGuard(Mutex &mutex_) ACQUIRE(mutex_) : mutex(mutex_) { + mutex.lock(); + } + void unlock() RELEASE() { + mutex.unlock(); + locked = false; + } + ~LockGuard() RELEASE() { + if(locked) + mutex.unlock(); + } + }; + +} // namespace SimpleWeb + +#endif // SIMPLE_WEB_MUTEX_HPP diff --git a/server/Simple-Web-Server/server_http.hpp b/server/Simple-Web-Server/server_http.hpp new file mode 100644 index 00000000..348e9db5 --- /dev/null +++ b/server/Simple-Web-Server/server_http.hpp @@ -0,0 +1,827 @@ +#ifndef SIMPLE_WEB_SERVER_HTTP_HPP +#define SIMPLE_WEB_SERVER_HTTP_HPP + +#include "asio_compatibility.hpp" +#include "mutex.hpp" +#include "utility.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +// Late 2017 TODO: remove the following checks and always use std::regex +#ifdef USE_BOOST_REGEX +#include +namespace SimpleWeb { + namespace regex = boost; +} +#else +#include +namespace SimpleWeb { + namespace regex = std; +} +#endif + +namespace SimpleWeb { + template + class Server; + + template + class ServerBase { + protected: + class Connection; + class Session; + + public: + /// Response class where the content of the response is sent to client when the object is about to be destroyed. + class Response : public std::enable_shared_from_this, public std::ostream { + friend class ServerBase; + friend class Server; + + std::unique_ptr streambuf = std::unique_ptr(new asio::streambuf()); + + std::shared_ptr session; + long timeout_content; + + Mutex send_queue_mutex; + std::list, std::function>> send_queue GUARDED_BY(send_queue_mutex); + + Response(std::shared_ptr session_, long timeout_content) noexcept : std::ostream(nullptr), session(std::move(session_)), timeout_content(timeout_content) { + rdbuf(streambuf.get()); + } + + template + void write_header(const CaseInsensitiveMultimap &header, size_type size) { + bool content_length_written = false; + bool chunked_transfer_encoding = false; + for(auto &field : header) { + if(!content_length_written && case_insensitive_equal(field.first, "content-length")) + content_length_written = true; + else if(!chunked_transfer_encoding && case_insensitive_equal(field.first, "transfer-encoding") && case_insensitive_equal(field.second, "chunked")) + chunked_transfer_encoding = true; + + *this << field.first << ": " << field.second << "\r\n"; + } + if(!content_length_written && !chunked_transfer_encoding && !close_connection_after_response) + *this << "Content-Length: " << size << "\r\n\r\n"; + else + *this << "\r\n"; + } + + void send_from_queue() REQUIRES(send_queue_mutex) { + auto self = this->shared_from_this(); + asio::async_write(*self->session->connection->socket, *send_queue.begin()->first, [self](const error_code &ec, std::size_t /*bytes_transferred*/) { + auto lock = self->session->connection->handler_runner->continue_lock(); + if(!lock) + return; + { + LockGuard lock(self->send_queue_mutex); + if(!ec) { + auto it = self->send_queue.begin(); + auto callback = std::move(it->second); + self->send_queue.erase(it); + if(self->send_queue.size() > 0) + self->send_from_queue(); + + lock.unlock(); + if(callback) + callback(ec); + } + else { + // All handlers in the queue is called with ec: + std::vector> callbacks; + for(auto &pair : self->send_queue) { + if(pair.second) + callbacks.emplace_back(std::move(pair.second)); + } + self->send_queue.clear(); + + lock.unlock(); + for(auto &callback : callbacks) + callback(ec); + } + } + }); + } + + void send_on_delete(const std::function &callback = nullptr) noexcept { + auto self = this->shared_from_this(); // Keep Response instance alive through the following async_write + asio::async_write(*session->connection->socket, *streambuf, [self, callback](const error_code &ec, std::size_t /*bytes_transferred*/) { + auto lock = self->session->connection->handler_runner->continue_lock(); + if(!lock) + return; + if(callback) + callback(ec); + }); + } + + public: + std::size_t size() noexcept { + return streambuf->size(); + } + + /// Send the content of the response stream to client. The callback is called when the send has completed. + /// + /// Use this function if you need to recursively send parts of a longer message, or when using server-sent events. + void send(std::function callback = nullptr) noexcept { + std::shared_ptr streambuf = std::move(this->streambuf); + this->streambuf = std::unique_ptr(new asio::streambuf()); + rdbuf(this->streambuf.get()); + + LockGuard lock(send_queue_mutex); + send_queue.emplace_back(std::move(streambuf), std::move(callback)); + if(send_queue.size() == 1) + send_from_queue(); + } + + /// Write directly to stream buffer using std::ostream::write. + void write(const char_type *ptr, std::streamsize n) { + std::ostream::write(ptr, n); + } + + /// Convenience function for writing status line, potential header fields, and empty content. + void write(StatusCode status_code = StatusCode::success_ok, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { + *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; + write_header(header, 0); + } + + /// Convenience function for writing status line, header fields, and content. + void write(StatusCode status_code, string_view content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { + *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; + write_header(header, content.size()); + if(!content.empty()) + *this << content; + } + + /// Convenience function for writing status line, header fields, and content. + void write(StatusCode status_code, std::istream &content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { + *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; + content.seekg(0, std::ios::end); + auto size = content.tellg(); + content.seekg(0, std::ios::beg); + write_header(header, size); + if(size) + *this << content.rdbuf(); + } + + /// Convenience function for writing success status line, header fields, and content. + void write(string_view content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { + write(StatusCode::success_ok, content, header); + } + + /// Convenience function for writing success status line, header fields, and content. + void write(std::istream &content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { + write(StatusCode::success_ok, content, header); + } + + /// Convenience function for writing success status line, and header fields. + void write(const CaseInsensitiveMultimap &header) { + write(StatusCode::success_ok, std::string(), header); + } + + /// If set to true, force server to close the connection after the response have been sent. + /// + /// This is useful when implementing a HTTP/1.0-server sending content + /// without specifying the content length. + bool close_connection_after_response = false; + }; + + class Content : public std::istream { + friend class ServerBase; + + public: + std::size_t size() noexcept { + return streambuf.size(); + } + /// Convenience function to return content as std::string. + std::string string() noexcept { + return std::string(asio::buffers_begin(streambuf.data()), asio::buffers_end(streambuf.data())); + } + + private: + asio::streambuf &streambuf; + Content(asio::streambuf &streambuf) noexcept : std::istream(&streambuf), streambuf(streambuf) {} + }; + + class Request { + friend class ServerBase; + friend class Server; + friend class Session; + + asio::streambuf streambuf; + std::weak_ptr connection; + std::string optimization = std::to_string(0); // TODO: figure out what goes wrong in gcc optimization without this line + + Request(std::size_t max_request_streambuf_size, const std::shared_ptr &connection_) noexcept : streambuf(max_request_streambuf_size), connection(connection_), content(streambuf) {} + + public: + std::string method, path, query_string, http_version; + + Content content; + + CaseInsensitiveMultimap header; + + /// The result of the resource regular expression match of the request path. + regex::smatch path_match; + + /// The time point when the request header was fully read. + std::chrono::system_clock::time_point header_read_time; + + asio::ip::tcp::endpoint remote_endpoint() const noexcept { + try { + if(auto connection = this->connection.lock()) + return connection->socket->lowest_layer().remote_endpoint(); + } + catch(...) { + } + return asio::ip::tcp::endpoint(); + } + + asio::ip::tcp::endpoint local_endpoint() const noexcept { + try { + if(auto connection = this->connection.lock()) + return connection->socket->lowest_layer().local_endpoint(); + } + catch(...) { + } + return asio::ip::tcp::endpoint(); + } + + /// Deprecated, please use remote_endpoint().address().to_string() instead. + DEPRECATED std::string remote_endpoint_address() const noexcept { + try { + if(auto connection = this->connection.lock()) + return connection->socket->lowest_layer().remote_endpoint().address().to_string(); + } + catch(...) { + } + return std::string(); + } + + /// Deprecated, please use remote_endpoint().port() instead. + DEPRECATED unsigned short remote_endpoint_port() const noexcept { + try { + if(auto connection = this->connection.lock()) + return connection->socket->lowest_layer().remote_endpoint().port(); + } + catch(...) { + } + return 0; + } + + /// Returns query keys with percent-decoded values. + CaseInsensitiveMultimap parse_query_string() const noexcept { + return SimpleWeb::QueryString::parse(query_string); + } + }; + + protected: + class Connection : public std::enable_shared_from_this { + public: + template + Connection(std::shared_ptr handler_runner_, Args &&...args) noexcept : handler_runner(std::move(handler_runner_)), socket(new socket_type(std::forward(args)...)) {} + + std::shared_ptr handler_runner; + + std::unique_ptr socket; // Socket must be unique_ptr since asio::ssl::stream is not movable + + std::unique_ptr timer; + + void close() noexcept { + error_code ec; + socket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, ec); + socket->lowest_layer().cancel(ec); + } + + void set_timeout(long seconds) noexcept { + if(seconds == 0) { + timer = nullptr; + return; + } + + timer = make_steady_timer(*socket, std::chrono::seconds(seconds)); + std::weak_ptr self_weak(this->shared_from_this()); // To avoid keeping Connection instance alive longer than needed + timer->async_wait([self_weak](const error_code &ec) { + if(!ec) { + if(auto self = self_weak.lock()) + self->close(); + } + }); + } + + void cancel_timeout() noexcept { + if(timer) { + try { + timer->cancel(); + } + catch(...) { + } + } + } + }; + + class Session { + public: + Session(std::size_t max_request_streambuf_size, std::shared_ptr connection_) noexcept : connection(std::move(connection_)), request(new Request(max_request_streambuf_size, connection)) {} + + std::shared_ptr connection; + std::shared_ptr request; + }; + + public: + class Config { + friend class ServerBase; + + Config(unsigned short port) noexcept : port(port) {} + + public: + /// Port number to use. Defaults to 80 for HTTP and 443 for HTTPS. Set to 0 get an assigned port. + unsigned short port; + /// If io_service is not set, number of threads that the server will use when start() is called. + /// Defaults to 1 thread. + std::size_t thread_pool_size = 1; + /// Timeout on request completion. Defaults to 5 seconds. + long timeout_request = 5; + /// Timeout on request/response content completion. Defaults to 300 seconds. + long timeout_content = 300; + /// Maximum size of request stream buffer. Defaults to architecture maximum. + /// Reaching this limit will result in a message_size error code. + std::size_t max_request_streambuf_size = (std::numeric_limits::max)(); + /// IPv4 address in dotted decimal form or IPv6 address in hexadecimal notation. + /// If empty, the address will be any address. + std::string address; + /// Set to false to avoid binding the socket to an address that is already in use. Defaults to true. + bool reuse_address = true; + /// Make use of RFC 7413 or TCP Fast Open (TFO) + bool fast_open = false; + }; + /// Set before calling start(). + Config config; + + private: + class regex_orderable : public regex::regex { + public: + std::string str; + + regex_orderable(const char *regex_cstr) : regex::regex(regex_cstr), str(regex_cstr) {} + regex_orderable(std::string regex_str_) : regex::regex(regex_str_), str(std::move(regex_str_)) {} + bool operator<(const regex_orderable &rhs) const noexcept { + return str < rhs.str; + } + }; + + public: + /// Use this container to add resources for specific request paths depending on the given regex and method. + /// Warning: do not add or remove resources after start() is called + std::map::Response>, std::shared_ptr::Request>)>>> resource; + + /// If the request path does not match a resource regex, this function is called. + std::map::Response>, std::shared_ptr::Request>)>> default_resource; + + /// Called when an error occurs. + std::function::Request>, const error_code &)> on_error; + + /// Called on upgrade requests. + std::function &, std::shared_ptr::Request>)> on_upgrade; + + /// If you want to reuse an already created asio::io_service, store its pointer here before calling start(). + std::shared_ptr io_service; + + /// Start the server. + /// If io_service is not set, an internal io_service is created instead. + /// The callback argument is called after the server is accepting connections, + /// where its parameter contains the assigned port. + void start(const std::function &callback = nullptr) { + std::unique_lock lock(start_stop_mutex); + + asio::ip::tcp::endpoint endpoint; + if(!config.address.empty()) + endpoint = asio::ip::tcp::endpoint(make_address(config.address), config.port); + else + endpoint = asio::ip::tcp::endpoint(asio::ip::tcp::v6(), config.port); + + if(!io_service) { + io_service = std::make_shared(); + internal_io_service = true; + } + + if(!acceptor) + acceptor = std::unique_ptr(new asio::ip::tcp::acceptor(*io_service)); + try { + acceptor->open(endpoint.protocol()); + } + catch(const system_error &error) { + if(error.code() == asio::error::address_family_not_supported && config.address.empty()) { + endpoint = asio::ip::tcp::endpoint(asio::ip::tcp::v4(), config.port); + acceptor->open(endpoint.protocol()); + } + else + throw; + } + acceptor->set_option(asio::socket_base::reuse_address(config.reuse_address)); + if(config.fast_open) { +#if defined(__linux__) && defined(TCP_FASTOPEN) + const int qlen = 5; // This seems to be the value that is used in other examples. + error_code ec; + acceptor->set_option(asio::detail::socket_option::integer(qlen), ec); +#endif // End Linux + } + acceptor->bind(endpoint); + + after_bind(); + + auto port = acceptor->local_endpoint().port(); + + acceptor->listen(); + accept(); + + if(internal_io_service && io_service->stopped()) + restart(*io_service); + + if(callback) + post(*io_service, [callback, port] { + callback(port); + }); + + if(internal_io_service) { + // If thread_pool_size>1, start m_io_service.run() in (thread_pool_size-1) threads for thread-pooling + threads.clear(); + for(std::size_t c = 1; c < config.thread_pool_size; c++) { + threads.emplace_back([this]() { + this->io_service->run(); + }); + } + + lock.unlock(); + + // Main thread + if(config.thread_pool_size > 0) + io_service->run(); + + lock.lock(); + + // Wait for the rest of the threads, if any, to finish as well + for(auto &t : threads) + t.join(); + } + } + + /// Stop accepting new requests, and close current connections. + void stop() noexcept { + std::lock_guard lock(start_stop_mutex); + + if(acceptor) { + error_code ec; + acceptor->close(ec); + + { + LockGuard lock(connections->mutex); + for(auto &connection : connections->set) + connection->close(); + connections->set.clear(); + } + + if(internal_io_service) + io_service->stop(); + } + } + + virtual ~ServerBase() noexcept { + handler_runner->stop(); + stop(); + } + + protected: + std::mutex start_stop_mutex; + + bool internal_io_service = false; + + std::unique_ptr acceptor; + std::vector threads; + + struct Connections { + Mutex mutex; + std::unordered_set set GUARDED_BY(mutex); + }; + std::shared_ptr connections; + + std::shared_ptr handler_runner; + + ServerBase(unsigned short port) noexcept : config(port), connections(new Connections()), handler_runner(new ScopeRunner()) {} + + virtual void after_bind() {} + virtual void accept() = 0; + + template + std::shared_ptr create_connection(Args &&...args) noexcept { + auto connections = this->connections; + auto connection = std::shared_ptr(new Connection(handler_runner, std::forward(args)...), [connections](Connection *connection) { + { + LockGuard lock(connections->mutex); + auto it = connections->set.find(connection); + if(it != connections->set.end()) + connections->set.erase(it); + } + delete connection; + }); + { + LockGuard lock(connections->mutex); + connections->set.emplace(connection.get()); + } + return connection; + } + + void read(const std::shared_ptr &session) { + session->connection->set_timeout(config.timeout_request); + asio::async_read_until(*session->connection->socket, session->request->streambuf, "\r\n\r\n", [this, session](const error_code &ec, std::size_t bytes_transferred) { + session->connection->set_timeout(config.timeout_content); + auto lock = session->connection->handler_runner->continue_lock(); + if(!lock) + return; + session->request->header_read_time = std::chrono::system_clock::now(); + + if(!ec) { + // request->streambuf.size() is not necessarily the same as bytes_transferred, from Boost-docs: + // "After a successful async_read_until operation, the streambuf may contain additional data beyond the delimiter" + // The chosen solution is to extract lines from the stream directly when parsing the header. What is left of the + // streambuf (maybe some bytes of the content) is appended to in the async_read-function below (for retrieving content). + std::size_t num_additional_bytes = session->request->streambuf.size() - bytes_transferred; + + if(!RequestMessage::parse(session->request->content, session->request->method, session->request->path, + session->request->query_string, session->request->http_version, session->request->header)) { + if(this->on_error) + this->on_error(session->request, make_error_code::make_error_code(errc::protocol_error)); + return; + } + + // If content, read that as well + auto header_it = session->request->header.find("Content-Length"); + if(header_it != session->request->header.end()) { + unsigned long long content_length = 0; + try { + content_length = std::stoull(header_it->second); + } + catch(const std::exception &) { + if(this->on_error) + this->on_error(session->request, make_error_code::make_error_code(errc::protocol_error)); + return; + } + if(content_length > session->request->streambuf.max_size()) { + auto response = std::shared_ptr(new Response(session, this->config.timeout_content)); + response->write(StatusCode::client_error_payload_too_large); + if(this->on_error) + this->on_error(session->request, make_error_code::make_error_code(errc::message_size)); + return; + } + if(content_length > num_additional_bytes) { + asio::async_read(*session->connection->socket, session->request->streambuf, asio::transfer_exactly(content_length - num_additional_bytes), [this, session](const error_code &ec, std::size_t /*bytes_transferred*/) { + auto lock = session->connection->handler_runner->continue_lock(); + if(!lock) + return; + + if(!ec) + this->find_resource(session); + else if(this->on_error) + this->on_error(session->request, ec); + }); + } + else + this->find_resource(session); + } + else if((header_it = session->request->header.find("Transfer-Encoding")) != session->request->header.end() && header_it->second == "chunked") { + // Expect hex number to not exceed 16 bytes (64-bit number), but take into account previous additional read bytes + auto chunk_size_streambuf = std::make_shared(std::max(16 + 2, session->request->streambuf.size())); + + // Move leftover bytes + auto &source = session->request->streambuf; + auto &target = *chunk_size_streambuf; + target.commit(asio::buffer_copy(target.prepare(source.size()), source.data())); + source.consume(source.size()); + + this->read_chunked_transfer_encoded(session, chunk_size_streambuf); + } + else + this->find_resource(session); + } + else if(this->on_error) + this->on_error(session->request, ec); + }); + } + + void read_chunked_transfer_encoded(const std::shared_ptr &session, const std::shared_ptr &chunk_size_streambuf) { + asio::async_read_until(*session->connection->socket, *chunk_size_streambuf, "\r\n", [this, session, chunk_size_streambuf](const error_code &ec, size_t bytes_transferred) { + auto lock = session->connection->handler_runner->continue_lock(); + if(!lock) + return; + + if(!ec) { + std::istream istream(chunk_size_streambuf.get()); + std::string line; + std::getline(istream, line); + bytes_transferred -= line.size() + 1; + unsigned long chunk_size = 0; + try { + chunk_size = std::stoul(line, 0, 16); + } + catch(...) { + if(this->on_error) + this->on_error(session->request, make_error_code::make_error_code(errc::protocol_error)); + return; + } + + if(chunk_size == 0) { + this->find_resource(session); + return; + } + + if(chunk_size + session->request->streambuf.size() > session->request->streambuf.max_size()) { + auto response = std::shared_ptr(new Response(session, this->config.timeout_content)); + response->write(StatusCode::client_error_payload_too_large); + if(this->on_error) + this->on_error(session->request, make_error_code::make_error_code(errc::message_size)); + return; + } + + auto num_additional_bytes = chunk_size_streambuf->size() - bytes_transferred; + + auto bytes_to_move = std::min(chunk_size, num_additional_bytes); + if(bytes_to_move > 0) { + // Move leftover bytes + auto &source = *chunk_size_streambuf; + auto &target = session->request->streambuf; + target.commit(asio::buffer_copy(target.prepare(bytes_to_move), source.data(), bytes_to_move)); + source.consume(bytes_to_move); + } + + if(chunk_size > num_additional_bytes) { + asio::async_read(*session->connection->socket, session->request->streambuf, asio::transfer_exactly(chunk_size - num_additional_bytes), [this, session, chunk_size_streambuf](const error_code &ec, size_t /*bytes_transferred*/) { + auto lock = session->connection->handler_runner->continue_lock(); + if(!lock) + return; + + if(!ec) { + // Remove "\r\n" + auto null_buffer = std::make_shared(2); + asio::async_read(*session->connection->socket, *null_buffer, asio::transfer_exactly(2), [this, session, chunk_size_streambuf, null_buffer](const error_code &ec, size_t /*bytes_transferred*/) { + auto lock = session->connection->handler_runner->continue_lock(); + if(!lock) + return; + if(!ec) + read_chunked_transfer_encoded(session, chunk_size_streambuf); + else + this->on_error(session->request, ec); + }); + } + else if(this->on_error) + this->on_error(session->request, ec); + }); + } + else if(2 + chunk_size > num_additional_bytes) { // If only end of chunk remains unread (\n or \r\n) + // Remove "\r\n" + if(2 + chunk_size - num_additional_bytes == 1) + istream.get(); + auto null_buffer = std::make_shared(2); + asio::async_read(*session->connection->socket, *null_buffer, asio::transfer_exactly(2 + chunk_size - num_additional_bytes), [this, session, chunk_size_streambuf, null_buffer](const error_code &ec, size_t /*bytes_transferred*/) { + auto lock = session->connection->handler_runner->continue_lock(); + if(!lock) + return; + if(!ec) + read_chunked_transfer_encoded(session, chunk_size_streambuf); + else + this->on_error(session->request, ec); + }); + } + else { + // Remove "\r\n" + istream.get(); + istream.get(); + + read_chunked_transfer_encoded(session, chunk_size_streambuf); + } + } + else if(this->on_error) + this->on_error(session->request, ec); + }); + } + + void find_resource(const std::shared_ptr &session) { + // Upgrade connection + if(on_upgrade) { + auto it = session->request->header.find("Upgrade"); + if(it != session->request->header.end()) { + // remove connection from connections + { + LockGuard lock(connections->mutex); + auto it = connections->set.find(session->connection.get()); + if(it != connections->set.end()) + connections->set.erase(it); + } + + on_upgrade(session->connection->socket, session->request); + return; + } + } + // Find path- and method-match, and call write + for(auto ®ex_method : resource) { + auto it = regex_method.second.find(session->request->method); + if(it != regex_method.second.end()) { + regex::smatch sm_res; + if(regex::regex_match(session->request->path, sm_res, regex_method.first)) { + session->request->path_match = std::move(sm_res); + write(session, it->second); + return; + } + } + } + auto it = default_resource.find(session->request->method); + if(it != default_resource.end()) + write(session, it->second); + } + + void write(const std::shared_ptr &session, + std::function::Response>, std::shared_ptr::Request>)> &resource_function) { + auto response = std::shared_ptr(new Response(session, config.timeout_content), [this](Response *response_ptr) { + auto response = std::shared_ptr(response_ptr); + response->send_on_delete([this, response](const error_code &ec) { + response->session->connection->cancel_timeout(); + if(!ec) { + if(response->close_connection_after_response) + return; + + auto range = response->session->request->header.equal_range("Connection"); + for(auto it = range.first; it != range.second; it++) { + if(case_insensitive_equal(it->second, "close")) + return; + else if(case_insensitive_equal(it->second, "keep-alive")) { + auto new_session = std::make_shared(this->config.max_request_streambuf_size, response->session->connection); + this->read(new_session); + return; + } + } + if(response->session->request->http_version >= "1.1") { + auto new_session = std::make_shared(this->config.max_request_streambuf_size, response->session->connection); + this->read(new_session); + return; + } + } + else if(this->on_error) + this->on_error(response->session->request, ec); + }); + }); + + try { + resource_function(response, session->request); + } + catch(const std::exception &) { + if(on_error) + on_error(session->request, make_error_code::make_error_code(errc::operation_canceled)); + return; + } + } + }; + + template + class Server : public ServerBase {}; + + using HTTP = asio::ip::tcp::socket; + + template <> + class Server : public ServerBase { + public: + /// Constructs a server object. + Server() noexcept : ServerBase::ServerBase(80) {} + + protected: + void accept() override { + auto connection = create_connection(*io_service); + + acceptor->async_accept(*connection->socket, [this, connection](const error_code &ec) { + auto lock = connection->handler_runner->continue_lock(); + if(!lock) + return; + + // Immediately start accepting a new connection (unless io_service has been stopped) + if(ec != error::operation_aborted) + this->accept(); + + auto session = std::make_shared(config.max_request_streambuf_size, connection); + + if(!ec) { + asio::ip::tcp::no_delay option(true); + error_code ec; + session->connection->socket->set_option(option, ec); + + this->read(session); + } + else if(this->on_error) + this->on_error(session->request, ec); + }); + } + }; +} // namespace SimpleWeb + +#endif /* SIMPLE_WEB_SERVER_HTTP_HPP */ diff --git a/server/Simple-Web-Server/status_code.hpp b/server/Simple-Web-Server/status_code.hpp new file mode 100644 index 00000000..9f9ecc56 --- /dev/null +++ b/server/Simple-Web-Server/status_code.hpp @@ -0,0 +1,176 @@ +#ifndef SIMPLE_WEB_STATUS_CODE_HPP +#define SIMPLE_WEB_STATUS_CODE_HPP + +#include +#include +#include +#include +#include + +namespace SimpleWeb { + enum class StatusCode { + unknown = 0, + information_continue = 100, + information_switching_protocols, + information_processing, + success_ok = 200, + success_created, + success_accepted, + success_non_authoritative_information, + success_no_content, + success_reset_content, + success_partial_content, + success_multi_status, + success_already_reported, + success_im_used = 226, + redirection_multiple_choices = 300, + redirection_moved_permanently, + redirection_found, + redirection_see_other, + redirection_not_modified, + redirection_use_proxy, + redirection_switch_proxy, + redirection_temporary_redirect, + redirection_permanent_redirect, + client_error_bad_request = 400, + client_error_unauthorized, + client_error_payment_required, + client_error_forbidden, + client_error_not_found, + client_error_method_not_allowed, + client_error_not_acceptable, + client_error_proxy_authentication_required, + client_error_request_timeout, + client_error_conflict, + client_error_gone, + client_error_length_required, + client_error_precondition_failed, + client_error_payload_too_large, + client_error_uri_too_long, + client_error_unsupported_media_type, + client_error_range_not_satisfiable, + client_error_expectation_failed, + client_error_im_a_teapot, + client_error_misdirection_required = 421, + client_error_unprocessable_entity, + client_error_locked, + client_error_failed_dependency, + client_error_upgrade_required = 426, + client_error_precondition_required = 428, + client_error_too_many_requests, + client_error_request_header_fields_too_large = 431, + client_error_unavailable_for_legal_reasons = 451, + server_error_internal_server_error = 500, + server_error_not_implemented, + server_error_bad_gateway, + server_error_service_unavailable, + server_error_gateway_timeout, + server_error_http_version_not_supported, + server_error_variant_also_negotiates, + server_error_insufficient_storage, + server_error_loop_detected, + server_error_not_extended = 510, + server_error_network_authentication_required + }; + + inline const std::map &status_code_strings() { + static const std::map status_code_strings = { + {StatusCode::unknown, ""}, + {StatusCode::information_continue, "100 Continue"}, + {StatusCode::information_switching_protocols, "101 Switching Protocols"}, + {StatusCode::information_processing, "102 Processing"}, + {StatusCode::success_ok, "200 OK"}, + {StatusCode::success_created, "201 Created"}, + {StatusCode::success_accepted, "202 Accepted"}, + {StatusCode::success_non_authoritative_information, "203 Non-Authoritative Information"}, + {StatusCode::success_no_content, "204 No Content"}, + {StatusCode::success_reset_content, "205 Reset Content"}, + {StatusCode::success_partial_content, "206 Partial Content"}, + {StatusCode::success_multi_status, "207 Multi-Status"}, + {StatusCode::success_already_reported, "208 Already Reported"}, + {StatusCode::success_im_used, "226 IM Used"}, + {StatusCode::redirection_multiple_choices, "300 Multiple Choices"}, + {StatusCode::redirection_moved_permanently, "301 Moved Permanently"}, + {StatusCode::redirection_found, "302 Found"}, + {StatusCode::redirection_see_other, "303 See Other"}, + {StatusCode::redirection_not_modified, "304 Not Modified"}, + {StatusCode::redirection_use_proxy, "305 Use Proxy"}, + {StatusCode::redirection_switch_proxy, "306 Switch Proxy"}, + {StatusCode::redirection_temporary_redirect, "307 Temporary Redirect"}, + {StatusCode::redirection_permanent_redirect, "308 Permanent Redirect"}, + {StatusCode::client_error_bad_request, "400 Bad Request"}, + {StatusCode::client_error_unauthorized, "401 Unauthorized"}, + {StatusCode::client_error_payment_required, "402 Payment Required"}, + {StatusCode::client_error_forbidden, "403 Forbidden"}, + {StatusCode::client_error_not_found, "404 Not Found"}, + {StatusCode::client_error_method_not_allowed, "405 Method Not Allowed"}, + {StatusCode::client_error_not_acceptable, "406 Not Acceptable"}, + {StatusCode::client_error_proxy_authentication_required, "407 Proxy Authentication Required"}, + {StatusCode::client_error_request_timeout, "408 Request Timeout"}, + {StatusCode::client_error_conflict, "409 Conflict"}, + {StatusCode::client_error_gone, "410 Gone"}, + {StatusCode::client_error_length_required, "411 Length Required"}, + {StatusCode::client_error_precondition_failed, "412 Precondition Failed"}, + {StatusCode::client_error_payload_too_large, "413 Payload Too Large"}, + {StatusCode::client_error_uri_too_long, "414 URI Too Long"}, + {StatusCode::client_error_unsupported_media_type, "415 Unsupported Media Type"}, + {StatusCode::client_error_range_not_satisfiable, "416 Range Not Satisfiable"}, + {StatusCode::client_error_expectation_failed, "417 Expectation Failed"}, + {StatusCode::client_error_im_a_teapot, "418 I'm a teapot"}, + {StatusCode::client_error_misdirection_required, "421 Misdirected Request"}, + {StatusCode::client_error_unprocessable_entity, "422 Unprocessable Entity"}, + {StatusCode::client_error_locked, "423 Locked"}, + {StatusCode::client_error_failed_dependency, "424 Failed Dependency"}, + {StatusCode::client_error_upgrade_required, "426 Upgrade Required"}, + {StatusCode::client_error_precondition_required, "428 Precondition Required"}, + {StatusCode::client_error_too_many_requests, "429 Too Many Requests"}, + {StatusCode::client_error_request_header_fields_too_large, "431 Request Header Fields Too Large"}, + {StatusCode::client_error_unavailable_for_legal_reasons, "451 Unavailable For Legal Reasons"}, + {StatusCode::server_error_internal_server_error, "500 Internal Server Error"}, + {StatusCode::server_error_not_implemented, "501 Not Implemented"}, + {StatusCode::server_error_bad_gateway, "502 Bad Gateway"}, + {StatusCode::server_error_service_unavailable, "503 Service Unavailable"}, + {StatusCode::server_error_gateway_timeout, "504 Gateway Timeout"}, + {StatusCode::server_error_http_version_not_supported, "505 HTTP Version Not Supported"}, + {StatusCode::server_error_variant_also_negotiates, "506 Variant Also Negotiates"}, + {StatusCode::server_error_insufficient_storage, "507 Insufficient Storage"}, + {StatusCode::server_error_loop_detected, "508 Loop Detected"}, + {StatusCode::server_error_not_extended, "510 Not Extended"}, + {StatusCode::server_error_network_authentication_required, "511 Network Authentication Required"}}; + return status_code_strings; + } + + inline StatusCode status_code(const std::string &status_code_string) noexcept { + if(status_code_string.size() < 3) + return StatusCode::unknown; + + auto number = status_code_string.substr(0, 3); + if(number[0] < '0' || number[0] > '9' || number[1] < '0' || number[1] > '9' || number[2] < '0' || number[2] > '9') + return StatusCode::unknown; + + class StringToStatusCode : public std::unordered_map { + public: + StringToStatusCode() { + for(auto &status_code : status_code_strings()) + emplace(status_code.second.substr(0, 3), status_code.first); + } + }; + static StringToStatusCode string_to_status_code; + + auto pos = string_to_status_code.find(number); + if(pos == string_to_status_code.end()) + return static_cast(atoi(number.c_str())); + return pos->second; + } + + inline const std::string &status_code(StatusCode status_code_enum) noexcept { + auto pos = status_code_strings().find(status_code_enum); + if(pos == status_code_strings().end()) { + static std::string empty_string; + return empty_string; + } + return pos->second; + } +} // namespace SimpleWeb + +#endif // SIMPLE_WEB_STATUS_CODE_HPP diff --git a/server/Simple-Web-Server/utility.hpp b/server/Simple-Web-Server/utility.hpp new file mode 100644 index 00000000..d9119dec --- /dev/null +++ b/server/Simple-Web-Server/utility.hpp @@ -0,0 +1,480 @@ +#ifndef SIMPLE_WEB_UTILITY_HPP +#define SIMPLE_WEB_UTILITY_HPP + +#include "status_code.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef DEPRECATED +#if defined(__GNUC__) || defined(__clang__) +#define DEPRECATED __attribute__((deprecated)) +#elif defined(_MSC_VER) +#define DEPRECATED __declspec(deprecated) +#else +#define DEPRECATED +#endif +#endif + +#if __cplusplus > 201402L || _MSVC_LANG > 201402L +#include +namespace SimpleWeb { + using string_view = std::string_view; +} +#elif !defined(ASIO_STANDALONE) +#include +namespace SimpleWeb { + using string_view = boost::string_ref; +} +#else +namespace SimpleWeb { + using string_view = const std::string &; +} +#endif + +namespace SimpleWeb { + inline bool case_insensitive_equal(const std::string &str1, const std::string &str2) noexcept { + return str1.size() == str2.size() && + std::equal(str1.begin(), str1.end(), str2.begin(), [](char a, char b) { + return tolower(a) == tolower(b); + }); + } + class CaseInsensitiveEqual { + public: + bool operator()(const std::string &str1, const std::string &str2) const noexcept { + return case_insensitive_equal(str1, str2); + } + }; + // Based on https://stackoverflow.com/questions/2590677/how-do-i-combine-hash-values-in-c0x/2595226#2595226 + class CaseInsensitiveHash { + public: + std::size_t operator()(const std::string &str) const noexcept { + std::size_t h = 0; + std::hash hash; + for(auto c : str) + h ^= hash(tolower(c)) + 0x9e3779b9 + (h << 6) + (h >> 2); + return h; + } + }; + + using CaseInsensitiveMultimap = std::unordered_multimap; + + /// Percent encoding and decoding + class Percent { + public: + /// Returns percent-encoded string + static std::string encode(const std::string &value) noexcept { + static auto hex_chars = "0123456789ABCDEF"; + + std::string result; + result.reserve(value.size()); // Minimum size of result + + for(auto &chr : value) { + if(!((chr >= '0' && chr <= '9') || (chr >= 'A' && chr <= 'Z') || (chr >= 'a' && chr <= 'z') || chr == '-' || chr == '.' || chr == '_' || chr == '~')) + result += std::string("%") + hex_chars[static_cast(chr) >> 4] + hex_chars[static_cast(chr) & 15]; + else + result += chr; + } + + return result; + } + + /// Returns percent-decoded string + static std::string decode(const std::string &value) noexcept { + std::string result; + result.reserve(value.size() / 3 + (value.size() % 3)); // Minimum size of result + + for(std::size_t i = 0; i < value.size(); ++i) { + auto &chr = value[i]; + if(chr == '%' && i + 2 < value.size()) { + auto hex = value.substr(i + 1, 2); + auto decoded_chr = static_cast(std::strtol(hex.c_str(), nullptr, 16)); + result += decoded_chr; + i += 2; + } + else if(chr == '+') + result += ' '; + else + result += chr; + } + + return result; + } + }; + + /// Query string creation and parsing + class QueryString { + public: + /// Returns query string created from given field names and values + static std::string create(const CaseInsensitiveMultimap &fields) noexcept { + std::string result; + + bool first = true; + for(auto &field : fields) { + result += (!first ? "&" : "") + field.first + '=' + Percent::encode(field.second); + first = false; + } + + return result; + } + + /// Returns query keys with percent-decoded values. + static CaseInsensitiveMultimap parse(const std::string &query_string) noexcept { + CaseInsensitiveMultimap result; + + if(query_string.empty()) + return result; + + std::size_t name_pos = 0; + auto name_end_pos = std::string::npos; + auto value_pos = std::string::npos; + for(std::size_t c = 0; c < query_string.size(); ++c) { + if(query_string[c] == '&') { + auto name = query_string.substr(name_pos, (name_end_pos == std::string::npos ? c : name_end_pos) - name_pos); + if(!name.empty()) { + auto value = value_pos == std::string::npos ? std::string() : query_string.substr(value_pos, c - value_pos); + result.emplace(std::move(name), Percent::decode(value)); + } + name_pos = c + 1; + name_end_pos = std::string::npos; + value_pos = std::string::npos; + } + else if(query_string[c] == '=' && name_end_pos == std::string::npos) { + name_end_pos = c; + value_pos = c + 1; + } + } + if(name_pos < query_string.size()) { + auto name = query_string.substr(name_pos, (name_end_pos == std::string::npos ? std::string::npos : name_end_pos - name_pos)); + if(!name.empty()) { + auto value = value_pos >= query_string.size() ? std::string() : query_string.substr(value_pos); + result.emplace(std::move(name), Percent::decode(value)); + } + } + + return result; + } + }; + + class HttpHeader { + public: + /// Parse header fields from stream + static CaseInsensitiveMultimap parse(std::istream &stream) noexcept { + CaseInsensitiveMultimap result; + std::string line; + std::size_t param_end; + while(getline(stream, line) && (param_end = line.find(':')) != std::string::npos) { + std::size_t value_start = param_end + 1; + while(value_start + 1 < line.size() && line[value_start] == ' ') + ++value_start; + if(value_start < line.size()) + result.emplace(line.substr(0, param_end), line.substr(value_start, line.size() - value_start - (line.back() == '\r' ? 1 : 0))); + } + return result; + } + + class FieldValue { + public: + class SemicolonSeparatedAttributes { + public: + /// Parse Set-Cookie or Content-Disposition from given header field value. + /// Attribute values are percent-decoded. + static CaseInsensitiveMultimap parse(const std::string &value) { + CaseInsensitiveMultimap result; + + std::size_t name_start_pos = std::string::npos; + std::size_t name_end_pos = std::string::npos; + std::size_t value_start_pos = std::string::npos; + for(std::size_t c = 0; c < value.size(); ++c) { + if(name_start_pos == std::string::npos) { + if(value[c] != ' ' && value[c] != ';') + name_start_pos = c; + } + else { + if(name_end_pos == std::string::npos) { + if(value[c] == ';') { + result.emplace(value.substr(name_start_pos, c - name_start_pos), std::string()); + name_start_pos = std::string::npos; + } + else if(value[c] == '=') + name_end_pos = c; + } + else { + if(value_start_pos == std::string::npos) { + if(value[c] == '"' && c + 1 < value.size()) + value_start_pos = c + 1; + else + value_start_pos = c; + } + else if(value[c] == '"' || value[c] == ';') { + result.emplace(value.substr(name_start_pos, name_end_pos - name_start_pos), Percent::decode(value.substr(value_start_pos, c - value_start_pos))); + name_start_pos = std::string::npos; + name_end_pos = std::string::npos; + value_start_pos = std::string::npos; + } + } + } + } + if(name_start_pos != std::string::npos) { + if(name_end_pos == std::string::npos) + result.emplace(value.substr(name_start_pos), std::string()); + else if(value_start_pos != std::string::npos) { + if(value.back() == '"') + result.emplace(value.substr(name_start_pos, name_end_pos - name_start_pos), Percent::decode(value.substr(value_start_pos, value.size() - 1))); + else + result.emplace(value.substr(name_start_pos, name_end_pos - name_start_pos), Percent::decode(value.substr(value_start_pos))); + } + } + + return result; + } + }; + }; + }; + + class RequestMessage { + public: + /** Parse request line and header fields from a request stream. + * + * @param[in] stream Stream to parse. + * @param[out] method HTTP method. + * @param[out] path Path from request URI. + * @param[out] query_string Query string from request URI. + * @param[out] version HTTP version. + * @param[out] header Header fields. + * + * @return True if stream is parsed successfully, false if not. + */ + static bool parse(std::istream &stream, std::string &method, std::string &path, std::string &query_string, std::string &version, CaseInsensitiveMultimap &header) noexcept { + std::string line; + std::size_t method_end; + if(getline(stream, line) && (method_end = line.find(' ')) != std::string::npos) { + method = line.substr(0, method_end); + + std::size_t query_start = std::string::npos; + std::size_t path_and_query_string_end = std::string::npos; + for(std::size_t i = method_end + 1; i < line.size(); ++i) { + if(line[i] == '?' && (i + 1) < line.size() && query_start == std::string::npos) + query_start = i + 1; + else if(line[i] == ' ') { + path_and_query_string_end = i; + break; + } + } + if(path_and_query_string_end != std::string::npos) { + if(query_start != std::string::npos) { + path = line.substr(method_end + 1, query_start - method_end - 2); + query_string = line.substr(query_start, path_and_query_string_end - query_start); + } + else + path = line.substr(method_end + 1, path_and_query_string_end - method_end - 1); + + std::size_t protocol_end; + if((protocol_end = line.find('/', path_and_query_string_end + 1)) != std::string::npos) { + if(line.compare(path_and_query_string_end + 1, protocol_end - path_and_query_string_end - 1, "HTTP") != 0) + return false; + version = line.substr(protocol_end + 1, line.size() - protocol_end - 2); + } + else + return false; + + header = HttpHeader::parse(stream); + } + else + return false; + } + else + return false; + return true; + } + }; + + class ResponseMessage { + public: + /** Parse status line and header fields from a response stream. + * + * @param[in] stream Stream to parse. + * @param[out] version HTTP version. + * @param[out] status_code HTTP status code. + * @param[out] header Header fields. + * + * @return True if stream is parsed successfully, false if not. + */ + static bool parse(std::istream &stream, std::string &version, std::string &status_code, CaseInsensitiveMultimap &header) noexcept { + std::string line; + std::size_t version_end; + if(getline(stream, line) && (version_end = line.find(' ')) != std::string::npos) { + if(5 < line.size()) + version = line.substr(5, version_end - 5); + else + return false; + if((version_end + 1) < line.size()) + status_code = line.substr(version_end + 1, line.size() - (version_end + 1) - (line.back() == '\r' ? 1 : 0)); + else + return false; + + header = HttpHeader::parse(stream); + } + else + return false; + return true; + } + }; + + /// Date class working with formats specified in RFC 7231 Date/Time Formats + class Date { + public: + /// Returns the given std::chrono::system_clock::time_point as a string with the following format: Wed, 31 Jul 2019 11:34:23 GMT. + static std::string to_string(const std::chrono::system_clock::time_point time_point) noexcept { + static std::string result_cache; + static std::chrono::system_clock::time_point last_time_point; + + static std::mutex mutex; + std::lock_guard lock(mutex); + + if(std::chrono::duration_cast(time_point - last_time_point).count() == 0 && !result_cache.empty()) + return result_cache; + + last_time_point = time_point; + + std::string result; + result.reserve(29); + + auto time = std::chrono::system_clock::to_time_t(time_point); + tm tm; +#if defined(_MSC_VER) || defined(__MINGW32__) + if(gmtime_s(&tm, &time) != 0) + return {}; + auto gmtime = &tm; +#else + auto gmtime = gmtime_r(&time, &tm); + if(!gmtime) + return {}; +#endif + + switch(gmtime->tm_wday) { + case 0: result += "Sun, "; break; + case 1: result += "Mon, "; break; + case 2: result += "Tue, "; break; + case 3: result += "Wed, "; break; + case 4: result += "Thu, "; break; + case 5: result += "Fri, "; break; + case 6: result += "Sat, "; break; + } + + result += gmtime->tm_mday < 10 ? '0' : static_cast(gmtime->tm_mday / 10 + 48); + result += static_cast(gmtime->tm_mday % 10 + 48); + + switch(gmtime->tm_mon) { + case 0: result += " Jan "; break; + case 1: result += " Feb "; break; + case 2: result += " Mar "; break; + case 3: result += " Apr "; break; + case 4: result += " May "; break; + case 5: result += " Jun "; break; + case 6: result += " Jul "; break; + case 7: result += " Aug "; break; + case 8: result += " Sep "; break; + case 9: result += " Oct "; break; + case 10: result += " Nov "; break; + case 11: result += " Dec "; break; + } + + auto year = gmtime->tm_year + 1900; + result += static_cast(year / 1000 + 48); + result += static_cast((year / 100) % 10 + 48); + result += static_cast((year / 10) % 10 + 48); + result += static_cast(year % 10 + 48); + result += ' '; + + result += gmtime->tm_hour < 10 ? '0' : static_cast(gmtime->tm_hour / 10 + 48); + result += static_cast(gmtime->tm_hour % 10 + 48); + result += ':'; + + result += gmtime->tm_min < 10 ? '0' : static_cast(gmtime->tm_min / 10 + 48); + result += static_cast(gmtime->tm_min % 10 + 48); + result += ':'; + + result += gmtime->tm_sec < 10 ? '0' : static_cast(gmtime->tm_sec / 10 + 48); + result += static_cast(gmtime->tm_sec % 10 + 48); + + result += " GMT"; + + result_cache = result; + return result; + } + }; +} // namespace SimpleWeb + +#ifdef __SSE2__ +#include +namespace SimpleWeb { + inline void spin_loop_pause() noexcept { _mm_pause(); } +} // namespace SimpleWeb +// TODO: need verification that the following checks are correct: +#elif defined(_MSC_VER) && _MSC_VER >= 1800 && (defined(_M_X64) || defined(_M_IX86)) +#include +namespace SimpleWeb { + inline void spin_loop_pause() noexcept { _mm_pause(); } +} // namespace SimpleWeb +#else +namespace SimpleWeb { + inline void spin_loop_pause() noexcept {} +} // namespace SimpleWeb +#endif + +namespace SimpleWeb { + /// Makes it possible to for instance cancel Asio handlers without stopping asio::io_service. + class ScopeRunner { + /// Scope count that is set to -1 if scopes are to be canceled. + std::atomic count; + + public: + class SharedLock { + friend class ScopeRunner; + std::atomic &count; + SharedLock(std::atomic &count) noexcept : count(count) {} + SharedLock &operator=(const SharedLock &) = delete; + SharedLock(const SharedLock &) = delete; + + public: + ~SharedLock() noexcept { + count.fetch_sub(1); + } + }; + + ScopeRunner() noexcept : count(0) {} + + /// Returns nullptr if scope should be exited, or a shared lock otherwise. + /// The shared lock ensures that a potential destructor call is delayed until all locks are released. + std::unique_ptr continue_lock() noexcept { + long expected = count; + while(expected >= 0 && !count.compare_exchange_weak(expected, expected + 1)) + spin_loop_pause(); + + if(expected < 0) + return nullptr; + else + return std::unique_ptr(new SharedLock(count)); + } + + /// Blocks until all shared locks are released, then prevents future shared locks. + void stop() noexcept { + long expected = 0; + while(!count.compare_exchange_weak(expected, -1)) { + if(expected < 0) + return; + expected = 0; + spin_loop_pause(); + } + } + }; +} // namespace SimpleWeb + +#endif // SIMPLE_WEB_UTILITY_HPP diff --git a/server/server.cpp b/server/server.cpp new file mode 100644 index 00000000..2bbe700b --- /dev/null +++ b/server/server.cpp @@ -0,0 +1,149 @@ +#include "Simple-Web-Server/server_http.hpp" +#include "external/sqlite_modern_cpp.h" +#include +#include +#include "rapidjson/document.h" +#include "rapidjson/writer.h" +#include "rapidjson/stringbuffer.h" + +using namespace std; +namespace po = boost::program_options; +using HttpServer = SimpleWeb::Server; + +#include +#include + +inline unsigned char from_hex (unsigned char ch) { + if (ch <= '9' && ch >= '0') + ch -= '0'; + else if (ch <= 'f' && ch >= 'a') + ch -= 'a' - 10; + else if (ch <= 'F' && ch >= 'A') + ch -= 'A' - 10; + else + ch = 0; + return ch; +} + +const std::string urldecode (const std::string& str) { + string result; + string::size_type i; + for (i = 0; i < str.size(); ++i) + { + if (str[i] == '+') + { + result += ' '; + } + else if (str[i] == '%' && str.size() > i+2) + { + const unsigned char ch1 = from_hex(str[i+1]); + const unsigned char ch2 = from_hex(str[i+2]); + const unsigned char ch = (ch1 << 4) | ch2; + result += ch; + i += 2; + } + else + { + result += str[i]; + } + } + return result; +} + +int main(int argc, char* argv[]) { + string input; + string staticPath; + unsigned port; + po::options_description desc("tilemaker-server\nServe tiles from an .mbtiles archive\n\nAvailable options"); + desc.add_options() + ("help","show help message") + ("input",po::value< string >(&input),"source .mbtiles") + ("static", po::value< string >(&staticPath)->default_value("static"), "path of static files") + ("port",po::value< unsigned >(&port)->default_value(8080), "port to serve tiles"); + po::positional_options_description p; + p.add("input", -1); + po::variables_map vm; + try { + po::store(po::command_line_parser(argc, argv).options(desc).positional(p).run(), vm); + } catch (const po::unknown_option& ex) { + cerr << "Unknown option: " << ex.get_option_name() << endl; + return -1; + } + po::notify(vm); + if (vm.count("help")) { std::cout << desc << std::endl; return 1; } + if (vm.count("input") == 0) { std::cerr << "You must specify an .mbtiles file. Run with --help to find out more." << std::endl; return -1; } + + HttpServer server; + server.config.port = port; + sqlite::database db; + db.init(input); + cout << "Starting local server on port " << server.config.port << endl; + + server.resource["^/([0-9]+)/([0-9]+)/([0-9]+).pbf$"]["GET"] = [&db](shared_ptr response, shared_ptr request) { + int32_t zoom = stoi(request->path_match[1]); + int32_t col = stoi(request->path_match[2]); + int32_t y = stoi(request->path_match[3]); + vector pbfBlob; + int tmsY = pow(2,zoom) - 1 - y; + db << "SELECT tile_data FROM tiles WHERE zoom_level=? AND tile_column=? AND tile_row=?" << zoom << col << tmsY >> pbfBlob; + std::string outStr(pbfBlob.begin(), pbfBlob.end()); + SimpleWeb::CaseInsensitiveMultimap header; + header.emplace("Content-Encoding", "gzip"); + response->write(outStr,header); + }; + + server.resource["^/metadata$"]["GET"] = [&db](shared_ptr response, shared_ptr request) { + rapidjson::Document document; + document.SetObject(); + rapidjson::Document::AllocatorType& allocator = document.GetAllocator(); + db << "SELECT name, value FROM metadata;" >> [&](string name, string value) { + if (name == "json") { + rapidjson::Document subDocument; + subDocument.Parse(value.c_str()); + document.AddMember("json",subDocument,allocator); + + } else { + rapidjson::Value nameVal; + nameVal.SetString(name.c_str(), allocator); + rapidjson::Value valueVal; + valueVal.SetString(value.c_str(), allocator); + document.AddMember(nameVal, valueVal, allocator); + } + }; + rapidjson::StringBuffer stringbuf; + rapidjson::Writer writer(stringbuf); + document.Accept(writer); + response->write(stringbuf.GetString()); + }; + + server.default_resource["GET"] = [&staticPath](shared_ptr response, shared_ptr request) { + try { + auto pathstr = urldecode(request->path); + if (pathstr == "/") pathstr = "/index.html"; + auto web_root_path = boost::filesystem::canonical(staticPath); + auto path = boost::filesystem::canonical(web_root_path / pathstr); + if(distance(web_root_path.begin(), web_root_path.end()) > distance(path.begin(), path.end()) || + !equal(web_root_path.begin(), web_root_path.end(), path.begin())) + throw invalid_argument("path must be within root path"); + + SimpleWeb::CaseInsensitiveMultimap header; + auto ifs = make_shared(); + ifs->open(path.string(), ifstream::in | ios::binary | ios::ate); + if(*ifs) { + auto length = ifs->tellg(); + ifs->seekg(0, ios::beg); + header.emplace("Content-Length", to_string(length)); + response->write(header); + vector buffer(length); + ifs->read(&buffer[0],length); + response->write(&buffer[0],length); + } else { + throw invalid_argument("could not read file"); + } + } catch(const exception &e) { + response->write(SimpleWeb::StatusCode::client_error_bad_request, "Could not open path " + request->path + ": " + e.what()); + } + }; + + server.start(); +} \ No newline at end of file