From e4ce595ebb169f279035a51a77aca788c98486d0 Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Thu, 5 Mar 2015 01:52:12 +0900 Subject: [PATCH] asio: Add serve_mux class to route incoming requet by path serve_mux is direct port of ServeMux from go --- examples/asio-sv.cc | 8 +- examples/asio-sv2.cc | 6 +- src/Makefile.am | 1 + src/asio_connection.h | 13 +- src/asio_http2_handler.cc | 8 +- src/asio_http2_handler.h | 5 +- src/asio_http2_impl.cc | 18 ++- src/asio_http2_impl.h | 6 +- src/asio_server.cc | 8 +- src/asio_server.h | 6 +- src/asio_server_serve_mux.cc | 176 +++++++++++++++++++++++ src/asio_server_serve_mux.h | 64 +++++++++ src/includes/nghttp2/asio_http2_server.h | 11 +- 13 files changed, 300 insertions(+), 30 deletions(-) create mode 100644 src/asio_server_serve_mux.cc create mode 100644 src/asio_server_serve_mux.h diff --git a/examples/asio-sv.cc b/examples/asio-sv.cc index 64cde06b..5578d037 100644 --- a/examples/asio-sv.cc +++ b/examples/asio-sv.cc @@ -62,10 +62,16 @@ int main(int argc, char *argv[]) { server.tls(argv[3], argv[4]); } - server.listen("*", port, [](const request &req, const response &res) { + server.handle("/", [](const request &req, const response &res) { res.write_head(200, {{"foo", {"bar"}}}); res.end("hello, world"); }); + server.handle("/secret/", [](const request &req, const response &res) { + res.write_head(200); + res.end("under construction!"); + }); + server.listen("*", port); + } catch (std::exception &e) { std::cerr << "exception: " << e.what() << "\n"; } diff --git a/examples/asio-sv2.cc b/examples/asio-sv2.cc index 537fb3c3..2fba5bbf 100644 --- a/examples/asio-sv2.cc +++ b/examples/asio-sv2.cc @@ -66,8 +66,7 @@ int main(int argc, char *argv[]) { server.tls(argv[4], argv[5]); } - server.listen("*", port, - [&docroot](const request &req, const response &res) { + server.handle("/", [&docroot](const request &req, const response &res) { auto path = percent_decode(req.uri().path); if (!check_path(path)) { res.write_head(404); @@ -99,6 +98,9 @@ int main(int argc, char *argv[]) { res.write_head(200, std::move(header)); res.end(file_reader_from_fd(fd)); }); + + server.listen("*", port); + } catch (std::exception &e) { std::cerr << "exception: " << e.what() << "\n"; } diff --git a/src/Makefile.am b/src/Makefile.am index 2b7f027f..254abf48 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -181,6 +181,7 @@ libnghttp2_asio_la_SOURCES = \ util.cc util.h http2.cc http2.h \ ssl.cc ssl.h \ asio_common.cc asio_common.h \ + asio_server_serve_mux.cc asio_server_serve_mux.h \ asio_client_session.cc \ asio_client_session_impl.cc asio_client_session_impl.h \ asio_client_session_tcp_impl.cc asio_client_session_tcp_impl.h \ diff --git a/src/asio_connection.h b/src/asio_connection.h index 01643749..b1f267c1 100644 --- a/src/asio_connection.h +++ b/src/asio_connection.h @@ -47,6 +47,7 @@ #include #include "asio_http2_handler.h" +#include "asio_server_serve_mux.h" #include "util.h" namespace nghttp2 { @@ -62,14 +63,14 @@ class connection : public std::enable_shared_from_this>, public: /// Construct a connection with the given io_service. template - explicit connection(request_cb cb, SocketArgs &&... args) - : socket_(std::forward(args)...), request_cb_(std::move(cb)), - writing_(false) {} + explicit connection(serve_mux &mux, SocketArgs &&... args) + : socket_(std::forward(args)...), mux_(mux), writing_(false) { + } /// Start the first asynchronous operation for the connection. void start() { - handler_ = std::make_shared( - socket_.get_io_service(), [this]() { do_write(); }, request_cb_); + handler_ = std::make_shared(socket_.get_io_service(), + [this]() { do_write(); }, mux_); if (handler_->start() != 0) { return; } @@ -147,7 +148,7 @@ public: private: socket_type socket_; - request_cb request_cb_; + serve_mux &mux_; std::shared_ptr handler_; diff --git a/src/asio_http2_handler.cc b/src/asio_http2_handler.cc index 3ab13859..7273fdf1 100644 --- a/src/asio_http2_handler.cc +++ b/src/asio_http2_handler.cc @@ -27,6 +27,7 @@ #include #include "asio_common.h" +#include "asio_server_serve_mux.h" #include "http2.h" #include "util.h" #include "template.h" @@ -412,8 +413,8 @@ int on_frame_not_send_callback(nghttp2_session *session, } // namespace http2_handler::http2_handler(boost::asio::io_service &io_service, - connection_write writefun, request_cb cb) - : writefun_(writefun), request_cb_(std::move(cb)), io_service_(io_service), + connection_write writefun, serve_mux &mux) + : writefun_(writefun), mux_(mux), io_service_(io_service), session_(nullptr), buf_(nullptr), buflen_(0), inside_callback_(false) {} http2_handler::~http2_handler() { nghttp2_session_del(session_); } @@ -486,7 +487,8 @@ http2_stream *http2_handler::find_stream(int32_t stream_id) { } void http2_handler::call_on_request(http2_stream &stream) { - request_cb_(stream.request(), stream.response()); + auto cb = mux_.handler(stream.request().impl()); + cb(stream.request(), stream.response()); } bool http2_handler::should_stop() const { diff --git a/src/asio_http2_handler.h b/src/asio_http2_handler.h index b5846024..9faa1691 100644 --- a/src/asio_http2_handler.h +++ b/src/asio_http2_handler.h @@ -42,6 +42,7 @@ namespace server { class http2_handler; class http2_stream; +class serve_mux; class request_impl { public: @@ -141,7 +142,7 @@ typedef std::function connection_write; class http2_handler : public std::enable_shared_from_this { public: http2_handler(boost::asio::io_service &io_service, connection_write writefun, - request_cb cb); + serve_mux &mux); ~http2_handler(); @@ -232,7 +233,7 @@ public: private: std::map> streams_; connection_write writefun_; - request_cb request_cb_; + serve_mux &mux_; boost::asio::io_service &io_service_; nghttp2_session *session_; const uint8_t *buf_; diff --git a/src/asio_http2_impl.cc b/src/asio_http2_impl.cc index 17236744..dfb8e36c 100644 --- a/src/asio_http2_impl.cc +++ b/src/asio_http2_impl.cc @@ -41,8 +41,8 @@ http2::http2() : impl_(make_unique()) {} http2::~http2() {} -void http2::listen(const std::string &address, uint16_t port, request_cb cb) { - impl_->listen(address, port, std::move(cb)); +void http2::listen(const std::string &address, uint16_t port) { + impl_->listen(address, port); } void http2::num_threads(size_t num_threads) { impl_->num_threads(num_threads); } @@ -53,6 +53,10 @@ void http2::tls(std::string private_key_file, std::string certificate_file) { void http2::backlog(int backlog) { impl_->backlog(backlog); } +bool http2::handle(std::string pattern, request_cb cb) { + return impl_->handle(std::move(pattern), std::move(cb)); +} + http2_impl::http2_impl() : num_threads_(1), backlog_(-1) {} namespace { @@ -62,8 +66,7 @@ std::vector &get_alpn_token() { } } // namespace -void http2_impl::listen(const std::string &address, uint16_t port, - request_cb cb) { +void http2_impl::listen(const std::string &address, uint16_t port) { std::unique_ptr ssl_ctx; if (!private_key_file_.empty() && !certificate_file_.empty()) { @@ -105,8 +108,7 @@ void http2_impl::listen(const std::string &address, uint16_t port, nullptr); } - server(address, port, num_threads_, std::move(cb), std::move(ssl_ctx), - backlog_).run(); + server(address, port, num_threads_, mux_, std::move(ssl_ctx), backlog_).run(); } void http2_impl::num_threads(size_t num_threads) { num_threads_ = num_threads; } @@ -119,6 +121,10 @@ void http2_impl::tls(std::string private_key_file, void http2_impl::backlog(int backlog) { backlog_ = backlog; } +bool http2_impl::handle(std::string pattern, request_cb cb) { + return mux_.handle(std::move(pattern), std::move(cb)); +} + } // namespace server template diff --git a/src/asio_http2_impl.h b/src/asio_http2_impl.h index b41098ae..975b883e 100644 --- a/src/asio_http2_impl.h +++ b/src/asio_http2_impl.h @@ -29,6 +29,8 @@ #include +#include "asio_server_serve_mux.h" + namespace nghttp2 { namespace asio_http2 { @@ -40,10 +42,11 @@ class server; class http2_impl { public: http2_impl(); - void listen(const std::string &address, uint16_t port, request_cb cb); + void listen(const std::string &address, uint16_t port); void num_threads(size_t num_threads); void tls(std::string private_key_file, std::string certificate_file); void backlog(int backlog); + bool handle(std::string pattern, request_cb cb); private: std::string private_key_file_; @@ -51,6 +54,7 @@ private: std::unique_ptr server_; std::size_t num_threads_; int backlog_; + serve_mux mux_; }; } // namespace server diff --git a/src/asio_server.cc b/src/asio_server.cc index f80d78f7..5b774ca0 100644 --- a/src/asio_server.cc +++ b/src/asio_server.cc @@ -43,13 +43,13 @@ namespace asio_http2 { namespace server { server::server(const std::string &address, uint16_t port, - std::size_t io_service_pool_size, request_cb cb, + std::size_t io_service_pool_size, serve_mux &mux, std::unique_ptr ssl_ctx, int backlog) : io_service_pool_(io_service_pool_size), signals_(io_service_pool_.get_io_service()), tick_timer_(io_service_pool_.get_io_service(), boost::posix_time::seconds(1)), - ssl_ctx_(std::move(ssl_ctx)), request_cb_(std::move(cb)) { + ssl_ctx_(std::move(ssl_ctx)), mux_(mux) { // Register to handle the signals that indicate when the server should exit. // It is safe to register for the same signal multiple times in a program, // provided all registration for the specified signal is made through Asio. @@ -114,7 +114,7 @@ typedef boost::asio::ssl::stream ssl_socket; void server::start_accept(boost::asio::ip::tcp::acceptor &acceptor) { if (ssl_ctx_) { auto new_connection = std::make_shared>( - request_cb_, io_service_pool_.get_io_service(), *ssl_ctx_); + mux_, io_service_pool_.get_io_service(), *ssl_ctx_); acceptor.async_accept( new_connection->socket().lowest_layer(), @@ -136,7 +136,7 @@ void server::start_accept(boost::asio::ip::tcp::acceptor &acceptor) { } else { auto new_connection = std::make_shared>( - request_cb_, io_service_pool_.get_io_service()); + mux_, io_service_pool_.get_io_service()); acceptor.async_accept( new_connection->socket(), diff --git a/src/asio_server.h b/src/asio_server.h index 894b7f59..36c4cb3a 100644 --- a/src/asio_server.h +++ b/src/asio_server.h @@ -56,13 +56,15 @@ namespace asio_http2 { namespace server { +class serve_mux; + /// The top-level class of the HTTP server. class server : private boost::noncopyable { public: /// Construct the server to listen on the specified TCP address and port, and /// serve up files from the given directory. explicit server(const std::string &address, uint16_t port, - std::size_t io_service_pool_size, request_cb cb, + std::size_t io_service_pool_size, serve_mux &mux_, std::unique_ptr ssl_ctx, int backlog = -1); @@ -88,7 +90,7 @@ private: std::unique_ptr ssl_ctx_; - request_cb request_cb_; + serve_mux &mux_; }; } // namespace server diff --git a/src/asio_server_serve_mux.cc b/src/asio_server_serve_mux.cc new file mode 100644 index 00000000..52c7f73d --- /dev/null +++ b/src/asio_server_serve_mux.cc @@ -0,0 +1,176 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * 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. + */ +#include "asio_server_serve_mux.h" + +#include "asio_http2_handler.h" +#include "util.h" +#include "http2.h" + +namespace nghttp2 { + +namespace asio_http2 { + +namespace server { + +namespace { +std::string create_html(int status_code) { + std::string res; + res.reserve(512); + auto status = ::nghttp2::http2::get_status_string(status_code); + res += ""; + res += status; + res += "

"; + res += status; + res += "

"; + return res; +} +} // namespace + +namespace { +request_cb redirect_handler(int status_code, std::string path) { + return [status_code, path](const request &req, const response &res) { + auto &uref = req.impl().uri(); + auto newloc = uref.scheme; + newloc += "://"; + newloc += uref.host; + newloc += path; + if (!uref.raw_query.empty()) { + newloc += "?"; + newloc += uref.raw_query; + } + auto html = create_html(status_code); + header_map h; + h.emplace("location", header_value{std::move(newloc)}); + h.emplace("content-length", header_value{util::utos(html.size())}); + h.emplace("content-type", header_value{"text/html; charset=utf-8"}); + + res.write_head(status_code, std::move(h)); + res.end(std::move(html)); + }; +} +} // namespace + +namespace { +request_cb status_handler(int status_code) { + return [status_code](const request &req, const response &res) { + auto html = create_html(status_code); + header_map h; + h.emplace("content-length", header_value{util::utos(html.size())}); + h.emplace("content-type", header_value{"text/html; charset=utf-8"}); + + res.write_head(status_code, std::move(h)); + res.end(std::move(html)); + }; +} +} // namespace + +bool serve_mux::handle(std::string pattern, request_cb cb) { + if (pattern.empty() || !cb) { + return false; + } + + auto it = mux_.find(pattern); + if (it != std::end(mux_) && (*it).second.user_defined) { + return false; + } + + // if pattern ends with '/' (e.g., /foo/), add implicit permanent + // redirect for '/foo'. + if (pattern.size() >= 2 && pattern.back() == '/') { + auto redirect_pattern = pattern.substr(0, pattern.size() - 1); + auto it = mux_.find(redirect_pattern); + if (it == std::end(mux_) || !(*it).second.user_defined) { + std::string path; + if (pattern[0] == '/') { + path = pattern; + } else { + // skip host part + path = pattern.substr(pattern.find('/')); + } + if (it == std::end(mux_)) { + mux_.emplace(std::move(redirect_pattern), + handler_entry{false, + redirect_handler(301, std::move(path)), + pattern}); + } else { + (*it).second = handler_entry{ + false, redirect_handler(301, std::move(path)), pattern}; + } + } + } + mux_.emplace(pattern, handler_entry{true, std::move(cb), pattern}); + + return true; +} + +request_cb serve_mux::handler(request_impl &req) const { + auto &path = req.uri().path; + if (req.method() != "CONNECT") { + auto clean_path = ::nghttp2::http2::path_join( + nullptr, 0, nullptr, 0, path.c_str(), path.size(), nullptr, 0); + if (clean_path != path) { + return redirect_handler(301, std::move(clean_path)); + } + } + auto &host = req.uri().host; + + auto cb = match(host + path); + if (cb) { + return cb; + } + cb = match(path); + if (cb) { + return cb; + } + return status_handler(404); +} + +request_cb serve_mux::match(const std::string &path) const { + const handler_entry *ent = nullptr; + size_t best = 0; + for (auto &kv : mux_) { + auto &pattern = kv.first; + if (!util::startsWith(path, pattern)) { + continue; + } + if (path.size() == pattern.size() || pattern.back() == '/' || + path[pattern.size()] == '/') { + if (!ent || best < pattern.size()) { + best = pattern.size(); + ent = &kv.second; + } + } + } + if (ent) { + return ent->cb; + } + return request_cb(); +} + +} // namespace server + +} // namespace asio_http2 + +} // namespace nghttp2 diff --git a/src/asio_server_serve_mux.h b/src/asio_server_serve_mux.h new file mode 100644 index 00000000..017a6bc2 --- /dev/null +++ b/src/asio_server_serve_mux.h @@ -0,0 +1,64 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2015 Tatsuhiro Tsujikawa + * + * 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. + */ +#ifndef ASIO_SERVER_SERVE_MUX_H +#define ASIO_SERVER_SERVE_MUX_H + +#include "nghttp2_config.h" + +#include + +namespace nghttp2 { + +namespace asio_http2 { + +namespace server { + +class request_impl; + +// port from go's ServeMux + +struct handler_entry { + bool user_defined; + request_cb cb; + std::string pattern; +}; + +class serve_mux { +public: + bool handle(std::string pattern, request_cb cb); + request_cb handler(request_impl &req) const; + request_cb match(const std::string &path) const; + +private: + std::map mux_; +}; + +} // namespace server + +} // namespace asio_http2 + +} // namespace nghttp2 + +#endif // ASIO_SERVER_SERVE_MUX_H diff --git a/src/includes/nghttp2/asio_http2_server.h b/src/includes/nghttp2/asio_http2_server.h index 2a2e0c49..056128a6 100644 --- a/src/includes/nghttp2/asio_http2_server.h +++ b/src/includes/nghttp2/asio_http2_server.h @@ -119,9 +119,14 @@ public: http2(); ~http2(); - // Starts listening connection on given address and port. The - // incoming requests are handled by given callback |cb|. - void listen(const std::string &address, uint16_t port, request_cb cb); + // Starts listening connection on given address and port and serves + // incoming requests. + void listen(const std::string &address, uint16_t port); + + // Registers request handler |cb| with path pattern |pattern|. This + // function will fail and returns false if same pattern has been + // already registered. Otherwise returns true. + bool handle(std::string pattern, request_cb cb); // Sets number of native threads to handle incoming HTTP request. // It defaults to 1.