commit 1b577871778c920ffe97a8739f3c9f74e6b6902e Author: Ryan Date: Tue Dec 24 22:23:04 2024 +0100 Hello libcode-seafire-protocol diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ec9f3de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +indent_size = 4 +max_line_length = off +trim_trailing_whitespace = false + +[*.yaml] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitea/workflows/on-push.yaml b/.gitea/workflows/on-push.yaml new file mode 100644 index 0000000..23a9e11 --- /dev/null +++ b/.gitea/workflows/on-push.yaml @@ -0,0 +1,24 @@ +name: on-push +on: [push] + +jobs: + build-and-test: + runs-on: linux + container: code.helloryan.se/infra/buildenv/cxx-amd64-fedora-40:latest + volumes: + - /build + steps: + - name: Clone repository + uses: actions/checkout@v3 + - name: Authenticate + run: | + git config unset http.https://code.helloryan.se/.extraheader + echo "${{ secrets.NETRC }}" >> ~/.netrc + - name: Initialize + run: | + bpkg create -d /build cc config.cc.coptions="-Wall -Werror" + bdep init -A /build + - name: Build + run: b + - name: Test + run: b test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c19e5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +.bdep/ +Doxyfile + +# Local default options files. +# +.build2/local/ + +# Compiler/linker output. +# +*.d +*.t +*.i +*.i.* +*.ii +*.ii.* +*.o +*.obj +*.gcm +*.pcm +*.ifc +*.so +*.dylib +*.dll +*.a +*.lib +*.exp +*.pdb +*.ilk +*.exe +*.exe.dlls/ +*.exe.manifest +*.pc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dfc745b --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright © 2024 Ryan. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. All advertising materials mentioning features or use of this software must + display the following acknowledgement: + + This product includes software developed by Ryan, http://helloryan.se/. + +4. Neither the name(s) of the copyright holder(s) nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b91d5a --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# libcode-seafire-protocol + +![Build status](https://code.helloryan.se/code/libcode-seafire-protocol/actions/workflows/on-push.yaml/badge.svg) + +## Requirements + +None, other than a modern C++-compiler. + +## Building + +See the wiki, https://code.helloryan.se/code/wiki/wiki/Build-Instructions, for +build instructions. + +## Contact + +Please report bugs and issues by sending an e-mail to: ryan@helloryan.se. + +## Contributing + +Please send an e-mail to ryan@helloryan.se to request an account and +write-access to the libcode-seafire-protocol repository. diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..974e01d --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,4 @@ +/config.build +/root/ +/bootstrap/ +build/ diff --git a/build/bootstrap.build b/build/bootstrap.build new file mode 100644 index 0000000..b2f24e9 --- /dev/null +++ b/build/bootstrap.build @@ -0,0 +1,7 @@ +project = libcode-seafire-protocol + +using version +using config +using test +using install +using dist diff --git a/build/export.build b/build/export.build new file mode 100644 index 0000000..4cb3464 --- /dev/null +++ b/build/export.build @@ -0,0 +1,6 @@ +$out_root/ +{ + include code/seafire/protocol/ +} + +export $out_root/code/seafire/protocol/$import.target diff --git a/build/root.build b/build/root.build new file mode 100644 index 0000000..21e0a2e --- /dev/null +++ b/build/root.build @@ -0,0 +1,16 @@ +# Uncomment to suppress warnings coming from external libraries. +# +#cxx.internal.scope = current + +cxx.std = latest + +using cxx + +hxx{*}: extension = hxx +ixx{*}: extension = ixx +txx{*}: extension = txx +cxx{*}: extension = cxx + +# The test target for cross-testing (running tests under Wine, etc). +# +test.target = $cxx.target diff --git a/buildfile b/buildfile new file mode 100644 index 0000000..bbe185e --- /dev/null +++ b/buildfile @@ -0,0 +1,5 @@ +./: {code/ tests/} doc{README.md} legal{LICENSE} manifest + +# Don't install tests. +# +tests/: install = false diff --git a/code/seafire/protocol/.gitignore b/code/seafire/protocol/.gitignore new file mode 100644 index 0000000..b1ed0e0 --- /dev/null +++ b/code/seafire/protocol/.gitignore @@ -0,0 +1,9 @@ +# Generated version header. +# +version.hxx + +# Unit test executables and Testscript output directories +# (can be symlinks). +# +*.test +test-*.test diff --git a/code/seafire/protocol/basic-parser.hxx b/code/seafire/protocol/basic-parser.hxx new file mode 100644 index 0000000..db92c1a --- /dev/null +++ b/code/seafire/protocol/basic-parser.hxx @@ -0,0 +1,302 @@ +#ifndef code__seafire__protocol__basic_parser_hxx_ +#define code__seafire__protocol__basic_parser_hxx_ + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace code::seafire::protocol +{ + + /// Implements a basic HTTP message parser. + /// + template + class basic_parser_t { + public: + /// Holds an iterator pair. + /// + struct iterator_pair_t + { + Iterator first; + Iterator last; + }; + + /// Holds iterator pairs for a header/value. + /// + struct header_t + { + iterator_pair_t field; + iterator_pair_t value; + }; + + /// Attempt to parse a message. + /// + Iterator + parse(Iterator, Iterator, std::error_code&); + + iterator_pair_t const& + version() const + { + return version_; + } + + std::vector const& + headers() const + { + return headers_; + } + + protected: + enum expectation_t + { + // Requests. + // + expect_method_start = 10, + expect_method, + + expect_target_start = 20, + expect_target, + + expect_version_h = 30, + expect_version_ht, + expect_version_htt, + expect_version_http, + expect_version_slash, + expect_version_major, + expect_version_period, + expect_version_minor, + expect_version_cr, + expect_version_lf, + + // Response + // + expect_response_version_h, + expect_response_version_ht, + expect_response_version_htt, + expect_response_version_http, + expect_response_version_slash, + expect_response_version_major, + expect_response_version_period, + expect_response_version_minor, + expect_response_version_space, + + expect_response_status_1, + expect_response_status_2, + expect_response_status_3, + expect_response_status_space, + + expect_response_reason_start, + expect_response_reason, + expect_response_reason_lf, + + // Headers. + // + expect_header_start = 100, + expect_header_field, + expect_header_value_start, + expect_header_value, + expect_header_terminating_lf, + + expect_terminating_lf, + + done = 9999 + + }; + + /// Construct a new parser. + /// + explicit + basic_parser_t(expectation_t expect) + : expect_{expect} + {} + + /// Copy-construct a new parser. + /// + basic_parser_t(basic_parser_t const&) = default; + + /// Move-construct a new parser. + /// + basic_parser_t(basic_parser_t&&) = default; + + /// Copy-assign this parser. + /// + basic_parser_t& + operator=(basic_parser_t const&) = default; + + /// Move-assign this parser. + /// + basic_parser_t& + operator=(basic_parser_t&&) = default; + + expectation_t expect_; + + iterator_pair_t method_; + iterator_pair_t target_; + iterator_pair_t version_; + + iterator_pair_t status_; + iterator_pair_t reason_; + + std::vector headers_; + + private: + expectation_t + parse_char(Iterator it, std::error_code&); + + }; + + /// Implements a basic request parser. + /// + template + class basic_request_parser_t + : public basic_parser_t + { + public: + basic_request_parser_t() + : basic_parser_t{basic_parser_t::expect_method_start} + {} + + typename basic_parser_t::iterator_pair_t const& + method() const + { + return basic_parser_t::method_; + } + + typename basic_parser_t::iterator_pair_t const& + target() const + { + return basic_parser_t::target_; + } + }; + + /// Implements a basic request parser. + /// + template + class basic_response_parser_t + : public basic_parser_t + { + public: + basic_response_parser_t() + : basic_parser_t{basic_parser_t::expect_response_version_h} + {} + + typename basic_parser_t::iterator_pair_t const& + status() const + { + return basic_parser_t::status_; + } + + typename basic_parser_t::iterator_pair_t const& + reason() const + { + return basic_parser_t::reason_; + } + }; + + template + void + extract_version(basic_parser_t const&, message_t&); + + template + void + extract_headers(basic_parser_t const&, message_t&); + + template + void + extract_method(basic_request_parser_t const&, request_t&); + + template + void + extract_target(basic_request_parser_t const&, request_t&); + + template + void + extract_status(basic_response_parser_t const&, response_t&); + + template + void + extract_reason(basic_response_parser_t const&, response_t&); + + template + void + extract_message(basic_parser_t const&, message_t&); + + template + void + extract_message(basic_request_parser_t const&, request_t&); + + template + void + extract_message(basic_response_parser_t const&, response_t&); + + template + InputIterator + parse_request(request_t&, InputIterator, InputIterator); + + template + InputIterator + parse_request(request_t&, + InputIterator, + InputIterator, + std::error_code&); + + template + InputIterator + parse_request(request_t&, + basic_request_parser_t&, + InputIterator, + InputIterator); + + template + InputIterator + parse_request(request_t&, + basic_request_parser_t&, + InputIterator, + InputIterator, + std::error_code&); + + template + InputIterator + parse_response(response_t&, + InputIterator, + InputIterator, + std::error_code&); + + template + InputIterator + parse_response(response_t&, + basic_response_parser_t&, + InputIterator, + InputIterator); + + template + InputIterator + parse_response(response_t&, + basic_response_parser_t&, + InputIterator, + InputIterator, + std::error_code&); + + class parser_not_ready + : public std::logic_error + { + public: + parser_not_ready() + : std::logic_error{"parser not ready"} + {} + + }; + +} // namespace code::seafire::protocol + +#include + +#endif diff --git a/code/seafire/protocol/basic-parser.txx b/code/seafire/protocol/basic-parser.txx new file mode 100644 index 0000000..bafe495 --- /dev/null +++ b/code/seafire/protocol/basic-parser.txx @@ -0,0 +1,546 @@ +namespace code::seafire::protocol +{ + + template + Iterator + basic_parser_t:: + parse(Iterator first, + Iterator last, + std::error_code& ec) + { + while (first != last) { + expect_ = parse_char(first++, ec); + + if (expect_ == done || ec) { + return first; + } + } + + if (first == last) { + ec = parse_error_t::incomplete_message; + } + + return first; + } + + template + typename basic_parser_t::expectation_t + basic_parser_t:: + parse_char(Iterator it, std::error_code& ec) + { + char const c{*it}; + + switch (expect_) { + case expect_method_start: + if (grammar::is_cr_or_lf(c)) { // ignore before request + return expect_method_start; + } + if (!grammar::is_tchar(c)) { + ec = parse_error_t::bad_method; + return done; + } + method_.first = it; + return expect_method; + + case expect_method: + if (grammar::is_space(c)) { + method_.last = it; + return expect_target_start; + } + if (!grammar::is_tchar(c)) { + ec = parse_error_t::bad_method; + return done; + } + return expect_method; + + case expect_target_start: + if (!grammar::is_target_char(c)) { + ec = parse_error_t::bad_target; + return done; + } + + target_.first = it; + return expect_target; + + case expect_target: + if (grammar::is_space(c)) { + target_.last = it; + return expect_version_h; + } + + if (!grammar::is_target_char(c)) { + ec = parse_error_t::bad_target; + return done; + } + + return expect_target; + + case expect_version_h: + if ('H' != c) { + ec = parse_error_t::bad_version; + return done; + } + return expect_version_ht; + + case expect_version_ht: + if ('T' != c) { + ec = parse_error_t::bad_version; + return done; + } + return expect_version_htt; + + case expect_version_htt: + if ('T' != c) { + ec = parse_error_t::bad_version; + return done; + } + return expect_version_http; + + case expect_version_http: + if ('P' != c) { + ec = parse_error_t::bad_version; + return done; + } + return expect_version_slash; + + case expect_version_slash: + if ('/' != c) { + ec = parse_error_t::bad_version; + return done; + } + return expect_version_major; + + case expect_version_major: + if (!grammar::is_digit(c)) { + ec = parse_error_t::bad_version; + return done; + } + version_.first = it; + return expect_version_period; + + case expect_version_period: + if ('.' != c) { + ec = parse_error_t::bad_version; + return done; + } + return expect_version_minor; + + case expect_version_minor: + if (!grammar::is_digit(c)) { + ec = parse_error_t::bad_version; + return done; + } + version_.last = it; + return expect_version_cr; + + case expect_version_cr: + if (!grammar::is_cr(c)) { + ec = parse_error_t::bad_version; + return done; + } + return expect_version_lf; + + case expect_version_lf: + if (!grammar::is_lf(c)) { + ec = parse_error_t::bad_version; + return done; + } + return expect_header_start; + + case expect_response_version_h: + if ('H' != c) { + ec = parse_error_t::bad_version; + return done; + } + return expect_response_version_ht; + + case expect_response_version_ht: + if ('T' != c) { + ec = parse_error_t::bad_version; + return done; + } + return expect_response_version_htt; + + case expect_response_version_htt: + if ('T' != c) { + ec = parse_error_t::bad_version; + return done; + } + return expect_response_version_http; + + case expect_response_version_http: + if ('P' != c) { + ec = parse_error_t::bad_version; + return done; + } + return expect_response_version_slash; + + case expect_response_version_slash: + if ('/' != c) { + ec = parse_error_t::bad_version; + return done; + } + return expect_response_version_major; + + case expect_response_version_major: + if (!grammar::is_digit(c)) { + ec = parse_error_t::bad_version; + return done; + } + version_.first = it; + return expect_response_version_period; + + case expect_response_version_period: + if ('.' != c) { + ec = parse_error_t::bad_version; + return done; + } + return expect_response_version_minor; + + case expect_response_version_minor: + if (!grammar::is_digit(c)) { + ec = parse_error_t::bad_version; + return done; + } + version_.last = it; + return expect_response_version_space; + + case expect_response_version_space: + if (!grammar::is_space(c)) { + ec = parse_error_t::bad_version; + return done; + } + return expect_response_status_1; + + case expect_response_status_1: + if (!grammar::is_digit(c)) { + ec = parse_error_t::bad_status; + return done; + } + status_.first = status_.last = it; + return expect_response_status_2; + + case expect_response_status_2: + if (!grammar::is_digit(c)) { + ec = parse_error_t::bad_status; + return done; + } + return expect_response_status_3; + + case expect_response_status_3: + if (!grammar::is_digit(c)) { + ec = parse_error_t::bad_status; + return done; + } + return expect_response_status_space; + + case expect_response_status_space: + if (' ' != c) { + ec = parse_error_t::bad_status; + return done; + } + status_.last = it; + return expect_response_reason_start; + + case expect_response_reason_start: + if (c != '\t' && c != ' ' && !grammar::is_vchar(c) && + grammar::is_obs_text(c)) { + ec = parse_error_t::bad_reason; + return done; + } + reason_.first = it; + return expect_response_reason; + + case expect_response_reason: + if ('\r' == c) { + reason_.last = it; + return expect_response_reason_lf; + } + if (c != '\t' && c != ' ' && !grammar::is_vchar(c) && + grammar::is_obs_text(c)) { + ec = parse_error_t::bad_reason; + return done; + } + return expect_response_reason; + + case expect_response_reason_lf: + if (c != '\t' && c != ' ' && !grammar::is_vchar(c) && + grammar::is_obs_text(c)) { + ec = parse_error_t::bad_reason; + return done; + } + return expect_header_start; + + case expect_header_start: + if (grammar::is_cr(c)) { + return expect_terminating_lf; + } + if (!grammar::is_tchar(c)) { + ec = parse_error_t::bad_header_field; + return done; + } + headers_.emplace_back(); + headers_.back().field.first = it; + return expect_header_field; + + case expect_header_field: + if (':' == c) { + headers_.back().field.last = it; + return expect_header_value_start; + } + if (!grammar::is_tchar(c)) { + ec = parse_error_t::bad_header_field; + return done; + } + return expect_header_field; + + case expect_header_value_start: + // TODO check through grammar + if (*it == ' ' || *it == '\t') { + return expect_header_value_start; + } + + headers_.back().value.first = it; + if (grammar::is_cr(c)) { + headers_.back().value.last = it; + return expect_header_terminating_lf; + } + if (!grammar::is_space(c) && grammar::is_control_char(c)) { + throw __LINE__; + ec = parse_error_t::bad_header_value; + return done; + } + return expect_header_value; + + case expect_header_value: + if (grammar::is_cr(c)) { + headers_.back().value.last = it; + return expect_header_terminating_lf; + } + if (!grammar::is_space(c) && grammar::is_control_char(c)) { + ec = parse_error_t::bad_header_value; + return done; + } + return expect_header_value; + + case expect_header_terminating_lf: + if (!grammar::is_lf(c)) { + ec = parse_error_t::bad_header_value; + return done; + } + return expect_header_start; + + case expect_terminating_lf: + if (!grammar::is_lf(c)) { + ec = parse_error_t::bad_terminator; + } + return done; + + case done: throw parser_not_ready{}; + } + + throw std::logic_error{"parser in invalid state"}; + } + + template + void + extract_version(basic_parser_t const& parser, message_t& m) + { + version_t v{static_cast(*parser.version().first - '0'), + static_cast(*parser.version().last - '0')}; + + m.set_version(std::move(v)); + } + + template + void + extract_headers(basic_parser_t const& parser, message_t& m) + { + header_collection_t headers; + + for (auto const& j : parser.headers()) { + std::string key{j.field.first, j.field.last}; + std::string value{j.value.first, j.value.last}; + + // Calling header.set() would replace any earlier set + // header with the same key. + // + headers.append(std::move(key), std::move(value)); + } + + m.set_headers(std::move(headers)); + } + + template + void + extract_method(basic_request_parser_t const& parser, request_t& r) + { + std::string method{parser.method().first, parser.method().last}; + r.set_method(std::move(method)); + } + + template + void + extract_target(basic_request_parser_t const& parser, request_t& r) + { + std::string target{parser.target().first, parser.target().last}; + + r.set_target(std::move(target)); + + auto target_uri = uri::try_parse(parser.target().first, parser.target().last); + + if (target_uri) { + r.set_target_uri(std::move(*target_uri)); + } + } + + template + void + extract_status(basic_response_parser_t const& parser, response_t& r) + { + auto digit1 = parser.status().first; + auto digit2 = digit1 + 1; + auto digit3 = digit1 + 2; + + unsigned long long cdig1 = (*digit1 - '0'); + unsigned long long cdig2 = (*digit2 - '0'); + unsigned long long cdig3 = (*digit3 - '0'); + + r.set_status((cdig1 * 100) + (cdig2 * 10) + cdig3); + } + + template + void + extract_reason(basic_response_parser_t const& parser, response_t& r) + { + std::string reason{parser.reason().first, parser.reason().last}; + + r.set_status({r.status().code(), std::move(reason)}); + } + + template + void + extract_message(basic_parser_t const& parser, message_t& m) + { + extract_version(parser, m); + extract_headers(parser, m); + } + + template + void + extract_message(basic_request_parser_t const& parser, request_t& r) + { + extract_message(parser, static_cast(r)); + extract_method(parser, r); + extract_target(parser, r); + } + + template + void + extract_message(basic_response_parser_t const& parser, response_t& r) + { + extract_message(parser, static_cast(r)); + extract_status(parser, r); + extract_reason(parser, r); + } + + template + InputIterator + parse_request(request_t& r, InputIterator first, InputIterator last) + { + basic_request_parser_t parser; + return parse_request(r, parser, first, last); + } + + template + InputIterator + parse_request(request_t& r, + InputIterator first, + InputIterator last, + std::error_code& ec) + { + basic_request_parser_t parser; + return parse_request(r, parser, first, last, ec); + } + + template + InputIterator + parse_request(request_t& r, + basic_request_parser_t& parser, + InputIterator first, + InputIterator last) + { + first = parser.parse(first, last); + extract_message(parser, r); + return first; + } + + template + InputIterator + parse_request(request_t& r, + basic_request_parser_t& parser, + InputIterator first, + InputIterator last, + std::error_code& ec) + { + first = parser.parse(first, last, ec); + + if (ec) + return first; + + extract_message(parser, r); + + return first; + } + + template + InputIterator + parse_response(response_t& r, InputIterator first, InputIterator last) + { + basic_response_parser_t parser; + return parse_response(r, parser, first, last); + } + + template + InputIterator + parse_response(response_t& r, + InputIterator first, + InputIterator last, + std::error_code& ec) + { + basic_response_parser_t parser; + return parse_response(r, parser, first, last, ec); + } + + template + InputIterator + parse_response(response_t& r, + basic_response_parser_t& parser, + InputIterator first, + InputIterator last) + { + first = parser.parse(first, last); + extract_message(parser, r); + return first; + } + + template + InputIterator + parse_response(response_t& r, + basic_response_parser_t& parser, + InputIterator first, + InputIterator last, + std::error_code& ec) + { + first = parser.parse(first, last, ec); + + if (ec) { + return first; + } + + extract_message(parser, r); + + return first; + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/buildfile b/code/seafire/protocol/buildfile new file mode 100644 index 0000000..b00aa85 --- /dev/null +++ b/code/seafire/protocol/buildfile @@ -0,0 +1,66 @@ +intf_libs = # Interface dependencies. +impl_libs = # Implementation dependencies. + +import intf_libs =+ libasio%lib{asio} +import intf_libs =+ libcode-uri%lib{code-uri} +import intf_libs =+ libcode-seafire-common%lib{code-seafire-common} + +./: lib{code-seafire-protocol}: libul{code-seafire-protocol} + +libul{code-seafire-protocol}: {hxx ixx txx cxx}{** -**.test... -version} \ + {hxx }{ version} + +libul{code-seafire-protocol}: $impl_libs $intf_libs + +# Unit tests. +# +exe{*.test}: +{ + test = true + install = false +} + +for t: cxx{**.test...} +{ + d = $directory($t) + n = $name($t)... + + ./: $d/exe{$n}: $t $d/{hxx ixx txx}{+$n} $d/testscript{+$n} + $d/exe{$n}: libul{code-seafire-protocol}: bin.whole = false +} + +hxx{version}: in{version} $src_root/manifest +{ + dist = true + clean = ($src_root != $out_root) +} + +# Build options. +# +cxx.poptions =+ "-I$out_root" "-I$src_root" + +# Export options. +# +lib{code-seafire-protocol}: +{ + cxx.export.poptions = "-I$out_root" "-I$src_root" + cxx.export.libs = $intf_libs +} + +# For pre-releases use the complete version to make sure they cannot +# be used in place of another pre-release or the final version. See +# the version module for details on the version.* variable values. +# +if $version.pre_release + lib{code-seafire-protocol}: bin.lib.version = "-$version.project_id" +else + lib{code-seafire-protocol}: bin.lib.version = "-$version.major.$version.minor" + +# Install into the code/seafire/protocol/ subdirectory of, say, /usr/include/ +# recreating subdirectories. +# +{hxx ixx txx}{*}: +{ + install = include/code/seafire/protocol/ + install.subdirs = true +} diff --git a/code/seafire/protocol/connection.cxx b/code/seafire/protocol/connection.cxx new file mode 100644 index 0000000..e22dbef --- /dev/null +++ b/code/seafire/protocol/connection.cxx @@ -0,0 +1,217 @@ +#include + +#include +#include +#include + +#include + +namespace code::seafire::protocol +{ + + /// Construct a new HTTP connection. + /// + /// \param s The I/O stream. + /// \param max The maximum size of the connection buffer. + /// + connection_t:: + connection_t(common::io::stream_t& s, std::size_t max) + : stream_{s}, + buffer_{max} + {} + + /// Destroy this connection. + /// + connection_t:: + ~connection_t() noexcept = default; + + /// Access the underlying I/O stream. + /// + common::io::stream_t& + connection_t:: + get_stream() + { + return stream_; + } + + asio::any_io_executor const& + connection_t:: + get_executor() + { + return get_stream().get_executor(); + } + + void + connection_t:: + cancel() + { + get_stream().cancel(); + } + + /// Perform a blocking read of a request. + /// + /// \param r The target request object. + /// \param content The target request content buffer. + /// \throws std::system_error Thrown on error. + /// + void + connection_t:: + read(request_t& r, asio::streambuf& content) + { + protocol::read(get_stream(), buffer_, r); + read_content(get_stream(), buffer_, content, r); + } + + /// Perform a blocking read of a request. + /// + /// \param r The target request object. + /// \param content The target request content buffer. + /// + void + connection_t:: + read(request_t& r, + asio::streambuf& content, + std::error_code& ec) + { + protocol::read(get_stream(), buffer_, r, ec); + + if (ec) { + return; + } + + read_content(get_stream(), buffer_, content, r, ec); + } + + /// Initiate an non-blocking read of a request. + /// + /// \param r The target request object. + /// \param content The target request content buffer. + /// \param handler The handler to invoke on completion. + /// + void + connection_t:: + async_read(request_t& r, + asio::streambuf& content, + read_handler_t handler) + { + auto bound = [this, &r, &content, handler](std::error_code const& ec) + { + if (ec) { + handler(ec); + return; + } + + async_read_content(get_stream(), buffer_, content, r, handler); + }; + + protocol::async_read(get_stream(), buffer_, r, bound); + } + + /// Perform a blocking read of a response. + /// + /// \param r The target response object. + /// \param content The target response content buffer. + /// \throws std::system_error Thrown on error. + /// + void + connection_t:: + read(response_t& r, asio::streambuf& content) + { + protocol::read(get_stream(), buffer_, r); + read_content(get_stream(), buffer_, content, r); + } + + /// Perform a blocking read of a response. + /// + /// \param r The target response object. + /// \param content The target response content buffer. + /// + void + connection_t:: + read(response_t& r, + asio::streambuf& content, + std::error_code& ec) + { + protocol::read(get_stream(), buffer_, r, ec); + + if (ec) { + return; + } + + read_content(get_stream(), buffer_, content, r, ec); + } + + /// Initiate an non-blocking read of a response. + /// + /// \param r The target response object. + /// \param content The target response content buffer. + /// \param handler The handler to invoke on completion. + /// + void + connection_t:: + async_read(response_t& r, + asio::streambuf& content, + read_handler_t handler) + { + auto bound = [this, &r, &content, handler](std::error_code const& ec) + { + if (ec) { + handler(ec); + return; + } + + async_read_content(get_stream(), buffer_, content, r, handler); + }; + + protocol::async_read(get_stream(), buffer_, r, bound); + } + + /// Initiate a blocking write of a request message. + /// + void + connection_t:: + write(request_t const& r, common::io::const_buffers_t const& content) + { + protocol::write(get_stream(), r, content); + } + + /// Initiate a blocking write of a request message. + /// + void + connection_t:: + write(request_t const& r, common::io::const_buffers_t const& content, std::error_code& ec) + { + protocol::write(get_stream(), r, content, ec); + } + + /// Initiate a non-blocking write of a request message. + /// + void + connection_t:: + async_write(request_t const& r, common::io::const_buffers_t const& content, write_handler_t handler) + { + protocol::async_write(get_stream(), r, content, handler); + } + + void + connection_t:: + write(response_t const& r, common::io::const_buffers_t const& content) + { + protocol::write(get_stream(), r, content); + } + + void + connection_t:: + write(response_t const& r, common::io::const_buffers_t const& content, std::error_code& ec) + { + protocol::write(get_stream(), r, content, ec); + } + + void + connection_t:: + async_write(response_t const& r, common::io::const_buffers_t const& content, write_handler_t handler) + { + protocol::async_write(get_stream(), r, content, handler); + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/connection.hxx b/code/seafire/protocol/connection.hxx new file mode 100644 index 0000000..bc5f0d6 --- /dev/null +++ b/code/seafire/protocol/connection.hxx @@ -0,0 +1,93 @@ +#ifndef code__seafire__protocol__connection_hxx_ +#define code__seafire__protocol__connection_hxx_ + +#include +#include + +#include + +#include + +#include +#include + +namespace code::seafire::protocol +{ + + /// Implements an HTTP/1.0/1.1 connection. + /// + class connection_t + { + public: + /// Asynchronous read handler type. + /// + using read_handler_t = std::function; + + /// Asynchronous write handler type. + /// + using write_handler_t = std::function; + + connection_t(common::io::stream_t&, std::size_t); + + connection_t(connection_t const&) = delete; + connection_t(connection_t&&) = delete; + + ~connection_t() noexcept; + + common::io::stream_t& + get_stream(); + + asio::any_io_executor const& + get_executor(); + + void + cancel(); + + void + read(request_t&, asio::streambuf&); + + void + read(request_t&, asio::streambuf&, std::error_code&); + + void + async_read(request_t&, asio::streambuf&, read_handler_t); + + void + read(response_t&, asio::streambuf&); + + void + read(response_t&, asio::streambuf&, std::error_code&); + + void + async_read(response_t&, asio::streambuf&, read_handler_t); + + void + write(request_t const&, common::io::const_buffers_t const&); + + void + write(request_t const&, common::io::const_buffers_t const&, std::error_code&); + + void + async_write(request_t const&, common::io::const_buffers_t const&, write_handler_t); + + void + write(response_t const&, common::io::const_buffers_t const&); + + void + write(response_t const&, common::io::const_buffers_t const&, std::error_code&); + + void + async_write(response_t const&, common::io::const_buffers_t const&, write_handler_t); + + connection_t& operator=(connection_t const&) = delete; + connection_t& operator=(connection_t&&) = delete; + + private: + common::io::stream_t& stream_; + asio::streambuf buffer_; + + }; + +} // namespace code::seafire::protocol + +#endif diff --git a/code/seafire/protocol/date-time.cxx b/code/seafire/protocol/date-time.cxx new file mode 100644 index 0000000..ccf03d3 --- /dev/null +++ b/code/seafire/protocol/date-time.cxx @@ -0,0 +1,42 @@ +#include + +#include +#include + +namespace code::seafire::protocol +{ + + std::string + format_http_date(std::chrono::system_clock::time_point const& time_point) + { + static constexpr const char time_format[] = "%a, %d %b %Y %H:%M:%S GMT"; + + std::time_t now_c = std::chrono::system_clock::to_time_t(time_point); + + std::stringstream str; + str.imbue(std::locale{}); + str << std::put_time(std::gmtime(&now_c), time_format); + + return str.str(); + } + + std::optional + try_parse_http_date(std::string const& text) + { + static constexpr const char time_format[] = "%a, %d %b %Y %H:%M:%S GMT"; + + std::tm tm{}; + + std::istringstream str{text}; + str.imbue(std::locale{}); + str >> std::get_time(&tm, time_format); + + if (str.fail()) + return {}; + + std::time_t time = std::mktime(&tm); + + return std::chrono::system_clock::from_time_t(time); + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/date-time.hxx b/code/seafire/protocol/date-time.hxx new file mode 100644 index 0000000..7da036f --- /dev/null +++ b/code/seafire/protocol/date-time.hxx @@ -0,0 +1,19 @@ +#ifndef code__seafire__protocol__date_time_hxx_ +#define code__seafire__protocol__date_time_hxx_ + +#include +#include +#include + +namespace code::seafire::protocol +{ + + std::string + format_http_date(std::chrono::system_clock::time_point const&); + + std::optional + try_parse_http_date(std::string const&); + +} // namespace code::seafire::protocol + +#endif diff --git a/code/seafire/protocol/error.cxx b/code/seafire/protocol/error.cxx new file mode 100644 index 0000000..5d91f07 --- /dev/null +++ b/code/seafire/protocol/error.cxx @@ -0,0 +1,88 @@ +#include + +namespace code::seafire::protocol +{ + + std::error_category const& + get_parse_error_category() + { + class category_t + : public std::error_category + { + public: + char const* + name() const noexcept override + { + return "code.seafire.parser"; + } + + std::string + message(int ec) const override + { + switch (static_cast(ec)) { + case parse_error_t::incomplete_message: return "incomplete message"; + case parse_error_t::bad_version: return "bad version"; + case parse_error_t::bad_header_field: return "bad header field"; + case parse_error_t::bad_header_value: return "bad header value"; + case parse_error_t::bad_terminator: return "bad terminator"; + case parse_error_t::bad_method: return "bad method"; + case parse_error_t::bad_target: return "bad target"; + case parse_error_t::bad_status: return "bad status"; + case parse_error_t::bad_reason: return "bad reason"; + } + + return "code.seafire.parser error"; + } + }; + + static category_t const category; + + return category; + } + + std::error_code + make_error_code(parse_error_t error) + { + return {static_cast(error), get_parse_error_category()}; + } + + std::error_category const& + get_protocol_error_category() + { + class category_t + : public std::error_category + { + public: + char const* + name() const noexcept override + { + return "code.seafire.protocol"; + } + + std::string + message(int ec) const override + { + switch (static_cast(ec)) { + case protocol_error_t::invalid_content_length: return "invalid content length"; + case protocol_error_t::request_too_large: return "request too large"; + case protocol_error_t::length_required: return "length required"; + case protocol_error_t::invalid_header_value: return "invalid header value"; + } + + return "code.seafire.protocol error"; + } + + }; + + static category_t const category; + + return category; + } + + std::error_code + make_error_code(protocol_error_t error) + { + return {static_cast(error), get_protocol_error_category()}; + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/error.hxx b/code/seafire/protocol/error.hxx new file mode 100644 index 0000000..f5f0bbd --- /dev/null +++ b/code/seafire/protocol/error.hxx @@ -0,0 +1,79 @@ +#ifndef code__seafire__protocol__error_hxx_ +#define code__seafire__protocol__error_hxx_ + +#include +#include + +namespace code::seafire::protocol +{ + + enum class parse_error_t + { + incomplete_message = 1, + + // Common. + bad_version, + bad_header_field, + bad_header_value, + bad_terminator, + + // Request-specific. + bad_method, + bad_target, + + // Response-specific. + bad_status, + bad_reason + + }; + + std::error_category const& + get_parse_error_category(); + + std::error_code + make_error_code(parse_error_t); + + enum class protocol_error_t + { + // indicates an invalid content-length. + // + invalid_content_length = 1, + + // indicates a too big request body. + // + request_too_large, + + // content-length missing. + // + length_required, + + // For now. + // + invalid_header_value + + }; + + std::error_category const& + get_protocol_error_category(); + + std::error_code + make_error_code(protocol_error_t); + +} // namespace code::seafire::protocol + +namespace std +{ + + template<> + struct is_error_code_enum + : true_type + {}; + + template<> + struct is_error_code_enum + : true_type + {}; + +} // namespace std + +#endif diff --git a/code/seafire/protocol/grammar.hxx b/code/seafire/protocol/grammar.hxx new file mode 100644 index 0000000..aa919fc --- /dev/null +++ b/code/seafire/protocol/grammar.hxx @@ -0,0 +1,49 @@ +#ifndef code__seafire__protocol__grammar_hxx_ +#define code__seafire__protocol__grammar_hxx_ + +namespace code::seafire::protocol::grammar +{ + + bool + is_cr(char c); + + bool + is_lf(char c); + + bool + is_cr_or_lf(char c); + + bool + is_space(char c); + + bool + is_digit(char c); + + bool + is_vchar(char c); + + bool + is_tchar(char c); + + bool + is_control_char(char c); + + bool + is_obs_text(char c); + + bool + is_hex(char c); + + bool + is_target_char(char c); + + template + void + skip_space(InputIterator& it, InputIterator const& end); + +} // namespace code::seafire::protocol::grammar + +#include +#include + +#endif diff --git a/code/seafire/protocol/grammar.ixx b/code/seafire/protocol/grammar.ixx new file mode 100644 index 0000000..f45e636 --- /dev/null +++ b/code/seafire/protocol/grammar.ixx @@ -0,0 +1,183 @@ +namespace code::seafire::protocol::grammar +{ + + /// Check if \a c is carriage-return. + /// + inline + bool + is_cr(char c) + { + return c == '\r'; + } + + /// Check if \a c is line-feed. + /// + inline + bool + is_lf(char c) + { + return c == '\n'; + } + + /// Check if \a c is carriage-return or line-feed. + /// + inline + bool + is_cr_or_lf(char c) + { + return c == '\r' || c == '\n'; + } + + /// Check if \a c is spage or tab. + /// + inline + bool + is_space(char c) + { + return c == ' ' || c == '\t'; + } + + /// Check if \a c is a decimal digit. + /// + inline + bool + is_digit(char c) + { + return 0 <= 'c' && c <= '9'; + } + + /// Check if \a c is a vchar. + /// + inline + bool + is_vchar(char c) + { + auto u = static_cast< unsigned char >(c); + + if (u == 0x7f || u >= 0x80) { + return false; + } + + return true; + } + + /// Check if \a c is a tchar. + /// + inline + bool + is_tchar(char c) + { + if ('a' <= c && c <= 'z') { + return true; + } + + if ('A' <= c && c <= 'Z') { + return true; + } + + if ('0' <= c && c <= '9') { + return true; + } + + switch (c) { + case '!': + case '#': + case '$': + case '%': + case '&': + case '\'': + case '*': + case '+': + case '-': + case '.': + case '^': + case '_': + case '`': + case '|': + case '~': + return true; + } + + return false; + } + + /// Check if \a c is a control char. + /// + inline + bool + is_control_char(char c) + { + return c <= 31 || c == 127; + } + + /// Check if \a c is obs-text. + /// + inline + bool + is_obs_text(char c) + { + auto u = static_cast< unsigned char >(c); + return u >= 0x80 && u <= 0xFF; + } + + /// Check if \a c is a hexadecimal digit. + /// + inline + bool + is_hex(char c) + { + if ('0' <= c && c <= '9') { + return true; + } + + if ('a' <= c && c <= 'f') { + return true; + } + + if ('A' <= c && c <= 'F') { + return true; + } + + return false; + } + + /// Check if \a c is a target-char. + /// + inline + bool + is_target_char(char c) + { + switch (c) { + case '!': + case '$': + case '%': + case '&': + case '(': + case ')': + case '*': + case '+': + case ',': + case '-': + case '.': + case '/': + case ':': + case ';': + case '=': + case '?': + case '@': + case '\'': + case '_': + case '~': + return true; + } + + if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')) + return true; + + if ('0' <= c && c <= '9') + return true; + + return false; + } + +} // namespace code::seafire::protocol::grammar diff --git a/code/seafire/protocol/grammar.txx b/code/seafire/protocol/grammar.txx new file mode 100644 index 0000000..fd18372 --- /dev/null +++ b/code/seafire/protocol/grammar.txx @@ -0,0 +1,15 @@ +namespace code::seafire::protocol::grammar +{ + + /// Skip space. + /// + template + void + skip_space(InputIterator& it, InputIterator const& end) + { + while (it != end && is_space(*it)) { + ++it; + } + } + +} // namespace code::seafire::protocol::grammar diff --git a/code/seafire/protocol/header-collection.cxx b/code/seafire/protocol/header-collection.cxx new file mode 100644 index 0000000..fc71bf2 --- /dev/null +++ b/code/seafire/protocol/header-collection.cxx @@ -0,0 +1,139 @@ +#include + +#include + +namespace code::seafire::protocol +{ + + /// Set a header. + /// + /// Calling set + void + header_collection_t:: + set(std::string key, std::string value) + { + normalize(key); + kv_store_.erase(key); + kv_store_.emplace(std::move(key), std::move(value)); + } + + /// Append a header value. + /// + void + header_collection_t:: + append(std::string key, std::string value) + { + normalize(key); + kv_store_.emplace(std::move(key), std::move(value)); + } + + /// Erase a header. + /// + void + header_collection_t:: + erase(std::string key) + { + normalize(key); + kv_store_.erase(key); + } + + /// Check if this collection contains a header. + /// + bool + header_collection_t:: + contains(std::string key) const + { + normalize(key); + return kv_store_.find(key) != kv_store_.end(); + } + + /// Get a header. + /// + std::vector + header_collection_t:: + get(std::string key) const + { + normalize(key); + + std::vector strings; + + auto lower = kv_store_.lower_bound(key); + auto upper = kv_store_.upper_bound(key); + + while (lower != upper) { + strings.push_back(lower->second); + ++lower; + } + + return strings; + } + + /// Get a header. + /// + std::optional + header_collection_t:: + get_one(std::string key) const + { + normalize(key); + + auto lower = kv_store_.lower_bound(key); + auto upper = kv_store_.upper_bound(key); + + if (lower != upper) + return lower->second; + + return {}; + } + + header_collection_t::const_iterator + header_collection_t:: + begin() const + { + return kv_store_.begin(); + } + + header_collection_t::const_iterator + header_collection_t:: + cbegin() const + { + return kv_store_.cbegin(); + } + + header_collection_t::const_iterator + header_collection_t:: + end() const + { + return kv_store_.end(); + } + + header_collection_t::const_iterator + header_collection_t:: + cend() const + { + return kv_store_.cend(); + } + + /// Normalize header name. + /// + void + header_collection_t:: + normalize(std::string& name) + { + std::locale loc{ "C" }; + + for (auto& c : name) + c = std::tolower(c, loc); + } + + /// Write headers to an output stream. + /// + std::ostream& + operator<<(std::ostream& o, header_collection_t const& h) + { + for (auto const& j : h) + o << j.first << " = " << j.second << '\n'; + + return o; + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/header-collection.hxx b/code/seafire/protocol/header-collection.hxx new file mode 100644 index 0000000..582248f --- /dev/null +++ b/code/seafire/protocol/header-collection.hxx @@ -0,0 +1,65 @@ +#ifndef code__seafire__protocol__header_collection_hxx_ +#define code__seafire__protocol__header_collection_hxx_ + +#include +#include +#include +#include +#include + +namespace code::seafire::protocol +{ + + /// Represents a collection of HTTP headers. + /// + class header_collection_t + { + public: + /// Iterator type. + /// + using const_iterator = std::multimap::const_iterator; + + void + set(std::string, std::string); + + void + append(std::string, std::string); + + void + erase(std::string); + + bool + contains(std::string) const; + + std::vector + get(std::string) const; + + std::optional + get_one(std::string) const; + + const_iterator + begin() const; + + const_iterator + cbegin() const; + + const_iterator + end() const; + + const_iterator + cend() const; + + static + void + normalize(std::string&); + + private: + std::multimap kv_store_; + }; + + std::ostream& + operator<<(std::ostream&, header_collection_t const&); + +} // namespace code::seafire::protocol + +#endif diff --git a/code/seafire/protocol/match.cxx b/code/seafire/protocol/match.cxx new file mode 100644 index 0000000..142b9de --- /dev/null +++ b/code/seafire/protocol/match.cxx @@ -0,0 +1,66 @@ +#include + +namespace code::seafire::protocol +{ + + match_request_t:: + match_request_t(request_t& m) + : message_{m} + {} + + std::pair + match_request_t:: + operator()(char const* begin, char const* end, std::error_code& ec) + { + if (begin == end) + return std::make_pair(begin, false); + + parser_type p; + + auto const last = p.parse(begin, end, ec); + + if (ec == parse_error_t::incomplete_message) { + // reset ec, so that the match will be attempted again. + // + ec = std::error_code{}; + return std::make_pair(begin, false); + } + + if (ec) + return std::make_pair(begin, true); + + extract_message(p, message_); + return std::make_pair(last, true); + } + + match_response_t:: + match_response_t(response_t& m) + : message_{m} + {} + + std::pair + match_response_t:: + operator()(char const* begin, char const* end, std::error_code& ec) + { + if (begin == end) + return std::make_pair(begin, false); + + parser_type p; + + auto const last = p.parse(begin, end, ec); + + if (ec == parse_error_t::incomplete_message) { + // reset ec, so that the match will be attempted again. + // + ec = std::error_code{}; + return std::make_pair(begin, false); + } + + if (ec) + return std::make_pair(begin, true); + + extract_message(p, message_); + return std::make_pair(last, true); + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/match.hxx b/code/seafire/protocol/match.hxx new file mode 100644 index 0000000..8b4414c --- /dev/null +++ b/code/seafire/protocol/match.hxx @@ -0,0 +1,53 @@ +#ifndef code__seafire__protocol__match_hxx_ +#define code__seafire__protocol__match_hxx_ + +#include +#include +#include +#include + +#include +#include + +namespace code::seafire::protocol +{ + + /// Implements an input match condition for matching a request. + /// + class match_request_t + { + public: + using parser_type = basic_request_parser_t; + + explicit + match_request_t(request_t& m); + + std::pair + operator()(char const*, char const*, std::error_code&); + + private: + request_t& message_; + + }; + + /// Implements an input match condition for matching a response. + /// + class match_response_t + { + public: + using parser_type = basic_response_parser_t; + + explicit + match_response_t(response_t& m); + + std::pair + operator()(char const*, char const*, std::error_code&); + + private: + response_t& message_; + + }; + +} // namespace code::seafire::protocol + +#endif diff --git a/code/seafire/protocol/media-type.cxx b/code/seafire/protocol/media-type.cxx new file mode 100644 index 0000000..049c6cd --- /dev/null +++ b/code/seafire/protocol/media-type.cxx @@ -0,0 +1,140 @@ +#include + +#include + +namespace code::seafire::protocol +{ + + /// Construct a new empty media type. + /// + media_type_t:: + media_type_t() = default; + + /// Construct a new media type. + /// + media_type_t:: + media_type_t(std::string type, std::string subtype) + : type_{std::move(type)}, subtype_{std::move(subtype)} + {} + + /// Construct a new media type. + /// + media_type_t:: + media_type_t(std::string type, + std::string subtype, + params_t params) + : type_{std::move(type) }, + subtype_{std::move(subtype)}, + params_{std::move(params)} + {} + + /// Get the type of this media type. + /// + std::string const& + media_type_t:: + type() const + { + return type_; + } + + /// Get the subtype of this media type. + /// + std::string const& + media_type_t:: + subtype() const + { + return subtype_; + } + + /// Get the parameters of this media type. + /// + media_type_t::params_t const& + media_type_t:: + params() const + { + return params_; + } + + /// Attempt to parse a media type. + /// + std::optional< media_type_t> + media_type_t:: + try_parse(std::string const& str, std::error_code& ec) + { + auto begin = str.begin(); + return try_parse(begin, str.end(), ec); + } + + /// Compare two media types for equality. + /// + /// Parameters are ignored. + /// + /// \relatesalso media_type_t + /// + bool + operator==(media_type_t const& lhs, media_type_t const& rhs) + { + if (lhs.type() != rhs.type()) { + if ("*" != lhs.type() && "*" != rhs.type()) { + return false; + } + } + + if (lhs.subtype() != rhs.subtype()) { + if ("*" != lhs.subtype() && "*" != rhs.subtype()) { + return false; + } + } + + return true; + } + + /// Compare two media types for inequality. + /// + /// Parameters are ignored. + /// + /// \relatesalso media_type_t + /// + bool + operator!=(media_type_t const& lhs, media_type_t const& rhs) + { + return !(lhs == rhs); + } + + /// Write media type to an output stream. + /// + /// \relatesalso media_type_t + /// + std::ostream& + to_stream(std::ostream& o, media_type_t const& m) + { + o << m.type() << '/' << m.subtype(); + for (auto const& j : m.params()) { + o << "; " << j.first << '=' << j.second; + } + return o; + } + + /// Convert a media type to a string. + /// + /// \relatesalso media_type_t + /// + std::string + to_string(media_type_t const& m) + { + std::stringstream str; + to_stream(str, m); + return str.str(); + } + + /// Write a media type to an output stream. + /// + /// \relatesalso media_type_t + /// + std::ostream& + operator<<(std::ostream& o, media_type_t const& m) + { + return to_stream(o, m); + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/media-type.hxx b/code/seafire/protocol/media-type.hxx new file mode 100644 index 0000000..d1ec046 --- /dev/null +++ b/code/seafire/protocol/media-type.hxx @@ -0,0 +1,77 @@ +#ifndef code__seafire__protocol__media_type_hxx_ +#define code__seafire__protocol__media_type_hxx_ + +#include + +#include +#include +#include +#include + +namespace code::seafire::protocol +{ + + /// Implements a media type according to RFC 2046. + /// + class media_type_t + { + public: + /// Media type parameters type. + /// + using params_t = std::map; + + media_type_t(); + + media_type_t(std::string, std::string); + + media_type_t(std::string, + std::string, + params_t); + + std::string const& + type() const; + + std::string const& + subtype() const; + + params_t const& + params() const; + + template + static + std::optional + try_parse(InputIterator&, + InputIterator const&, + std::error_code&); + + static + std::optional + try_parse(std::string const&, std::error_code&); + + private: + std::string type_; + std::string subtype_; + params_t params_; + + }; + + bool + operator==(media_type_t const&, media_type_t const&); + + bool + operator!=(media_type_t const&, media_type_t const&); + + std::ostream& + to_stream(std::ostream&, media_type_t const&); + + std::string + to_string(media_type_t const&); + + std::ostream& + operator<<(std::ostream&, media_type_t const&); + +} // namespace code::seafire::protocol + +#include + +#endif diff --git a/code/seafire/protocol/media-type.txx b/code/seafire/protocol/media-type.txx new file mode 100644 index 0000000..8e49d25 --- /dev/null +++ b/code/seafire/protocol/media-type.txx @@ -0,0 +1,125 @@ +namespace code::seafire::protocol +{ + + template + std::optional + media_type_t:: + try_parse(InputIterator& begin, + InputIterator const& end, + std::error_code& ec) + { + std::optional const failure; + + auto skip_whitespace = [&] + { + while (begin != end && grammar::is_space(*begin)) { + ++begin; + } + }; + + auto try_parse_token = [&]() -> std::optional + { + if (begin == end) + return std::nullopt; + + if (!grammar::is_tchar(*begin)) + return std::nullopt; + + std::string token; + token += *begin; + ++begin; + + while (begin != end) { + if (!grammar::is_tchar(*begin)) + break; + + token += *begin; + ++begin; + } + + return std::move(token); + }; + + auto try_parse_parameter = [&]() -> std::optional> + { + if (begin == end || *begin != ';') + return std::nullopt; + + ++begin; // skips ';' + + skip_whitespace(); + + auto name = try_parse_token(); + + if (!name || begin == end || *begin != '=') + return std::nullopt; + + ++begin; // skips '=' + + // Quoted-string value? + if (begin != end && *begin == '"') { + // fixme: support quoted string values + // + return std::nullopt; + } + + auto value = try_parse_token(); + + if (!value) + return std::nullopt; + + // fixme: normalize parameter name. + // + + return std::pair{*name, *value}; + }; + + auto try_parse_parameters = [&]() -> std::map + { + std::map< std::string, std::string > params; + + while (begin != end) { + auto param = try_parse_parameter(); + + if (!param) { + return params; + } + + params.insert(*param); + + skip_whitespace(); + } + + return params; + }; + + skip_whitespace(); + + auto type = try_parse_token(); + + if (!type) { + return std::nullopt; + } + + if (begin == end || *begin != '/') { + return std::nullopt; + } + + ++begin; // skips '/' + + auto subtype = try_parse_token(); + + if (!subtype) { + return std::nullopt; + } + + skip_whitespace(); + + return {{ + std::move(*type), + std::move(*subtype), + try_parse_parameters() + }}; + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/message.cxx b/code/seafire/protocol/message.cxx new file mode 100644 index 0000000..abab28b --- /dev/null +++ b/code/seafire/protocol/message.cxx @@ -0,0 +1,89 @@ +#include + +namespace code::seafire::protocol +{ + + /// Construct a new message. + /// + message_t:: + message_t() = default; + + /// Construct a new message. + /// + message_t:: + message_t(version_t version) + : version_{version} + {} + + /// Get the message version. + /// + version_t const& + message_t:: + version() const + { + return version_; + } + + /// Set the message version. + /// + void + message_t:: + set_version(version_t v) + { + version_ = std::move(v); + } + + /// Access the message headers. + /// + header_collection_t const& + message_t:: + headers() const + { + return headers_; + } + + /// Set/replace message header. + /// + void + message_t:: + set_header(std::string name, std::string value) + { + headers_.set(std::move(name), std::move(value)); + } + + /// Append message header. + /// + void + message_t:: + append_header(std::string name, std::string value) + { + headers_.append(std::move(name), std::move(value)); + } + + /// Erase message header. + /// + void + message_t:: + erase_header(std::string name) + { + headers_.erase(name); + } + + /// Set/replace message headers. + /// + void + message_t:: + set_headers(header_collection_t headers) + { + headers_ = std::move(headers); + } + + /// Write headers to output stream. + /// + std::ostream& + operator<<(std::ostream& o, message_t const& m) + { + return o << m.headers(); + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/message.hxx b/code/seafire/protocol/message.hxx new file mode 100644 index 0000000..0de6047 --- /dev/null +++ b/code/seafire/protocol/message.hxx @@ -0,0 +1,86 @@ +#ifndef code__seafire__protocol__message_hxx_ +#define code__seafire__protocol__message_hxx_ + +#include +#include +#include + +#include +#include + +namespace code::seafire::protocol +{ + + /// Common base class for HTTP messages. + /// + class message_t + { + public: + message_t(); + + explicit + message_t(version_t); + + version_t const& + version() const; + + void + set_version(version_t); + + header_collection_t const& + headers() const; + + void + set_headers(header_collection_t); + + void + set_header(std::string, std::string); + + void + append_header(std::string, std::string); + + void + erase_header(std::string); + + private: + version_t version_; + header_collection_t headers_; + + }; + + template + bool + has(message_t const& m); + + template + bool + has_quick(message_t const& m); + + template + std::optional> + get(message_t const& m); + + template + std::optional> + get(message_t const& m, std::error_code& ec); + + template + void + set(message_t& m, Args&&... args); + + template + void + set_if_not_set(message_t& m, Args&&... args); + + template + void + erase(message_t& m); + + std::ostream& + operator<<(std::ostream& o, message_t const& m); + +} // namespace code::seafire::protocol + +#include + +#endif diff --git a/code/seafire/protocol/message.txx b/code/seafire/protocol/message.txx new file mode 100644 index 0000000..2f3e77c --- /dev/null +++ b/code/seafire/protocol/message.txx @@ -0,0 +1,66 @@ +namespace code::seafire::protocol +{ + + template + bool + has(message_t const& m) + { + // FIXME: Parse header as well, to make sure it is valid. + return m.headers().contains(H::name); + } + + template + bool + has_quick(message_t const& m) + { + return m.headers().contains(H::name); + } + + template + std::optional> + get(message_t const& m) + { + std::error_code ignored_ec; + return get(m, ignored_ec); + } + + template + std::optional> + get(message_t const& m, std::error_code& ec) + { + return H::try_parse(m.headers().get(H::name), ec); + } + + template + void + set(message_t& m, Args&&... args) + { + using type = traits::alias_type_t; + + if constexpr (traits::has_overridden_to_string_v) { + m.set_header(H::name, H::to_string(type{std::forward(args)...})); + } + else { + using std::to_string; + m.set_header(H::name, to_string(type{std::forward(args)...})); + } + } + + template + void + set_if_not_set(message_t& m, Args&&... args) + { + if (has_quick()) + return; + + set(m, std::forward(args)...); + } + + template + void + erase(message_t& m) + { + m.erase_header(H::name); + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/protocol-version.cxx b/code/seafire/protocol/protocol-version.cxx new file mode 100644 index 0000000..ec6722e --- /dev/null +++ b/code/seafire/protocol/protocol-version.cxx @@ -0,0 +1,71 @@ +#include + +#include + +namespace code::seafire::protocol +{ + + bool + version_t:: + operator==(version_t const& other) const noexcept + { + return major == other.major && minor == other.minor; + } + + bool + version_t:: + operator!=(version_t const& other) const noexcept + { + return !(*this == other); + } + + std::ostream& + to_stream(std::ostream& o, version_t const& v) + { + o << "HTTP/" << v.major << '.' << v.minor; + return o; + } + + std::string + to_string(version_t const& v) + { + std::stringstream str; + to_stream(str, v); + return str.str(); + } + + std::ostream& + operator<<(std::ostream& o, version_t const& v) + { + return to_stream(o, v); + } + + void + to_buffers(common::io::const_buffers_t& buffers, version_t const& v) + { + static char constexpr http10[]{'H', 'T', 'T', 'P', '/', '1', '.', '0'}; + static char constexpr http11[]{'H', 'T', 'T', 'P', '/', '1', '.', '1'}; + + static char constexpr http[]{'H', 'T', 'T', 'P', '/'}; + static char constexpr digits[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; + static char constexpr dot[]{'.'}; + + using common::io::buffer; + + if (v == http_1_0) { + buffers.emplace_back(buffer(http10)); + return; + } + + if (v == http_1_1) { + buffers.emplace_back(buffer(http11)); + return; + } + + buffers.emplace_back(buffer(http)); + buffers.emplace_back(buffer(&digits[v.major % 10], 1)); + buffers.emplace_back(buffer(dot)); + buffers.emplace_back(buffer(&digits[v.minor % 10], 1)); + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/protocol-version.hxx b/code/seafire/protocol/protocol-version.hxx new file mode 100644 index 0000000..b61988e --- /dev/null +++ b/code/seafire/protocol/protocol-version.hxx @@ -0,0 +1,66 @@ +#ifndef code__seafire__protocol__protocol_version_hxx_ +#define code__seafire__protocol__protocol_version_hxx_ + +#include + +#include + +#include +#include +#include +#include + +namespace code::seafire::protocol +{ + + /// Represents an HTTP version. + /// + struct version_t + { + /// Construct a new protocol version. + /// + constexpr + version_t() noexcept + : major{0}, minor{0} + {} + + /// Construct a new protocol version. + /// + /// \param major The major version number. + /// \param minor The minor version number. + /// + constexpr + version_t(std::uint16_t major, std::uint16_t minor) noexcept + : major{major}, minor{minor} + {} + + bool + operator==(version_t const&) const noexcept; + + bool + operator!=(version_t const&) const noexcept; + + std::uint16_t major; + std::uint16_t minor; + + }; + + inline constexpr version_t const http_1_0{1, 0}; + + inline constexpr version_t const http_1_1{1, 1}; + + std::ostream& + to_stream(std::ostream&, version_t const&); + + std::string + to_string(version_t const&); + + std::ostream& + operator<<(std::ostream&, version_t const&); + + void + to_buffers(common::io::const_buffers_t&, version_t const&); + +} // namespace code::seafire::protocol + +#endif diff --git a/code/seafire/protocol/read-content.cxx b/code/seafire/protocol/read-content.cxx new file mode 100644 index 0000000..fa36f35 --- /dev/null +++ b/code/seafire/protocol/read-content.cxx @@ -0,0 +1,225 @@ +#include + +#include + +namespace code::seafire::protocol +{ + + void + read_content(common::io::stream_t& s, + asio::streambuf& i, + asio::streambuf& c, + std::size_t content_length) + { + std::error_code ec; + read_content(s, i, c, content_length, ec); + + if (ec) { + throw std::system_error{ec}; + } + } + + void + read_content(common::io::stream_t& s, + asio::streambuf& i, + asio::streambuf& c, + std::size_t content_length, + std::error_code& ec) + { + if (auto pending = i.size(); pending > 0) { + auto copied = buffer_copy( + c.prepare(pending), + i.data(), + std::min(pending, content_length) + ); + i.consume(copied); + c.commit(copied); + content_length -= copied; + } + + auto n = s.read(c.prepare(content_length), ec); + c.commit(n); + } + + void + async_read_content(common::io::stream_t& s, + asio::streambuf& i, + asio::streambuf& c, + std::size_t content_length, + std::function h) + { + if (auto pending = i.size(); pending > 0) { + auto copied = buffer_copy( + c.prepare(pending), + i.data(), + std::min(pending, content_length) + ); + + i.consume(copied); + c.commit(copied); + content_length -= copied; + } + + auto bound = [&c, h](std::error_code const& ec, std::size_t n) + { + c.commit(n); + h(ec); + }; + + s.async_read(c.prepare(content_length), bound); + } + + /// Read content from stream. + /// + void + read_content(common::io::stream_t& s, + asio::streambuf& i, + asio::streambuf& c, + request_t const& r) + { + std::error_code ec; + read_content(s, i, c, r, ec); + + if (ec) { + throw std::system_error{ec}; + } + } + + void + read_content(common::io::stream_t& s, + asio::streambuf& i, + asio::streambuf& c, + request_t const& r, + std::error_code& ec) + { + // fixme: support transfer encoding. + // + + auto content_length = get(r, ec); + + if (ec) { + return; + } + + if (content_length) { + read_content(s, i, c, *content_length, ec); + return; + } + + // we just assume the message does not contain a payload. + // + } + + void + async_read_content(common::io::stream_t& s, + asio::streambuf& i, + asio::streambuf& c, + request_t const& r, + std::function handler) + { + std::error_code ec; + auto content_length = get(r, ec); + + if (ec) { + asio::post( + s.get_executor(), + [handler, ec]() + { + handler(ec); + } + ); + return; + } + + if (content_length) { + async_read_content(s, i, c, *content_length, handler); + return; + } + + // we assume the request does not include a payload. + // + asio::post( + s.get_executor(), + [handler] + { + handler({}); + } + ); + } + + void + read_content(common::io::stream_t& s, + asio::streambuf& i, + asio::streambuf& c, + response_t const& r) + { + std::error_code ec; + read_content(s, i, c, r, ec); + + if (ec) { + throw std::system_error{ec}; + } + } + + void + read_content(common::io::stream_t& s, + asio::streambuf& i, + asio::streambuf& c, + response_t const& r, + std::error_code& ec) + { + // fixme: support transfer encoding. + // + + auto content_length = get(r, ec); + + if (ec) { + return; + } + + if (content_length) { + read_content(s, i, c, *content_length, ec); + return; + } + + // fixme: read until eof. + // + // fixme: return error until eof is implemented. + // + } + + void + async_read_content(common::io::stream_t& s, + asio::streambuf& i, + asio::streambuf& c, + response_t const& r, + std::function handler) + { + // fixme: support transfer encoding. + // + + std::error_code ec; + auto content_length = get(r, ec); + + if (ec) { + asio::post( + s.get_executor(), + [handler, ec] + { + handler(ec); + } + ); + } + + if (content_length) { + async_read_content(s, i, c, *content_length, handler); + return; + } + + // fixme: read until eof. + // + // fixme: return error until eof is implemented. + // + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/read-content.hxx b/code/seafire/protocol/read-content.hxx new file mode 100644 index 0000000..3026c49 --- /dev/null +++ b/code/seafire/protocol/read-content.hxx @@ -0,0 +1,74 @@ +#ifndef code__seafire__protocol__read_content_hxx_ +#define code__seafire__protocol__read_content_hxx_ + +#include +#include + +#include + +namespace code::seafire::protocol +{ + + void + read_content(common::io::stream_t&, + asio::streambuf&, + asio::streambuf&, + std::size_t); + + void + read_content(common::io::stream_t&, + asio::streambuf&, + asio::streambuf&, + std::size_t, + std::error_code&); + + void + async_read_content(common::io::stream_t&, + asio::streambuf&, + asio::streambuf&, + std::size_t, + std::function); + + void + read_content(common::io::stream_t&, + asio::streambuf&, + asio::streambuf&, + request_t const&); + + void + read_content(common::io::stream_t&, + asio::streambuf&, + asio::streambuf&, + request_t const&, + std::error_code&); + + void + async_read_content(common::io::stream_t&, + asio::streambuf&, + asio::streambuf&, + request_t const&, + std::function); + + void + read_content(common::io::stream_t&, + asio::streambuf&, + asio::streambuf&, + response_t const&); + + void + read_content(common::io::stream_t&, + asio::streambuf&, + asio::streambuf&, + response_t const&, + std::error_code&); + + void + async_read_content(common::io::stream_t&, + asio::streambuf&, + asio::streambuf&, + response_t const&, + std::function); + +} // namespace code::seafire::protocol + +#endif diff --git a/code/seafire/protocol/read.cxx b/code/seafire/protocol/read.cxx new file mode 100644 index 0000000..af964ac --- /dev/null +++ b/code/seafire/protocol/read.cxx @@ -0,0 +1,171 @@ +#include +#include + +#include + +#include + +#include + +namespace code::seafire::protocol +{ + + /// Read a request message from buffer \a b, reading more content from + /// stream \a s as required. + /// + /// The read request message is placed in \a r. + /// + /// If the buffer \a b contains a full request message, no I/O operations + /// will be performed. + /// + /// \throws std::system_error Thrown on error. + /// \relatesalso request_t + /// + void + read(common::io::stream_t& s, + asio::streambuf& b, + request_t& r) + { + std::error_code ec; + read(s, b, r, ec); + + if (ec) { + throw std::system_error{ec}; + } + } + + /// Read a request message from buffer \a b, reading more content from + /// stream \a s as required. + /// + /// The read request message is placed in \a r. + /// + /// If the buffer \a b contains a full request message, no I/O operations + /// will be performed. + /// + /// Errors are reported through \a ec. + /// + /// \relatesalso request_t + /// + void + read(common::io::stream_t& s, + asio::streambuf& b, + request_t& r, + std::error_code& ec) + { + auto bytes_consumed = common::io::read_until(s, b, match_request_t{r}, ec); + + if (ec) { + return; + } + + b.consume(bytes_consumed); + } + + /// Initialize an asynchronous read of a request message from buffer \a b, + /// reading more content from \a s as required. + /// + /// The read request message is placed in \a r. + /// + /// If the buffer \a b contains a full request message, no I/O operations + /// will be performed. + /// + /// \a r must be valid until the completion-handler \a h is called. + /// + void + async_read(common::io::stream_t& s, + asio::streambuf& b, + request_t& r, + std::function h) + { + auto bound = [&b, h](std::error_code const& ec, std::size_t n) + { + if (!ec) { + b.consume(n); + } + + h(ec); + }; + + common::io::async_read_until(s, b, match_request_t{r}, bound); + } + + /// Read a response message from buffer \a b, reading more content from + /// stream \a s as required. + /// + /// The read response message is placed in \a r. + /// + /// If the buffer \a b contains a full response message, no I/O operations + /// will be performed. + /// + /// \throws std::system_error Thrown on error. + /// \relatesalso response_t + /// + void + read(common::io::stream_t& s, + asio::streambuf& b, + response_t& r) + { + std::error_code ec; + read(s, b, r, ec); + + if (ec) { + throw std::system_error{ec}; + } + } + + /// Read a response message from buffer \a b, reading more content from + /// stream \a s as required. + /// + /// The read response message is placed in \a r. + /// + /// If the buffer \a b contains a full response message, no I/O operations + /// will be performed. + /// + /// Errors are reported through \a ec. + /// + /// \relatesalso response_t + /// + void + read(common::io::stream_t& s, + asio::streambuf& b, + response_t& r, + std::error_code& ec) + { + auto bytes_consumed = common::io::read_until(s, b, match_response_t{r}, ec); + + if (ec) { + return; + } + + b.consume(bytes_consumed); + } + + /// Initialize an asynchronous read of a response message from buffer \a b, + /// reading more content from \a s as required. + /// + /// The read response message is placed in \a r. + /// + /// If the buffer \a b contains a full response message, no I/O operations + /// will be performed. + /// + /// \a r must be valid until the completion-handler \a h is called. + /// + void + async_read(common::io::stream_t& s, + asio::streambuf& b, + response_t& r, + std::function h) + { + auto bound = [&b, h](std::error_code const& ec, std::size_t n) + { + if (!ec) { + b.consume(n); + } + + h(ec); + }; + + common::io::async_read_until(s, b, match_response_t{r}, bound); + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/read.hxx b/code/seafire/protocol/read.hxx new file mode 100644 index 0000000..ad0ab38 --- /dev/null +++ b/code/seafire/protocol/read.hxx @@ -0,0 +1,54 @@ +#ifndef code__seafire__protocol__read_hxx_ +#define code__seafire__protocol__read_hxx_ + +#include +#include +#include + +#include + +#include + +#include +#include + +namespace code::seafire::protocol +{ + + void + read(common::io::stream_t&, + asio::streambuf&, + request_t&); + + void + read(common::io::stream_t&, + asio::streambuf&, + request_t&, + std::error_code&); + + void + async_read(common::io::stream_t&, + asio::streambuf&, + request_t&, + std::function); + + void + read(common::io::stream_t&, + asio::streambuf&, + response_t&); + + void + read(common::io::stream_t&, + asio::streambuf&, + response_t&, + std::error_code&); + + void + async_read(common::io::stream_t&, + asio::streambuf&, + response_t&, + std::function); + +} // namespace code::seafire::protocol + +#endif diff --git a/code/seafire/protocol/reason.cxx b/code/seafire/protocol/reason.cxx new file mode 100644 index 0000000..d7f96bf --- /dev/null +++ b/code/seafire/protocol/reason.cxx @@ -0,0 +1,56 @@ +#include + +namespace code::seafire::protocol +{ + + char const* + get_reason(unsigned short code) + { + switch (code) { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; // rfc7232 + case 307: return "Temporary Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; // rc7235 + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; // rc7235 + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; // rfc7232 + case 413: return "Payload Too Large"; + case 414: return "URI Too Long"; + case 415: return "Unsupported Media Type"; + case 417: return "Expectation Failed"; + case 426: return "Upgrade Required"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "HTTP Version Not Supported"; + + // Unofficial. + case 420: return "Enhance Your Calm"; + } + + return "Unknown"; + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/reason.hxx b/code/seafire/protocol/reason.hxx new file mode 100644 index 0000000..7e67041 --- /dev/null +++ b/code/seafire/protocol/reason.hxx @@ -0,0 +1,12 @@ +#ifndef code__seafire__protocol__reason_hxx_ +#define code__seafire__protocol__reason_hxx_ + +namespace code::seafire::protocol +{ + + char const* + get_reason(unsigned short); + +} // namespace code::seafire::protocol + +#endif diff --git a/code/seafire/protocol/request.cxx b/code/seafire/protocol/request.cxx new file mode 100644 index 0000000..e3de6fe --- /dev/null +++ b/code/seafire/protocol/request.cxx @@ -0,0 +1,149 @@ +#include + +#include + +namespace code::seafire::protocol +{ + + /// Construct a new request message. + /// + request_t:: + request_t() + {} + + /// Construct a new request message. + /// + /// \param method The method of the request message. + /// \param target The target of the request message. + /// \param version The version of the request message. + /// + request_t:: + request_t(std::string method, + std::string target, + version_t version) + { + set_method(std::move(method)); + set_target(std::move(target)); + set_version(version); + } + + /// Access the method of the request message. + /// + std::string const& + request_t:: + method() const + { + return method_; + } + + /// Set the method of the request message. + /// + void + request_t:: + set_method(std::string method) + { + method_ = std::move(method); + } + + /// Access the target of the request message. + /// + std::string const& + request_t:: + target() const + { + return target_; + } + + /// Set the target of the request message. + /// + void + request_t:: + set_target(std::string target) + { + target_ = std::move(target); + } + + /// Access the target of the request as a URI. + /// + uri::uri_t const& + request_t:: + target_uri() const + { + return target_uri_; + } + + /// Set the target URI of the request message. + /// + void + request_t:: + set_target_uri(uri::uri_t target_uri) + { + target_uri_ = std::move(target_uri); + } + + /// Convert an HTTP request message to byte buffers. + /// + /// The buffers are invalidated when the request message is destroyed + /// or any of its properties are modified. + /// + /// The buffers are added to the \a buffers vector. + /// + /// \relatesalso request_t + /// + void + to_buffers(common::io::const_buffers_t& buffers, request_t const& r) + { + static char const colon_space[]{ ':', ' ' }; + static char const crlf[]{ '\r', '\n' }; + static char const space[]{ ' ' }; + + using common::io::buffer; + + buffers.emplace_back(buffer(r.method())); + buffers.emplace_back(buffer(space)); + buffers.emplace_back(buffer(r.target())); + buffers.emplace_back(buffer(space)); + + to_buffers(buffers, r.version()); + buffers.emplace_back(buffer(crlf)); + + for (auto const& header : r.headers()) { + buffers.emplace_back(buffer(header.first)); + buffers.emplace_back(buffer(colon_space)); + buffers.emplace_back(buffer(header.second)); + buffers.emplace_back(buffer(crlf)); + } + + buffers.emplace_back(buffer(crlf)); + } + + /// Convert an HTTP request message to byte buffers. + /// + /// The buffers are invalidated when the request message is destroyed + /// or any of its properties are modified. + /// + /// The buffers are returned as an array of buffers. + /// + /// \relatesalso request_t + /// + common::io::const_buffers_t + to_buffers(request_t const& r) + { + common::io::const_buffers_t buffers; + to_buffers(buffers, r); + return buffers; + } + + /// Write a request message to an output stream. + /// + /// \relatesalso request_t + /// + std::ostream& + operator<<(std::ostream& o, request_t const& r) + { + o << r.method() << ' ' << r.target() << ' ' << r.version() << '\n'; + o << static_cast(r); + return o; + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/request.hxx b/code/seafire/protocol/request.hxx new file mode 100644 index 0000000..df92f23 --- /dev/null +++ b/code/seafire/protocol/request.hxx @@ -0,0 +1,62 @@ +#ifndef code__seafire__protocol__request_hxx_ +#define code__seafire__protocol__request_hxx_ + +#include + +#include + +#include + +#include +#include +#include + +namespace code::seafire::protocol +{ + + class request_t + : public message_t + { + public: + request_t(); + + request_t(std::string, std::string, version_t); + + std::string const& + method() const; + + void + set_method(std::string); + + std::string const& + target() const; + + void + set_target(std::string); + + uri::uri_t const& + target_uri() const; + + void + set_target_uri(uri::uri_t); + + private: + std::string method_; + std::string target_; + uri::uri_t target_uri_; + std::string query_; + + }; + + void + to_buffers(common::io::const_buffers_t&, request_t const&); + + common::io::const_buffers_t + to_buffers(request_t const&); + + std::ostream& + operator<<(std::ostream&, request_t const&); + +} // namespace code::seafire::protocol + +#endif diff --git a/code/seafire/protocol/response.cxx b/code/seafire/protocol/response.cxx new file mode 100644 index 0000000..44eabe0 --- /dev/null +++ b/code/seafire/protocol/response.cxx @@ -0,0 +1,101 @@ +#include + +#include + +namespace code::seafire::protocol +{ + + /// Construct a new response message. + /// + response_t:: + response_t() = default; + + /// Construct a new response message. + /// + /// \param version The HTTP version of this response message. + /// \param status The HTTP status of this response message. + /// + response_t:: + response_t(version_t version, status_code_t status) + : message_t{version}, + status_{status} + {} + + /// Access the message status. + /// + status_code_t const& + response_t:: + status() const + { + return status_; + } + + /// Set the message status. + /// + void + response_t:: + set_status(status_code_t status) + { + status_ = std::move(status); + } + + /// Convert an HTTP response message to byte buffers. + /// + /// The buffers are invalidated when the response message is destroyed + /// or any of its properties are modified. + /// + /// The buffers are added to the \a buffers vector. + /// + /// \relatesalso response_t + /// + void + to_buffers(common::io::const_buffers_t& buffers, response_t const& r) + { + static char const colon_space[]{':', ' '}; + static char const crlf[]{'\r', '\n'}; + static char const digits[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; + static char const space[]{' '}; + + using common::io::buffer; + + to_buffers(buffers, r.version()); + buffers.emplace_back(buffer(space)); + + auto code = r.status(); + + buffers.emplace_back(buffer(&digits[code / 100], 1)); + buffers.emplace_back(buffer(&digits[code % 100 / 10], 1)); + buffers.emplace_back(buffer(&digits[code % 10], 1)); + buffers.emplace_back(buffer(" ", 1)); + + buffers.emplace_back(buffer(r.status().reason())); + buffers.emplace_back(buffer(crlf)); + + for (auto const& header : r.headers()) { + buffers.emplace_back(buffer(header.first)); + buffers.emplace_back(buffer(colon_space)); + buffers.emplace_back(buffer(header.second)); + buffers.emplace_back(buffer(crlf)); + } + + buffers.emplace_back(buffer(crlf)); + } + + /// Convert an HTTP response message to byte buffers. + /// + /// The buffers are invalidated when the response message is destroyed + /// or any of its properties are modified. + /// + /// The buffers are returned as an array of buffers. + /// + /// \relatesalso response_t + /// + common::io::const_buffers_t + to_buffers(response_t const& r) + { + common::io::const_buffers_t buffers; + to_buffers(buffers, r); + return buffers; + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/response.hxx b/code/seafire/protocol/response.hxx new file mode 100644 index 0000000..76a589b --- /dev/null +++ b/code/seafire/protocol/response.hxx @@ -0,0 +1,44 @@ +#ifndef code__seafire__protocol__response_hxx_ +#define code__seafire__protocol__response_hxx_ + +#include +#include + +#include + +#include +#include + +namespace code::seafire::protocol +{ + + /// Represents an HTTP/1.1 response message. + /// + class response_t + : public message_t + { + public: + response_t(); + + response_t(version_t, status_code_t); + + status_code_t const& + status() const; + + void + set_status(status_code_t status); + + private: + status_code_t status_; + + }; + + void + to_buffers(common::io::const_buffers_t&, response_t const&); + + common::io::const_buffers_t + to_buffers(response_t const&); + +} // namespace code::seafire::protocol + +#endif diff --git a/code/seafire/protocol/rfc7230/connection.cxx b/code/seafire/protocol/rfc7230/connection.cxx new file mode 100644 index 0000000..8369127 --- /dev/null +++ b/code/seafire/protocol/rfc7230/connection.cxx @@ -0,0 +1,88 @@ +#include + +namespace code::seafire::protocol::rfc7230 +{ + + connection_t:: + connection_t(std::set tokens) + : tokens_{std::move(tokens)} + {} + + connection_t:: + connection_t(std::initializer_list tokens) + : tokens_{tokens.begin(), tokens.end()} + {} + + std::set const& + connection_t:: + tokens() const + { + return tokens_; + } + + bool + connection_t:: + close() const + { + return tokens().find("close") != tokens().end(); + } + + bool + connection_t:: + keep_alive() const + { + return tokens().find("keep-alive") != tokens().end(); + } + + bool + connection_t:: + upgrade() const + { + return tokens().find("upgrade") != tokens().end(); + } + + std::optional + connection_t:: + try_parse(std::vector const& strings, std::error_code&) + { + std::set tokens; + + for (auto const& j : strings) { + auto begin = j.begin(); + auto end = j.end(); + + auto t = try_parse_tokens(begin, end); + + if (!t) { + return std::nullopt; + } + + if (begin != end) { + return std::nullopt; + } + + for (auto const& k : *t) { + tokens.emplace(k); + } + } + + return std::set{tokens.begin(), tokens.end()}; + } + + std::string + to_string(connection_t const& c) + { + std::string joined; + + for (auto const& j : c.tokens()) { + if (!joined.empty()) { + joined.append(", "); + } + + joined.append(j.str()); + } + + return joined; + } + +} // namespace code::seafire::protocol::rfc7230 diff --git a/code/seafire/protocol/rfc7230/connection.hxx b/code/seafire/protocol/rfc7230/connection.hxx new file mode 100644 index 0000000..5ecd1f5 --- /dev/null +++ b/code/seafire/protocol/rfc7230/connection.hxx @@ -0,0 +1,51 @@ +#ifndef code__seafire__protocol__rfc7230__connection_hxx_ +#define code__seafire__protocol__rfc7230__connection_hxx_ + +#include + +#include +#include +#include +#include + +namespace code::seafire::protocol::rfc7230 +{ + + /// Represents the HTTP `connection` header. + /// + class connection_t + { + public: + static constexpr char const name[] = "connection"; + + connection_t(std::set); + + connection_t(std::initializer_list); + + std::set const& + tokens() const; + + bool + close() const; + + bool + keep_alive() const; + + bool + upgrade() const; + + static + std::optional + try_parse(std::vector const&, std::error_code&); + + private: + std::set tokens_; + + }; + + std::string + to_string(connection_t const&); + +} // namespace code::seafire::protocol::rfc7230 + +#endif diff --git a/code/seafire/protocol/rfc7230/content-length.cxx b/code/seafire/protocol/rfc7230/content-length.cxx new file mode 100644 index 0000000..cc9adb5 --- /dev/null +++ b/code/seafire/protocol/rfc7230/content-length.cxx @@ -0,0 +1,28 @@ +#include + +#include + +namespace code::seafire::protocol::rfc7230 +{ + + std::optional + content_length_t:: + try_parse(std::vector const& strings, std::error_code& ec) + { + if (strings.size() == 1) { + try { + return std::stoull(strings[0]); + } + catch (...) { + ec = protocol_error_t::invalid_content_length; + return std::nullopt; + } + } + else if (strings.size() > 1) { + ec = protocol_error_t::invalid_content_length; + } + + return std::nullopt; + } + +} // namespace code::seafire::protocol::rfc7230 diff --git a/code/seafire/protocol/rfc7230/content-length.hxx b/code/seafire/protocol/rfc7230/content-length.hxx new file mode 100644 index 0000000..bcd2a08 --- /dev/null +++ b/code/seafire/protocol/rfc7230/content-length.hxx @@ -0,0 +1,29 @@ +#ifndef code__seafire__protocol__rfc7230__content_length_hxx_ +#define code__seafire__protocol__rfc7230__content_length_hxx_ + +#include +#include +#include +#include +#include +#include + +namespace code::seafire::protocol::rfc7230 +{ + + // fixme: make class + struct content_length_t + { + using alias_type = std::uint64_t; + + static constexpr char const name[] = "content-length"; + + static + std::optional + try_parse(std::vector const&, std::error_code&); + + }; + +} // namespace code::seafire::protocol::rfc7230 + +#endif diff --git a/code/seafire/protocol/rfc7230/host.cxx b/code/seafire/protocol/rfc7230/host.cxx new file mode 100644 index 0000000..dc212e5 --- /dev/null +++ b/code/seafire/protocol/rfc7230/host.cxx @@ -0,0 +1,95 @@ +#include + +#include + +namespace code::seafire::protocol::rfc7230 +{ + + host_t:: + host_t(std::string hostname) + : hostname_{std::move(hostname)} + {} + + host_t:: + host_t(std::string hostname, std::optional port) + : hostname_{std::move(hostname)}, port_{std::move(port)} + {} + + std::string const& + host_t:: + hostname() const + { + return hostname_; + } + + std::optional const& + host_t:: + port() const + { + return port_; + } + + std::optional + host_t:: + try_parse(std::vector const& strings, std::error_code&) + { + std::string host_part; + std::optional opt_port_part; + + if (auto it = strings.rbegin(); it != strings.rend()) { + auto first = it->begin(); + auto last = it->end(); + + auto try_parse_host = [&](auto init) + { + auto c = init; + + while (c != last && uri::grammar::is_host(*c)) { + host_part += *c++; + } + + return c; + }; + + auto try_parse_port = [&](auto init) + { + auto c = init; + + if (c != last && *c == ':') { + ++c; // skips ':' + + opt_port_part = std::string{}; + + while (c != last && uri::grammar::is_digit(*c)) { + *opt_port_part += *c++; + } + + return c; + } + + return init; + }; + + first = try_parse_host(first); + first = try_parse_port(first); + + if (first != last) { + return std::nullopt; + } + } + + return {{host_part, opt_port_part}}; + } + + std::string + to_string(host_t const& host) + { + std::string str{host.hostname()}; + + if (host.port()) + str += ':' + *host.port(); + + return str; + } + +} // namespace code::seafire::protocol::rfc7230 diff --git a/code/seafire/protocol/rfc7230/host.hxx b/code/seafire/protocol/rfc7230/host.hxx new file mode 100644 index 0000000..5bf48d4 --- /dev/null +++ b/code/seafire/protocol/rfc7230/host.hxx @@ -0,0 +1,43 @@ +#ifndef code__seafire__protocol__rfc7230__host_hxx_ +#define code__seafire__protocol__rfc7230__host_hxx_ + +#include +#include +#include +#include + +namespace code::seafire::protocol::rfc7230 +{ + + class host_t + { + public: + static constexpr char const name[] = "host"; + + explicit + host_t(std::string); + + host_t(std::string, std::optional); + + std::string const& + hostname() const; + + std::optional const& + port() const; + + static + std::optional + try_parse(std::vector const&, std::error_code&); + + private: + std::string hostname_; + std::optional port_; + + }; + + std::string + to_string(host_t const&); + +} // namespace code::seafire::protocol::rfc7230 + +#endif diff --git a/code/seafire/protocol/rfc7231/accept.cxx b/code/seafire/protocol/rfc7231/accept.cxx new file mode 100644 index 0000000..561dcfe --- /dev/null +++ b/code/seafire/protocol/rfc7231/accept.cxx @@ -0,0 +1,16 @@ +#include + +namespace code::seafire::protocol::rfc7231 +{ + + std::optional + accept_t:: + try_parse(std::vector const& strings, std::error_code& ec) + { + if (strings.empty()) + return std::nullopt; + + return media_range_t::try_parse(strings, ec); + } + +} // namespace code::seafire::protocol::rfc7231 diff --git a/code/seafire/protocol/rfc7231/accept.hxx b/code/seafire/protocol/rfc7231/accept.hxx new file mode 100644 index 0000000..36c35b4 --- /dev/null +++ b/code/seafire/protocol/rfc7231/accept.hxx @@ -0,0 +1,27 @@ +#ifndef code__seafire__protocol__rc7231__accept_hxx_ +#define code__seafire__protocol__rc7231__accept_hxx_ + +#include + +#include +#include +#include + +namespace code::seafire::protocol::rfc7231 +{ + + struct accept_t + { + using alias_type = media_range_t; + + static constexpr char const name[] = "accept"; + + static + std::optional + try_parse(std::vector< std::string > const& strings, std::error_code& ec); + + }; + +} // namespace code::seafire::protocol::rfc7231 + +#endif diff --git a/code/seafire/protocol/rfc7231/allow.hxx b/code/seafire/protocol/rfc7231/allow.hxx new file mode 100644 index 0000000..d85a455 --- /dev/null +++ b/code/seafire/protocol/rfc7231/allow.hxx @@ -0,0 +1,36 @@ +#ifndef code__seafire__protocol__rc7231__allow_hxx_ +#define code__seafire__protocol__rc7231__allow_hxx_ + +#include + +#include +#include + +namespace code::seafire::protocol::rfc7231 +{ + + struct allow_t { + using alias_type = tokens_t; + + static constexpr char const name[] = "allow"; + + static + std::string + to_string(tokens_t const& tokens) + { + std::ostringstream str; + + if (auto it = tokens.begin(); it != tokens.end()) { + str << *it; + + while (++it != tokens.end()) + str << ", " << *it; + } + + return str.str(); + } + }; + +} // namespace code::seafire::protocol::rfc7231 + +#endif diff --git a/code/seafire/protocol/rfc7231/content-type.cxx b/code/seafire/protocol/rfc7231/content-type.cxx new file mode 100644 index 0000000..e871792 --- /dev/null +++ b/code/seafire/protocol/rfc7231/content-type.cxx @@ -0,0 +1,17 @@ +#include + +namespace code::seafire::protocol::rfc7231 +{ + + std::optional + content_type_t:: + try_parse(std::vector const& strings, std::error_code& ec) + { + if (auto it = strings.rbegin(); it != strings.rend()) { + return media_type_t::try_parse(*it, ec); + } + + return {}; + } + +} // namespace code::seafire::protocol::rfc7231 diff --git a/code/seafire/protocol/rfc7231/content-type.hxx b/code/seafire/protocol/rfc7231/content-type.hxx new file mode 100644 index 0000000..cff6d3b --- /dev/null +++ b/code/seafire/protocol/rfc7231/content-type.hxx @@ -0,0 +1,28 @@ +#ifndef code__seafire__protocol__rfc7231__content_type_hxx_ +#define code__seafire__protocol__rfc7231__content_type_hxx_ + +#include + +#include +#include +#include + +namespace code::seafire::protocol::rfc7231 +{ + + // fixme: make class + struct content_type_t + { + using alias_type = media_type_t; + + static constexpr char const name[] = "content-type"; + + static + std::optional + try_parse(std::vector const&, std::error_code&); + + }; + +} // namespace code::seafire::protocol::rfc7231 + +#endif diff --git a/code/seafire/protocol/rfc7231/date.hxx b/code/seafire/protocol/rfc7231/date.hxx new file mode 100644 index 0000000..753e8b2 --- /dev/null +++ b/code/seafire/protocol/rfc7231/date.hxx @@ -0,0 +1,43 @@ +#ifndef code__seafire__protocol__rfc7231__date_hxx_ +#define code__seafire__protocol__rfc7231__date_hxx_ + +#include + +#include +#include +#include +#include + +namespace code::seafire::protocol::rfc7231 +{ + + /// fixme: make class + struct date_t + { + using alias_type = std::chrono::system_clock::time_point; + + static constexpr char const name[] = "date"; + + static + std::optional + try_parse(std::vector const& strings, std::error_code&) + { + if (strings.empty()) { + return std::nullopt; + } + + return try_parse_http_date(strings.front()); + } + + static + std::string + to_string(std::chrono::system_clock::time_point const& time) + { + return format_http_date(time); + } + + }; + +} // namespace code::seafire::protocol::rfc7231 + +#endif diff --git a/code/seafire/protocol/rfc7231/location.hxx b/code/seafire/protocol/rfc7231/location.hxx new file mode 100644 index 0000000..ee28294 --- /dev/null +++ b/code/seafire/protocol/rfc7231/location.hxx @@ -0,0 +1,28 @@ +#ifndef code__seafire__protocol__rc7231__location_hxx_ +#define code__seafire__protocol__rc7231__location_hxx_ + +#include + +#include +#include + +namespace code::seafire::protocol::rfc7231 +{ + + struct location_t + { + using alias_type = uri::uri_t; + + static constexpr const char name[] = "location"; + + static + std::string + to_string(uri::uri_t const& location) + { + return uri::to_string(location); + } + }; + +} // namespace code::seafire::protocol::rfc7231 + +#endif diff --git a/code/seafire/protocol/rfc7231/media-range.cxx b/code/seafire/protocol/rfc7231/media-range.cxx new file mode 100644 index 0000000..e98cd2e --- /dev/null +++ b/code/seafire/protocol/rfc7231/media-range.cxx @@ -0,0 +1,112 @@ +#include + +namespace code::seafire::protocol::rfc7231 +{ + + media_range_t:: + media_range_t() + {} + + media_range_t:: + media_range_t(media_type_t type) + : types_{std::move(type)} + {} + + media_range_t:: + media_range_t(std::vector types) + : types_{std::move(types)} + { + sort_internals(); + } + + media_range_t::const_iterator + media_range_t:: + begin() const + { + return get().begin(); + } + + media_range_t::const_iterator + media_range_t:: + cbegin() const + { + return get().cbegin(); + } + + media_range_t::const_iterator + media_range_t:: + end() const + { + return get().end(); + } + + media_range_t::const_iterator + media_range_t:: + cend() const + { + return get().cend(); + } + + std::vector const& + media_range_t:: + get() const + { + return types_; + } + + std::optional + media_range_t:: + try_parse(std::vector const& strings, std::error_code& ec) + { + if (auto it = strings.rbegin(); it != strings.rend()) { + auto begin = it->begin(); + return try_parse(begin, it->end(), ec); + } + + return std::nullopt; + } + + void + media_range_t:: + sort_internals() + { + struct { + int + specificity(media_type_t const& type) + { + if (type.type() == "*") + return 0; + + if (type.subtype() == "*") + return 1; + + if (type.params().size() < 1) + return 2; + + return 3; + } + + bool + operator()(media_type_t const& a, media_type_t const& b) + { + auto const sa = specificity(a); + auto const sb = specificity(b); + + if (sa == sb) { + if (a.type() < b.type()) + return true; + + if (a.subtype() < b.subtype()) + return true; + + return false; + } + + return sb < sa; + } + } precedence; + + std::sort(types_.begin(), types_.end(), precedence); + } + +} // namespace code::seafire::protocol::rfc7231 diff --git a/code/seafire/protocol/rfc7231/media-range.hxx b/code/seafire/protocol/rfc7231/media-range.hxx new file mode 100644 index 0000000..db840fd --- /dev/null +++ b/code/seafire/protocol/rfc7231/media-range.hxx @@ -0,0 +1,60 @@ +#ifndef code_seafire__protocol__rc7231__media_range_hxx_ +#define code_seafire__protocol__rc7231__media_range_hxx_ + +#include + +#include +#include +#include + +namespace code::seafire::protocol::rfc7231 +{ + + class media_range_t { + public: + using const_iterator = typename std::vector::const_iterator; + + media_range_t(); + + media_range_t(media_type_t type); + + media_range_t(std::vector types); + + const_iterator + begin() const; + + const_iterator + cbegin() const; + + const_iterator + end() const; + + const_iterator + cend() const; + + std::vector const& + get() const; + + template + static + std::optional + try_parse(InputIterator& begin, + InputIterator const& end, + std::error_code& ec); + + static + std::optional + try_parse(std::vector const& strings, std::error_code& ec); + + private: + void + sort_internals(); + + std::vector types_; + }; + +} // namespace code::seafire::protocol::rfc7231 + +#include + +#endif diff --git a/code/seafire/protocol/rfc7231/media-range.txx b/code/seafire/protocol/rfc7231/media-range.txx new file mode 100644 index 0000000..a42806b --- /dev/null +++ b/code/seafire/protocol/rfc7231/media-range.txx @@ -0,0 +1,37 @@ +namespace code::seafire::protocol::rfc7231 +{ + + template + std::optional + media_range_t:: + try_parse(InputIterator& begin, + InputIterator const& end, + std::error_code& ec) + { + auto skip_whitespace = [&] { + while (begin != end && grammar::is_space(*begin)) + ++begin; + }; + + std::vector types; + + while (begin != end) { + skip_whitespace(); + + auto type = media_type_t::try_parse(begin, end, ec); + + if (ec || !type) + return std::nullopt; + + types.emplace_back(std::move(*type)); + + skip_whitespace(); + + if (begin != end && *begin == ',') + ++begin; + } + + return types; + } + +} // namespace code::seafire::protocol::rfc7231 diff --git a/code/seafire/protocol/rfc7231/product.cxx b/code/seafire/protocol/rfc7231/product.cxx new file mode 100644 index 0000000..7a7ee1e --- /dev/null +++ b/code/seafire/protocol/rfc7231/product.cxx @@ -0,0 +1,96 @@ +#include + +namespace code::seafire::protocol::rfc7231 +{ + + product_t:: + product_t(token_t name) + : name_{std::move(name)} + {} + + product_t:: + product_t(token_t name, token_t version) + : name_{std::move(name)}, + version_{std::move(version)} + {} + + token_t const& + product_t:: + name() const + { + return name_; + } + + std::optional const& + product_t:: + version() const + { + return version_; + } + + void + to_stream(std::ostream& o, product_t const& p) + { + if (p.name().empty()) + return; + + o << p.name(); + + if (p.version() && !p.version()->empty()) + o << "/" << *p.version(); + } + + std::string + to_string(product_t const& p) + { + std::ostringstream str_stream; + to_stream(str_stream, p); + return str_stream.str(); + } + + void + to_stream(std::ostream& o, products_t const& products) + { + for (auto const& j : products) { + to_stream(o, j); + o << " "; + } + } + + std::string + to_string(products_t const& products) + { + std::ostringstream str_stream; + to_stream(str_stream, products); + return str_stream.str(); + } + + std::optional + try_parse_product(std::string const& str) + { + auto begin = str.begin(); + return try_parse_product(begin, str.end()); + } + + std::optional + try_parse_product(std::string const& str, std::error_code& ec) + { + auto begin = str.begin(); + return try_parse_product(begin, str.end(), ec); + } + + std::optional + try_parse_products(std::string const& str) + { + auto begin = str.begin(); + return try_parse_products(begin, str.end()); + } + + std::optional + try_parse_products(std::string const& str, std::error_code& ec) + { + auto begin = str.begin(); + return try_parse_products(begin, str.end(), ec); + } + +} // namespace code::seafire::protocol::rfc7231 diff --git a/code/seafire/protocol/rfc7231/product.hxx b/code/seafire/protocol/rfc7231/product.hxx new file mode 100644 index 0000000..4d83a3a --- /dev/null +++ b/code/seafire/protocol/rfc7231/product.hxx @@ -0,0 +1,77 @@ +#ifndef code__seafire__protocol__rfc7231__product_hxx_ +#define code__seafire__protocol__rfc7231__product_hxx_ + +#include +#include + +#include +#include +#include +#include +#include + +namespace code::seafire::protocol::rfc7231 +{ + + class product_t { + public: + product_t(token_t); + + product_t(token_t, token_t); + + token_t const& + name() const; + + std::optional const& + version() const; + + private: + token_t name_; + std::optional version_; + + }; + + using products_t = std::vector; + + void + to_stream(std::ostream&, product_t const&); + + std::string + to_string(product_t const&); + + void + to_stream(std::ostream&, products_t const&); + + std::string + to_string(products_t const&); + + template + std::optional + try_parse_product(InputIterator&, InputIterator); + + template + std::optional + try_parse_product(InputIterator&, InputIterator, std::error_code& ec); + + std::optional + try_parse_product(std::string const&); + + template + std::optional + try_parse_products(InputIterator&, InputIterator); + template + + std::optional + try_parse_products(InputIterator&, InputIterator, std::error_code&); + + std::optional + try_parse_products(std::string const&); + + std::optional + try_parse_products(std::string const&, std::error_code&); + +} // namespace code::seafire::protocol::rfc7231 + +#include + +#endif diff --git a/code/seafire/protocol/rfc7231/product.txx b/code/seafire/protocol/rfc7231/product.txx new file mode 100644 index 0000000..9eeda9e --- /dev/null +++ b/code/seafire/protocol/rfc7231/product.txx @@ -0,0 +1,115 @@ +namespace code::seafire::protocol::rfc7231 +{ + + template + std::optional + try_parse_product(InputIterator& first, InputIterator last) + { + auto name = try_parse_token(first, last); + + if (!name) { + return std::nullopt; + } + + if (first != last && *first == '/') { + ++first; // skips '/' + auto version = try_parse_token(first, last); + + if (!version) { + return std::nullopt; + } + + return {{*name, *version}}; + } + + return {{*name}}; + } + + template + std::optional + try_parse_product(InputIterator& first, InputIterator last, std::error_code& ec) + { + ec = {}; + + auto name = try_parse_token(first, last, ec); + + if (ec) { + return std::nullopt; + } + + if (!name) { + return std::nullopt; + } + + if (first != last && *first == '/') { + ++first; // skips '/' + auto version = try_parse_token(first, last, ec); + + if (ec) { + return std::nullopt; + } + + if (!version) { + return std::nullopt; + } + + return {{*name, *version}}; + } + + return {{*name}}; + } + + + template + std::optional + try_parse_products(InputIterator& first, InputIterator last) + { + products_t products; + + while (first != last) { + auto p = try_parse_product(first, last); + + if (!p) { + return std::nullopt; + } + + products.emplace_back(std::move(*p)); + + while (first != last && grammar::is_space(*first)) { + ++first; // skips whitespace + } + } + + return products; + } + + template + std::optional + try_parse_products(InputIterator& first, InputIterator last, std::error_code& ec) + { + ec = {}; + + products_t products; + + while (first != last) { + auto p = try_parse_product(first, last, ec); + + if (ec) { + return std::nullopt; + } + + if (!p) { + return std::nullopt; + } + + products.emplace_back(std::move(*p)); + + while (first != last && grammar::is_space(*first)) { + ++first; // skips whitespace + } + } + + return products; + } + +} // namespace code::seafire::protocol::rfc7231 diff --git a/code/seafire/protocol/rfc7231/server.hxx b/code/seafire/protocol/rfc7231/server.hxx new file mode 100644 index 0000000..1842dd9 --- /dev/null +++ b/code/seafire/protocol/rfc7231/server.hxx @@ -0,0 +1,42 @@ +#ifndef code__seafire__protocol__rfc7231__server_hxx_ +#define code__seafire__protocol__rfc7231__server_hxx_ + +#include + +#include +#include + +namespace code::seafire::protocol::rfc7231 +{ + + struct server_t + { + using alias_type = products_t; + + static constexpr const char* name = "server"; + + static + std::optional + try_parse(std::vector const& strings) + { + if (auto it = strings.rbegin(); it != strings.rend()) + return try_parse_products(*it); + + return {}; + } + + static + std::optional + try_parse(std::vector const& strings, std::error_code& ec) + { + if (auto it = strings.rbegin(); it != strings.rend()) + return try_parse_products(*it, ec); + + return {}; + } + + }; + +} // namespace code::seafire::protocol::rfc7231 + +#endif diff --git a/code/seafire/protocol/rfc7232/entity-tag.cxx b/code/seafire/protocol/rfc7232/entity-tag.cxx new file mode 100644 index 0000000..246a680 --- /dev/null +++ b/code/seafire/protocol/rfc7232/entity-tag.cxx @@ -0,0 +1,76 @@ +#include + +namespace code::seafire::protocol::rfc7232 +{ + + entity_tag_t:: + entity_tag_t(std::string tag, tag_type type) + : tag_{ std::move(tag) }, type_{ type } + { + if (auto p = this->tag().find('"'); p != std::string::npos) + throw std::invalid_argument{ "ETag may not contain \"" }; + } + + std::string const& + entity_tag_t:: + tag() const + { + return tag_; + } + + entity_tag_t::tag_type + entity_tag_t:: + type() const + { + return type_; + } + + std::string + to_string(entity_tag_t const& et) + { + if (is_strong(et)) + return "\"" + et.tag() + "\""; + + return "W/\"" + et.tag() + '"'; + } + + bool + is_strong(entity_tag_t const& et) + { + return et.type() == entity_tag_t::strong; + } + + bool + is_weak(entity_tag_t const& et) + { + return et.type() == entity_tag_t::weak; + } + + bool + operator==(entity_tag_t const& lhs, entity_tag_t const& rhs) + { + return strong_compare(lhs, rhs); + } + + bool + operator!=(entity_tag_t const& lhs, entity_tag_t const& rhs) + { + return !(lhs == rhs); + } + + bool + strong_compare(entity_tag_t const& lhs, entity_tag_t const& rhs) + { + if (is_weak(lhs) || is_weak(rhs)) + return false; + + return lhs.tag() == rhs.tag(); + } + + bool + weak_compare(entity_tag_t const& lhs, entity_tag_t const& rhs) + { + return lhs.tag() == rhs.tag(); + } + +} // namespace code::seafire::protocol::rfc7232 diff --git a/code/seafire/protocol/rfc7232/entity-tag.hxx b/code/seafire/protocol/rfc7232/entity-tag.hxx new file mode 100644 index 0000000..d9cd750 --- /dev/null +++ b/code/seafire/protocol/rfc7232/entity-tag.hxx @@ -0,0 +1,72 @@ +#ifndef code__seafire__protocol__entity_tag_hxx_ +#define code__seafire__protocol__entity_tag_hxx_ + +#include +#include +#include +#include + +namespace code::seafire::protocol::rfc7232 +{ + + class entity_tag_t + { + public: + enum class tag_type { strong, weak }; + + static constexpr tag_type strong = tag_type::strong; + static constexpr tag_type weak = tag_type::weak; + + entity_tag_t(std::string tag, tag_type type); + + std::string const& + tag() const; + + tag_type + type() const; + + template + static + std::optional + try_parse(InputIterator& it, + InputIterator const& end, + std::error_code& ec); + + template + static + std::optional + try_parse(InputIterator&& it, + InputIterator const& end, + std::error_code& ec); + + private: + std::string tag_; + tag_type type_; + }; + + std::string + to_string(entity_tag_t const& et); + + bool + is_strong(entity_tag_t const& et); + + bool + is_weak(entity_tag_t const& et); + + bool + operator==(entity_tag_t const& lhs, entity_tag_t const& rhs); + + bool + operator!=(entity_tag_t const& lhs, entity_tag_t const& rhs); + + bool + strong_compare(entity_tag_t const& lhs, entity_tag_t const& rhs); + + bool + weak_compare(entity_tag_t const& lhs, entity_tag_t const& rhs); + + } // namespace code::seafire::protocol::rfc7232 + +#include + +#endif diff --git a/code/seafire/protocol/rfc7232/entity-tag.txx b/code/seafire/protocol/rfc7232/entity-tag.txx new file mode 100644 index 0000000..a7c2532 --- /dev/null +++ b/code/seafire/protocol/rfc7232/entity-tag.txx @@ -0,0 +1,55 @@ +namespace code::seafire::protocol::rfc7232 +{ + + template + std::optional + entity_tag_t:: + try_parse(InputIterator& it, + InputIterator const& end, + std::error_code& ec) + { + bool weak = false; + + if (it == end) + return {}; + + if (*it == 'W') { + ++it; // consume 'W' + + if (it == end || *it != '/') + return {}; + + ++it; // consume '/' + + weak = true; + } + + if (it == end || *it != '"') + return {}; + + ++it; // consume '"' + + std::string opaque; + + while (it != end && *it != '"') // TODO: Validate *it + opaque += *it++; + + if (it == end || *it != '"') + return {}; + + ++it; // consume '"' + + return entity_tag_t{opaque, weak ? entity_tag_t::weak : entity_tag_t::strong}; + } + + template + std::optional + entity_tag_t:: + try_parse(InputIterator&& it, + InputIterator const& end, + std::error_code& ec) + { + return try_parse(it, end, ec); + } + +} // namespace code::seafire::protocol::rfc7232 diff --git a/code/seafire/protocol/rfc7232/etag.hxx b/code/seafire/protocol/rfc7232/etag.hxx new file mode 100644 index 0000000..ac7f7ef --- /dev/null +++ b/code/seafire/protocol/rfc7232/etag.hxx @@ -0,0 +1,18 @@ +#ifndef code__seafire__protocol__rfc7232__etag_hxx_ +#define code__seafire__protocol__rfc7232__etag_hxx_ + +#include + +namespace code::seafire::protocol::rfc7232 +{ + + struct etag_t { + using alias_type = entity_tag_t; + + static constexpr char const name[] = "etag"; + + }; + +} // namespace code::seafire::protocol::rcf7232 + +#endif diff --git a/code/seafire/protocol/rfc7232/if-match.hxx b/code/seafire/protocol/rfc7232/if-match.hxx new file mode 100644 index 0000000..89ce4b0 --- /dev/null +++ b/code/seafire/protocol/rfc7232/if-match.hxx @@ -0,0 +1,55 @@ +#ifndef libseafire__protocol__rfc7232__if_match_hxx_ +#define libseafire__protocol__rfc7232__if_match_hxx_ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace code::seafire::protocol::rfc7232 +{ + + class if_match_t + { + public: + static constexpr char const name[] = "if-match"; + + struct anything_t {}; + + static constexpr anything_t anything{}; + + if_match_t(anything_t); + + if_match_t(std::vector); + + bool + is_anything() const; + + std::vector const& + tags() const; + + static + std::optional + try_parse(std::vector const&, + std::error_code&); + + private: + std::variant< + anything_t, std::vector + > value_; + }; + + std::string + to_string(if_match_t const&); + +} // namespace code::seafire::protocol::rfc7232 + +#include + +#endif diff --git a/code/seafire/protocol/rfc7232/if-match.ixx b/code/seafire/protocol/rfc7232/if-match.ixx new file mode 100644 index 0000000..f781d39 --- /dev/null +++ b/code/seafire/protocol/rfc7232/if-match.ixx @@ -0,0 +1,115 @@ +namespace code::seafire::protocol::rfc7232 +{ + + inline + if_match_t:: + if_match_t(anything_t) + : value_{anything} + {} + + inline + if_match_t:: + if_match_t(std::vector tags) + : value_{std::move(tags)} + {} + + inline + bool + if_match_t:: + is_anything() const + { + return std::holds_alternative(value_); + } + + inline + std::vector const& + if_match_t:: + tags() const + { + if (std::holds_alternative>(value_)) + return std::get>(value_); + + throw std::invalid_argument{"invalid if-match"}; + } + + inline + std::optional + if_match_t:: + try_parse(std::vector const& strings, + std::error_code& ec) + { + std::vector tags; + + for (auto const& j : strings) { + auto it = j.begin(); + + while (it != j.end()) { + grammar::skip_space(it, j.end()); + + if (it != j.end() && *it == '*') { + ++it; // consume '*' + + grammar::skip_space(it, j.end()); + + if (it != j.end() || tags.size() > 0) { + ec = make_error_code(protocol_error_t::invalid_header_value); + return std::nullopt; + } + + return anything; + } + + auto tag = entity_tag_t::try_parse(it, j.end(), ec); + + if (ec) + return std::nullopt; + + if (!tag) { + ec = make_error_code(protocol_error_t::invalid_header_value); + return std::nullopt; + } + + tags.emplace_back(std::move(*tag)); + + grammar::skip_space(it, j.end()); + + if (it != j.end()) { + if (*it != ',') { + ec = make_error_code(protocol_error_t::invalid_header_value); + return std::nullopt; + } + + ++it; + } + } + } + + if (tags.empty()) + return {}; + + return tags; + } + + inline + std::string + to_string(if_match_t const& if_match) + { + if (if_match.is_anything()) + return "*"; + + std::ostringstream str; + + auto it = if_match.tags().begin(); + + if (it != if_match.tags().end()) + str << to_string(*it++); + + while (it != if_match.tags().end()) { + str << ", "; + str << to_string(*it++); + } + + return str.str(); + } + +} // namespace code::seafire::protocol::rfc7232 diff --git a/code/seafire/protocol/rfc7232/if-modified-since.hxx b/code/seafire/protocol/rfc7232/if-modified-since.hxx new file mode 100644 index 0000000..67b12cb --- /dev/null +++ b/code/seafire/protocol/rfc7232/if-modified-since.hxx @@ -0,0 +1,38 @@ +#ifndef code__seafire__protocol__rfc7232__if_modified_since_hxx_ +#define code__seafire__protocol__rfc7232__if_modified_since_hxx_ + +#include + +#include +#include +#include +#include + +namespace code::seafire::protocol::rfc7232 { + + struct if_modified_since_t + { + using alias_type = std::chrono::system_clock::time_point; + + static constexpr const char name[] = "if-modified-since"; + + static std::string + to_string(std::chrono::system_clock::time_point const& point_in_time) + { + return format_http_date(point_in_time); + } + + static + std::optional + try_parse(std::vector const& strings, std::error_code& ec) + { + if (strings.empty()) + return {}; + + return try_parse_http_date(strings.front()); + } + }; + +} // namespace code::seafire::protocol::rfc7232 + +#endif diff --git a/code/seafire/protocol/rfc7232/if-none-match.hxx b/code/seafire/protocol/rfc7232/if-none-match.hxx new file mode 100644 index 0000000..68c2ad9 --- /dev/null +++ b/code/seafire/protocol/rfc7232/if-none-match.hxx @@ -0,0 +1,34 @@ +#ifndef code__seafire__protocol__rfc7232__if_none_match_hxx_ +#define code__seafire__protocol__rfc7232__if_none_match_hxx_ + +#include + +#include +#include + +namespace code::seafire::protocol::rfc7232 +{ + + struct if_none_match_t + { + static constexpr char const name[] = "if-none-match"; + + static std::optional + try_parse(std::vector const& strings, std::error_code& ec) + { + // FIXME implement. + return {}; + } + }; + + inline + std::string + to_string(if_none_match_t const& if_none_match) + { + // FIXME implement. + return {}; + } + +} // namespace code::seafire::protocol::rfc7232 + +#endif diff --git a/code/seafire/protocol/rfc7232/if-unmodified-since.hxx b/code/seafire/protocol/rfc7232/if-unmodified-since.hxx new file mode 100644 index 0000000..6b58cea --- /dev/null +++ b/code/seafire/protocol/rfc7232/if-unmodified-since.hxx @@ -0,0 +1,38 @@ +#ifndef code__seafire__protocol__rfc7232__if_unmodified_since_hxx_ +#define code__seafire__protocol__rfc7232__if_unmodified_since_hxx_ + +#include + +#include +#include +#include +#include + +namespace code::seafire::protocol::rfc7232 +{ + + struct if_unmodified_since_t + { + using alias_type = std::chrono::system_clock::time_point; + + static constexpr const char name[] = "if-unmodified-since"; + + static std::string + to_string(std::chrono::system_clock::time_point const& point_in_time) + { + return format_http_date(point_in_time); + } + + static std::optional + try_parse(std::vector const& strings, std::error_code& ec) + { + if (strings.empty()) + return {}; + + return try_parse_http_date(strings.front()); + } + }; + +} // namespace code::seafire::protocol::rfc7232 + +#endif diff --git a/code/seafire/protocol/rfc7232/last-modified.hxx b/code/seafire/protocol/rfc7232/last-modified.hxx new file mode 100644 index 0000000..ab46890 --- /dev/null +++ b/code/seafire/protocol/rfc7232/last-modified.hxx @@ -0,0 +1,39 @@ +#ifndef code__seafire__protocol__rc7232__last_modified_hxx_ +#define code__seafire__protocol__rc7232__last_modified_hxx_ + +#include + +#include +#include +#include +#include +#include + +namespace code::seafire::protocol::rfc7232 +{ + + struct last_modified_t + { + using alias_type = std::chrono::system_clock::time_point; + + static constexpr const char name[] = "last-modified"; + + static std::string + to_string(std::chrono::system_clock::time_point const& point_in_time) + { + return format_http_date(point_in_time); + } + + static std::optional + try_parse(std::vector const& strings) + { + if (strings.empty()) + return std::nullopt; + + return try_parse_http_date(strings.front()); + } + }; + +} // namespace code::seafire::protocol::rfc7232 + +#endif diff --git a/code/seafire/protocol/status-code.cxx b/code/seafire/protocol/status-code.cxx new file mode 100644 index 0000000..d24f7cf --- /dev/null +++ b/code/seafire/protocol/status-code.cxx @@ -0,0 +1,157 @@ +#include + +#include + +namespace code::seafire::protocol +{ + + status_code_t:: + status_code_t() = default; + + /// Construct a new status code. + /// + /// Valid codes are 100 to 999 (inclusive). + /// + /// The reason string will be set if the status code is standard. + /// + /// \throws std::invalid_argument Thrown if \a code is invalid. + /// + status_code_t:: + status_code_t(unsigned short code) + : code_{code}, reason_{get_reason(code)} + { + if (code < 100 || 999 < code) { + throw std::invalid_argument{"invalid status code"}; + } + } + + /// Construct a new status code with a custom reason. + /// + /// Valid codes are 100 to 999 (inclusive). + /// + /// \throws std::invalid_argument Thrown if \a code is invalid. + /// + status_code_t:: + status_code_t(unsigned short code, std::string reason) + : code_{ code }, reason_{ std::move(reason) } + { + if (code < 100 || 999 < code) { + throw std::invalid_argument{"invalid status code"}; + } + } + + /// Access the code of this status code. + /// + unsigned short + status_code_t:: + code() const + { + return code_; + } + + /// Access the reason string of this status code. + /// + std::string const& + status_code_t:: + reason() const + { + return reason_; + } + + /// Convert this status code to an unsigned short value. + /// + status_code_t:: + operator unsigned short() + { + return code_; + } + + /// Compare two status codes for equality. + /// + bool + operator==(status_code_t const& lhs, status_code_t const& rhs) + { + return lhs.code() == rhs.code(); + } + + /// Compare two status codes for equality. + /// + bool + operator==(status_code_t const& lhs, unsigned short rhs) + { + return lhs.code() == rhs; + } + + /// Compare two status codes for equality. + /// + bool + operator==(unsigned short lhs, status_code_t const& rhs) + { + return lhs == rhs.code(); + } + + /// Compare two status codes for inequality. + /// + bool + operator!=(status_code_t const& lhs, status_code_t const& rhs) + { + return !(lhs == rhs); + } + + /// Compare two status codes for inequality. + /// + bool + operator!=(status_code_t const& lhs, unsigned short rhs) + { + return !(lhs == rhs); + } + + /// Compare two status codes for inequality. + /// + bool + operator!=(unsigned short lhs, status_code_t const& rhs) + { + return !(lhs == rhs); + } + + /// Check if the given status code, \a sc, is informational. + /// + bool + is_informational(status_code_t const& sc) + { + return (sc.code() / 100) == 1; + } + + /// Check if the given status code, \a sc, indicates success. + /// + bool + is_success(status_code_t const& sc) + { + return (sc.code() / 100) == 2; + } + + /// Check if the given status code, \a sc, is a redirection. + /// + bool + is_redirection(status_code_t const& sc) + { + return (sc.code() / 100) == 3; + } + + /// Check if the given status code, \a sc, indicates a client error. + /// + bool + is_client_error(status_code_t const& sc) + { + return (sc.code() / 100) == 4; + } + + /// Check if the given status code, \a sc, indicates a server error. + /// + bool + is_server_error(status_code_t const& sc) + { + return (sc.code() / 100) == 5; + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/status-code.hxx b/code/seafire/protocol/status-code.hxx new file mode 100644 index 0000000..ad67ef9 --- /dev/null +++ b/code/seafire/protocol/status-code.hxx @@ -0,0 +1,71 @@ +#ifndef code__seafire__protocol__status_code_hxx_ +#define code__seafire__protocol__status_code_hxx_ + +#include + +#include + +namespace code::seafire::protocol +{ + + /// Represents an HTTP status code. + /// + class status_code_t + { + public: + status_code_t(); + + status_code_t(unsigned short code); + + status_code_t(unsigned short code, std::string reason); + + unsigned short + code() const; + + std::string const& + reason() const; + + operator unsigned short(); + + private: + unsigned short code_{0}; + std::string reason_; + + }; + + bool + operator==(status_code_t const&, status_code_t const&); + + bool + operator==(status_code_t const&, unsigned short); + + bool + operator==(unsigned short, status_code_t const&); + + bool + operator!=(status_code_t const&, status_code_t const&); + + bool + operator!=(status_code_t const&, unsigned short); + + bool + operator!=(unsigned short, status_code_t const&); + + bool + is_informational(status_code_t const&); + + bool + is_success(status_code_t const&); + + bool + is_redirection(status_code_t const&); + + bool + is_client_error(status_code_t const&); + + bool + is_server_error(status_code_t const&); + +} // namespace code::seafire::protocol + +#endif diff --git a/code/seafire/protocol/token.cxx b/code/seafire/protocol/token.cxx new file mode 100644 index 0000000..42fb678 --- /dev/null +++ b/code/seafire/protocol/token.cxx @@ -0,0 +1,80 @@ +#include + +namespace code::seafire::protocol +{ + + /// Construct a new empty token. + /// + token_t:: + token_t() = default; + + /// Construct a new token from a string. + /// + /// \param str The token string. + /// \throws std::invalid_argument Thrown if \a str is invalid. + /// + token_t:: + token_t(std::string str) + : str_{ + validate_token(str) + ? std::move(str) + : throw std::invalid_argument{"invalid token"} + } + {} + + /// Construct a new token from a string. + /// + /// \param str The token string. + /// \throws std::invalid_argument Thrown if \a str is invalid. + /// + token_t:: + token_t(char const* str) + : str_{ + str + ? ( + validate_token(str) + ? str + : throw std::invalid_argument{"invalid token"} + ) + : throw std::invalid_argument{"invalid token"} + } + {} + + bool + token_t:: + empty() const + { + return str().empty(); + } + + std::string const& + token_t:: + str() const + { + return str_; + } + + bool + token_t:: + validate_token(std::string const& str) + { + // we allow empty tokens for the sake of simplicity. + // + if (str.empty()) + return true; + + for (auto const& c : str) { + if (!grammar::is_tchar(c)) + return false; + } + + return true; + } + + std::ostream& + operator<<(std::ostream& o, token_t const& t) + { + return o << t.str(); + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/token.hxx b/code/seafire/protocol/token.hxx new file mode 100644 index 0000000..bf24d8e --- /dev/null +++ b/code/seafire/protocol/token.hxx @@ -0,0 +1,170 @@ +#ifndef code__seafire__protocol__token_hxx_ +#define code__seafire__protocol__token_hxx_ + +#include + +#include +#include +#include +#include + +namespace code::seafire::protocol +{ + + class token_t + { + public: + token_t(); + + token_t(std::string); + + token_t(char const*); + + bool + empty() const; + + std::string const& + str() const; + + auto + operator<=>(token_t const&) const = default; + + static + bool + validate_token(std::string const&); + + private: + std::string str_; + + }; + + std::ostream& + operator<<(std::ostream&, token_t const&); + + /// Vector of tokens. + /// + using tokens_t = std::vector; + + /// Attempt to parse a token. + /// + template + std::optional + try_parse_token(Iterator& begin, Iterator end) + { + std::string token; + + while (begin != end && grammar::is_space(*begin)) { + ++begin; + } + + while (begin != end && grammar::is_tchar(*begin)) { + token += *begin++; + } + + if (token.empty()) { + return std::nullopt; + } + + return token; + } + + template + std::optional + try_parse_token(Iterator& begin, Iterator end, std::error_code& ec) + { + ec = {}; + + std::string token; + + while (begin != end && grammar::is_space(*begin)) { + ++begin; + } + + while (begin != end && grammar::is_tchar(*begin)) { + token += *begin++; + } + + if (token.empty()) { + return std::nullopt; + } + + return token; + } + + /// Attempt to parse tokens. + /// + template + std::optional + try_parse_tokens(Iterator& begin, Iterator end) + { + tokens_t tokens; + + while (begin != end) { + while (begin != end && grammar::is_space(*begin)) { + ++begin; + } + + auto token = try_parse_token(begin, end); + + if (!token) { + return std::nullopt; + } + + tokens.emplace_back(std::move(*token)); + + while (begin != end && grammar::is_space(*begin)) { + ++begin; + } + + if (begin == end || *begin != ',') { + break; + } + + ++begin; // skips ',' + } + + return tokens; + } + + /// Attempt to parse tokens. + /// + template + std::optional + try_parse_tokens(Iterator& begin, Iterator end, std::error_code& ec) + { + tokens_t tokens; + + while (begin != end) { + while (begin != end && grammar::is_space(*begin)) { + ++begin; + } + + auto token = try_parse_token(begin, end, ec); + + if (ec) { + return std::nullopt; + } + + if (!token) { + return std::nullopt; + } + + tokens.emplace_back(std::move(*token)); + + while (begin != end && grammar::is_space(*begin)) { + ++begin; + } + + if (begin == end || *begin != ',') { + break; + } + + ++begin; // skips ',' + } + + return tokens; + } + +} // namespace code::seafire::protocol + +#endif diff --git a/code/seafire/protocol/traits.hxx b/code/seafire/protocol/traits.hxx new file mode 100644 index 0000000..0a7c604 --- /dev/null +++ b/code/seafire/protocol/traits.hxx @@ -0,0 +1,70 @@ +#ifndef code__seafire__protocol__traits_hxx_ +#define code__seafire__protocol__traits_hxx_ + +#include + +namespace code::seafire::protocol::traits +{ + + /// Check if T has an alias type defined. + /// + template> + struct has_alias_type + : std::false_type + {}; + + template + struct has_alias_type< + T, + std::void_t< + decltype(std::declval()) + > + > : std::true_type + {}; + + /// Helper variable for has_alias_type. + /// + template + inline constexpr bool has_alias_type_v = has_alias_type::value; + + /// Access the alias type of T, if available, otherwise T. + /// + template> + struct alias_type + { + using type = T; + }; + + template + struct alias_type + { + using type = typename T::alias_type; + }; + + /// Helper for alias_type. + /// + template + using alias_type_t = typename alias_type::type; + + template> + struct has_overridden_to_string + : std::false_type + {}; + + template + struct has_overridden_to_string< + T, + std::void_t< + decltype(T::to_string(std::declval>())) + > + > : std::true_type + {}; + + template + inline constexpr bool has_overridden_to_string_v{ + has_overridden_to_string::value + }; + +} // seafire::protocol::traits + +#endif diff --git a/code/seafire/protocol/version.hxx.in b/code/seafire/protocol/version.hxx.in new file mode 100644 index 0000000..535d6e3 --- /dev/null +++ b/code/seafire/protocol/version.hxx.in @@ -0,0 +1,37 @@ +#ifndef code__seafire__protocol__version_hxx_ +#define code__seafire__protocol__version_hxx_ + +// The numeric version format is AAAAABBBBBCCCCCDDDE where: +// +// AAAAA - major version number +// BBBBB - minor version number +// CCCCC - bugfix version number +// DDD - alpha / beta (DDD + 500) version number +// E - final (0) / snapshot (1) +// +// When DDDE is not 0, 1 is subtracted from AAAAABBBBBCCCCC. For example: +// +// Version AAAAABBBBBCCCCCDDDE +// +// 0.1.0 0000000001000000000 +// 0.1.2 0000000001000020000 +// 1.2.3 0000100002000030000 +// 2.2.0-a.1 0000200001999990010 +// 3.0.0-b.2 0000299999999995020 +// 2.2.0-a.1.z 0000200001999990011 +// +#define LIBCODE_SEAFIRE_PROTOCOL_VERSION $libcode_seafire_protocol.version.project_number$ULL +#define LIBCODE_SEAFIRE_PROTOCOL_VERSION_STR "$libcode_seafire_protocol.version.project$" +#define LIBCODE_SEAFIRE_PROTOCOL_VERSION_ID "$libcode_seafire_protocol.version.project_id$" +#define LIBCODE_SEAFIRE_PROTOCOL_VERSION_FULL "$libcode_seafire_protocol.version$" + +#define LIBCODE_SEAFIRE_PROTOCOL_VERSION_MAJOR $libcode_seafire_protocol.version.major$ +#define LIBCODE_SEAFIRE_PROTOCOL_VERSION_MINOR $libcode_seafire_protocol.version.minor$ +#define LIBCODE_SEAFIRE_PROTOCOL_VERSION_PATCH $libcode_seafire_protocol.version.patch$ + +#define LIBCODE_SEAFIRE_PROTOCOL_PRE_RELEASE $libcode_seafire_protocol.version.pre_release$ + +#define LIBCODE_SEAFIRE_PROTOCOL_SNAPSHOT_SN $libcode_seafire_protocol.version.snapshot_sn$ULL +#define LIBCODE_SEAFIRE_PROTOCOL_SNAPSHOT_ID "$libcode_seafire_protocol.version.snapshot_id$" + +#endif diff --git a/code/seafire/protocol/write.cxx b/code/seafire/protocol/write.cxx new file mode 100644 index 0000000..f070af7 --- /dev/null +++ b/code/seafire/protocol/write.cxx @@ -0,0 +1,174 @@ +#include + +namespace code::seafire::protocol +{ + + void + write(common::io::stream_t& s, + request_t const& r) + { + common::io::const_buffers_t payload; + to_buffers(payload, r); + s.write(payload); + } + + void + write(common::io::stream_t& s, + request_t const& r, + std::error_code& ec) + { + common::io::const_buffers_t payload; + to_buffers(payload, r); + s.write(payload, ec); + } + + void + write(common::io::stream_t& s, + request_t const& r, + common::io::const_buffers_t const& content) + { + common::io::const_buffers_t payload; + to_buffers(payload, r); + + for (auto const& j : content) { + payload.emplace_back(j); + } + + s.write(payload); + } + + void + write(common::io::stream_t& s, + request_t const& r, + common::io::const_buffers_t const& content, + std::error_code& ec) + { + common::io::const_buffers_t payload; + to_buffers(payload, r); + + for (auto const& j : content) { + payload.emplace_back(j); + } + + s.write(payload, ec); + } + + void + async_write(common::io::stream_t& s, + request_t const& r, + std::function handler) + { + auto bound = [handler](std::error_code const& ec, std::size_t) + { + handler(ec); + }; + + s.async_write(to_buffers(r), bound); + } + + void + async_write(common::io::stream_t& s, + request_t const& r, + common::io::const_buffers_t const& content, + std::function handler) + { + auto bound = [handler](std::error_code const& ec, std::size_t) + { + handler(ec); + }; + + common::io::const_buffers_t payload; + to_buffers(payload, r); + + for (auto const& j : content) { + payload.emplace_back(j); + } + + s.async_write(payload, bound); + } + + void + write(common::io::stream_t& s, + response_t const& r) + { + common::io::const_buffers_t payload; + to_buffers(payload, r); + s.write(payload); + } + + void + write(common::io::stream_t& s, + response_t const& r, + std::error_code& ec) + { + common::io::const_buffers_t payload; + to_buffers(payload, r); + s.write(payload, ec); + } + + void + write(common::io::stream_t& s, + response_t const& r, + common::io::const_buffers_t const& content) + { + common::io::const_buffers_t payload; + to_buffers(payload, r); + + for (auto const& j : content) { + payload.emplace_back(j); + } + + s.write(payload); + } + + void + write(common::io::stream_t& s, + response_t const& r, + common::io::const_buffers_t const& content, + std::error_code& ec) + { + common::io::const_buffers_t payload; + to_buffers(payload, r); + + for (auto const& j : content) { + payload.emplace_back(j); + } + + s.write(payload, ec); + } + + void + async_write(common::io::stream_t& s, + response_t const& r, + std::function handler) + { + auto bound = [handler](std::error_code const& ec, std::size_t) + { + handler(ec); + }; + + s.async_write(to_buffers(r), bound); + } + + void + async_write(common::io::stream_t& s, + response_t const& r, + common::io::const_buffers_t const& content, + std::function handler) + { + auto bound = [handler](std::error_code const& ec, std::size_t) + { + handler(ec); + }; + + common::io::const_buffers_t payload; + to_buffers(payload, r); + + for (auto const& j : content) { + payload.emplace_back(j); + } + + s.async_write(payload, bound); + } + +} // namespace code::seafire::protocol diff --git a/code/seafire/protocol/write.hxx b/code/seafire/protocol/write.hxx new file mode 100644 index 0000000..6b23ddc --- /dev/null +++ b/code/seafire/protocol/write.hxx @@ -0,0 +1,82 @@ +#ifndef code__seafire__protocol__write_hxx_ +#define code__seafire__protocol__write_hxx_ + +#include +#include + +#include +#include + +#include + +#include +#include + +namespace code::seafire::protocol +{ + + void + write(common::io::stream_t&, + request_t const&); + + void + write(common::io::stream_t&, + request_t const&, + std::error_code&); + + void + write(common::io::stream_t&, + request_t const&, + std::vector const&); + + void + write(common::io::stream_t&, + request_t const&, + std::vector const&, + std::error_code&); + + void + async_write(common::io::stream_t&, + request_t const&, + std::function); + + void + async_write(common::io::stream_t&, + request_t const&, + std::vector const&, + std::function); + + void + write(common::io::stream_t&, + response_t const&); + + void + write(common::io::stream_t&, + response_t const&, + std::error_code&); + + void + write(common::io::stream_t&, + response_t const&, + std::vector const&); + + void + write(common::io::stream_t&, + response_t const&, + std::vector const&, + std::error_code&); + + void + async_write(common::io::stream_t&, + response_t const&, + std::function); + + void + async_write(common::io::stream_t&, + response_t const&, + std::vector const&, + std::function); + +} // namespace code::seafire::protocol + +#endif diff --git a/manifest b/manifest new file mode 100644 index 0000000..5ec94f6 --- /dev/null +++ b/manifest @@ -0,0 +1,14 @@ +: 1 +name: libcode-seafire-protocol +version: 0.1.0-a.0.z +language: c++ +summary: libcode-seafire-protocol C++ library +license: BSD-4-Clause +description-file: README.md +url: https://helloryan.se/code/ +email: ryan@helloryan.se +depends: * build2 >= 0.17.0 +depends: * bpkg >= 0.17.0 +depends: libasio ^1.29.0 +depends: libcode-uri ^0.1.0- +depends: libcode-seafire-common ^0.1.0- diff --git a/repositories.manifest b/repositories.manifest new file mode 100644 index 0000000..c09f998 --- /dev/null +++ b/repositories.manifest @@ -0,0 +1,15 @@ +: 1 +summary: libcode-seafire-protocol project repository + +: +role: prerequisite +location: https://pkg.cppget.org/1/beta +trust: 70:64:FE:E4:E0:F3:60:F1:B4:51:E1:FA:12:5C:E0:B3:DB:DF:96:33:39:B9:2E:E5:C2:68:63:4C:A6:47:39:43 + +: +role: prerequisite +location: https://code.helloryan.se/code/libcode-uri.git##HEAD + +: +role: prerequisite +location: https://code.helloryan.se/code/libcode-seafire-common.git##HEAD diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..662178d --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,8 @@ +# Test executables. +# +driver + +# Testscript output directories (can be symlinks). +# +test +test-* diff --git a/tests/build/.gitignore b/tests/build/.gitignore new file mode 100644 index 0000000..974e01d --- /dev/null +++ b/tests/build/.gitignore @@ -0,0 +1,4 @@ +/config.build +/root/ +/bootstrap/ +build/ diff --git a/tests/build/bootstrap.build b/tests/build/bootstrap.build new file mode 100644 index 0000000..a07b5ea --- /dev/null +++ b/tests/build/bootstrap.build @@ -0,0 +1,5 @@ +project = # Unnamed tests subproject. + +using config +using test +using dist diff --git a/tests/build/root.build b/tests/build/root.build new file mode 100644 index 0000000..a67b2fe --- /dev/null +++ b/tests/build/root.build @@ -0,0 +1,16 @@ +cxx.std = latest + +using cxx + +hxx{*}: extension = hxx +ixx{*}: extension = ixx +txx{*}: extension = txx +cxx{*}: extension = cxx + +# Every exe{} in this subproject is by default a test. +# +exe{*}: test = true + +# The test target for cross-testing (running tests under Wine, etc). +# +test.target = $cxx.target diff --git a/tests/buildfile b/tests/buildfile new file mode 100644 index 0000000..aeeab15 --- /dev/null +++ b/tests/buildfile @@ -0,0 +1 @@ +./: {*/ -build/}