From cdd23d35d0d7a5d4ea0c61dc2c987ecdc5b436b0 Mon Sep 17 00:00:00 2001 From: King_DuckZ Date: Thu, 18 May 2017 18:50:01 +0100 Subject: [PATCH] Make on_process() return an HttpHeader. Response type is now decided by on_process' return value. --- src/tawashi_implem/CMakeLists.txt | 1 + src/tawashi_implem/cgi_environment_vars.hpp | 2 +- src/tawashi_implem/http_header.cpp | 153 +++++++++++++++++++ src/tawashi_implem/http_header.hpp | 72 +++++++++ src/tawashi_implem/pastie_response.cpp | 5 +- src/tawashi_implem/pastie_response.hpp | 2 +- src/tawashi_implem/response.cpp | 49 +++--- src/tawashi_implem/response.hpp | 15 +- src/tawashi_implem/submit_paste_response.cpp | 48 +++--- src/tawashi_implem/submit_paste_response.hpp | 8 +- 10 files changed, 280 insertions(+), 75 deletions(-) create mode 100644 src/tawashi_implem/http_header.cpp create mode 100644 src/tawashi_implem/http_header.hpp diff --git a/src/tawashi_implem/CMakeLists.txt b/src/tawashi_implem/CMakeLists.txt index 8b73ebf..6c89619 100644 --- a/src/tawashi_implem/CMakeLists.txt +++ b/src/tawashi_implem/CMakeLists.txt @@ -26,6 +26,7 @@ add_library(${PROJECT_NAME} STATIC tiger.c error_response.cpp tawashi_exception.cpp + http_header.cpp ) target_include_directories(${PROJECT_NAME} diff --git a/src/tawashi_implem/cgi_environment_vars.hpp b/src/tawashi_implem/cgi_environment_vars.hpp index 2731e83..6ee4124 100644 --- a/src/tawashi_implem/cgi_environment_vars.hpp +++ b/src/tawashi_implem/cgi_environment_vars.hpp @@ -22,7 +22,7 @@ #include "enum.h" namespace tawashi { - BETTER_ENUM(CGIVars, std::size_t, + SLOW_ENUM(CGIVars, std::size_t, AUTH_TYPE = 0, CONTENT_LENGTH, CONTENT_TYPE, diff --git a/src/tawashi_implem/http_header.cpp b/src/tawashi_implem/http_header.cpp new file mode 100644 index 0000000..9037900 --- /dev/null +++ b/src/tawashi_implem/http_header.cpp @@ -0,0 +1,153 @@ +/* Copyright 2017, Michele Santullo + * This file is part of "tawashi". + * + * "tawashi" is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * "tawashi" is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with "tawashi". If not, see . + */ + +#include "http_header.hpp" +#include "duckhandy/lexical_cast.hpp" +#include "duckhandy/sequence_bt.hpp" +#include "sprout/array/array.hpp" +#include +#include + +namespace tawashi { + namespace { + constexpr const char* get_status_code_desc (HttpStatusCodes parCode) { + switch (parCode) { + case HttpStatusCodes::Code301_MovedPermanently: return "Moved Permanently"; + case HttpStatusCodes::Code302_Found: return "Found"; + case HttpStatusCodes::Code303_SeeOther: return "See Other"; + case HttpStatusCodes::Code400_BadRequest: return "Bad Request"; + case HttpStatusCodes::Code403_Forbidden: return "Forbidden"; + case HttpStatusCodes::Code404_NotFound: return "Not Found"; + case HttpStatusCodes::Code413_PayloadTooLarge: return "Payload Too Large"; + case HttpStatusCodes::Code429_TooManyRequests: return "Too Many Requests"; + case HttpStatusCodes::Code431_RequestHeaderFieldsTooLarge: return "Request Header Fields Too Large"; + case HttpStatusCodes::Code500_InternalServerError: return "Internal Server Error"; + case HttpStatusCodes::Code501_NotImplemented: return "Not Implemented"; + case HttpStatusCodes::Code503_ServiceUnavailable: return "Service Unavailable"; + } + return "INVALID STATUS CODE"; + } + + inline constexpr uint16_t single_status_code_to_code (const char* parCode) { + std::size_t idx = 0; + uint16_t ret_num = 0; + uint16_t digits = 0; + char cur_char = 0; + while ((cur_char = parCode[idx++]) and digits < 3) { + if (cur_char >= '0' and cur_char <= '9') { + uint16_t multip = 1; + for (uint16_t z = 1; z < (3 - digits); ++z) { + multip *= 10; + } + ret_num += multip * (cur_char - '0'); + ++digits; + } + } + return ret_num; + } + + template + inline constexpr sprout::array make_status_codes_lookup (dhandy::bt::number_seq) { + return sprout::array { + single_status_code_to_code(HttpStatusCodes::_names()[Values])... + }; + } + + uint16_t status_code_name_to_num (HttpStatusCodes parCode) { + constexpr const auto status_codes = make_status_codes_lookup(dhandy::bt::number_range()); + return status_codes[parCode._to_integral()]; + } + + constexpr auto g_status_code_descriptions = ::better_enums::make_map(get_status_code_desc); + } //unnamed namespace + + HttpHeader::HttpHeader() : + m_param("text/html"), + m_status_code(HttpStatusCodes::CodeNone), + m_header_type(ContentType) + { + } + + HttpHeader::HttpHeader (Types parType, HttpStatusCodes parCode, std::string&& parParam) : + m_param(std::move(parParam)), + m_status_code(parCode), + m_header_type(parType) + { + } + + void HttpHeader::set_status (HttpStatusCodes parCode) { + m_status_code = parCode; + } + + void HttpHeader::unset_status() { + m_status_code = HttpStatusCodes::CodeNone; + } + + void HttpHeader::set_type (Types parType, std::string&& parParameter) { + m_header_type = parType; + m_param = std::move(parParameter); + } + + std::ostream& operator<< (std::ostream& parStream, const HttpHeader& parHeader) { + const HttpStatusCodes code_none = HttpStatusCodes::CodeNone; + if (parHeader.status_code() != code_none) { + parStream << + "Status: " << + status_code_name_to_num(parHeader.status_code()) << + g_status_code_descriptions[parHeader.status_code()] << + '\n' + ; + } + switch (parHeader.type()) { + case HttpHeader::ContentType: + SPDLOG_TRACE(spdlog::get("statuslog"), "Response is a Content-type (data)"); + parStream << "Content-type: " << parHeader.parameter() << '\n'; + break; + case HttpHeader::Location: + SPDLOG_TRACE(spdlog::get("statuslog"), "Response is a Location (redirect)"); + parStream << "Location: " << parHeader.parameter() << '\n'; + break; + } + parStream << '\n'; + return parStream; + } + + HttpHeader make_header_type_html() { + return HttpHeader(HttpHeader::ContentType, HttpStatusCodes::CodeNone, "text/html"); + } + + HttpHeader make_header_type_text_utf8() { + return HttpHeader(HttpHeader::ContentType, HttpStatusCodes::CodeNone, "text/plain; charset=utf-8"); + } + + HttpStatusCodes int_to_status_code (uint16_t parCode) { + constexpr const auto status_codes = make_status_codes_lookup(dhandy::bt::number_range()); + auto it_found_code = std::find(status_codes.begin(), status_codes.end(), parCode); + if (it_found_code != status_codes.end()) { + const auto index = it_found_code - status_codes.begin(); + assert(index < HttpStatusCodes::_size() - 1); + return HttpStatusCodes::_from_integral(index); + } + else { + return HttpStatusCodes::CodeNone; + } + } + + bool HttpHeader::body_required() const { + return type() == ContentType; + } +} //namespace tawashi diff --git a/src/tawashi_implem/http_header.hpp b/src/tawashi_implem/http_header.hpp new file mode 100644 index 0000000..371fd18 --- /dev/null +++ b/src/tawashi_implem/http_header.hpp @@ -0,0 +1,72 @@ +/* Copyright 2017, Michele Santullo + * This file is part of "tawashi". + * + * "tawashi" is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * "tawashi" is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with "tawashi". If not, see . + */ + +#pragma once + +#include "enum.h" +#include +#include +#include + +namespace tawashi { + SLOW_ENUM(HttpStatusCodes, uint16_t, + Code301_MovedPermanently, + Code302_Found, + Code303_SeeOther, + Code400_BadRequest, + Code403_Forbidden, + Code404_NotFound, + Code413_PayloadTooLarge, + Code429_TooManyRequests, + Code431_RequestHeaderFieldsTooLarge, + Code500_InternalServerError, + Code501_NotImplemented, + Code503_ServiceUnavailable, + CodeNone + ) + class HttpHeader { + public: + enum Types : uint8_t { + ContentType, + Location, + Status + }; + + HttpHeader(); + HttpHeader (Types parType, HttpStatusCodes parCode, std::string&& parParam); + ~HttpHeader() noexcept = default; + + Types type() const { return m_header_type; } + HttpStatusCodes status_code() const { return m_status_code; } + const std::string& parameter() const { return m_param; } + bool body_required() const; + + void set_status (HttpStatusCodes parCode); + void unset_status(); + void set_type (Types parType, std::string&& parParameter); + + private: + std::string m_param; + HttpStatusCodes m_status_code; + Types m_header_type; + }; + + std::ostream& operator<< (std::ostream& parStream, const HttpHeader& parHeader); + HttpHeader make_header_type_html(); + HttpHeader make_header_type_text_utf8(); + HttpStatusCodes int_to_status_code (uint16_t parCode); +} //namespace tawashi diff --git a/src/tawashi_implem/pastie_response.cpp b/src/tawashi_implem/pastie_response.cpp index 0daa6aa..f0ed244 100644 --- a/src/tawashi_implem/pastie_response.cpp +++ b/src/tawashi_implem/pastie_response.cpp @@ -42,11 +42,11 @@ namespace tawashi { { } - void PastieResponse::on_process() { + HttpHeader PastieResponse::on_process() { auto get = cgi_env().query_string_split(); const std::string& query_str(cgi_env().query_string()); if (get["m"] == "plain" or query_str.empty()) { - this->change_type(Response::ContentType, "text/plain; charset=utf-8"); + return make_header_type_text_utf8(); m_plain_text = true; } else if (query_str == g_nolang_token) { @@ -61,6 +61,7 @@ namespace tawashi { if (m_lang_file.empty()) m_lang_file = "default.lang"; } + return make_header_type_html(); } void PastieResponse::on_mustache_prepare (mstch::map& parContext) { diff --git a/src/tawashi_implem/pastie_response.hpp b/src/tawashi_implem/pastie_response.hpp index 30621cc..5ca3c2a 100644 --- a/src/tawashi_implem/pastie_response.hpp +++ b/src/tawashi_implem/pastie_response.hpp @@ -34,7 +34,7 @@ namespace tawashi { virtual boost::string_ref page_basename() const override { return boost::string_ref("pastie"); } private: - virtual void on_process() override; + virtual HttpHeader on_process() override; virtual void on_mustache_prepare (mstch::map& parContext) override; virtual std::string on_mustache_retrieve() override; diff --git a/src/tawashi_implem/response.cpp b/src/tawashi_implem/response.cpp index ec8b963..e39101c 100644 --- a/src/tawashi_implem/response.cpp +++ b/src/tawashi_implem/response.cpp @@ -124,13 +124,11 @@ namespace tawashi { const Kakoune::SafePtr& parCgiEnv, bool parWantRedis ) : - m_resp_value(g_def_response_type), //m_page_basename(fetch_page_basename(m_cgi_env)), m_cgi_env(parCgiEnv), m_settings(parSettings), m_website_root(make_root_path(*parSettings)), m_base_uri(make_base_uri(m_settings->at("base_uri"), m_cgi_env->https())), - m_resp_type(ContentType), m_stream_out(parStreamOut), m_header_sent(false) { @@ -155,7 +153,8 @@ namespace tawashi { Response::~Response() noexcept = default; - void Response::on_process() { + HttpHeader Response::on_process() { + return HttpHeader(); } void Response::on_mustache_prepare (mstch::map&) { @@ -183,26 +182,13 @@ namespace tawashi { } SPDLOG_TRACE(statuslog, "Raising event on_process"); - this->on_process(); + HttpHeader http_header = this->on_process(); + *m_stream_out << http_header; + SPDLOG_TRACE(statuslog, "Raising event on_mustache_prepare"); this->on_mustache_prepare(mustache_context); - m_header_sent = true; - bool render_page = true; - switch (m_resp_type) { - case ContentType: - SPDLOG_TRACE(statuslog, "Response is a Content-type (data)"); - *m_stream_out << "Content-type: " << m_resp_value << "\n\n"; - break; - case Location: - SPDLOG_TRACE(statuslog, "Response is a Location (redirect)"); - *m_stream_out << "Status: 303 See Other" << "\n"; - *m_stream_out << "Location: " << m_resp_value << "\n\n"; - render_page = false; - break; - } - - if (render_page) { + if (http_header.body_required()) { SPDLOG_TRACE(statuslog, "Rendering in mustache"); *m_stream_out << mstch::render( on_mustache_retrieve(), @@ -229,13 +215,6 @@ namespace tawashi { return *m_cgi_env; } - void Response::change_type (Types parRespType, std::string&& parValue) { - assert(not m_header_sent); - assert(not parValue.empty()); - m_resp_type = parRespType; - m_resp_value = std::move(parValue); - } - const std::string& Response::base_uri() const { return m_base_uri; } @@ -254,4 +233,20 @@ namespace tawashi { assert(m_settings); return *m_settings; } + + HttpHeader Response::make_redirect (HttpStatusCodes parCode, const std::string& parLocation) { + std::ostringstream oss; + oss << base_uri() << '/' << parLocation; + return HttpHeader(HttpHeader::Location, parCode, oss.str()); + } + + HttpHeader Response::make_error_redirect (uint16_t parCode, ErrorReasons parReason) { + auto statuslog = spdlog::get("statuslog"); + assert(statuslog); + statuslog->info("Redirecting to error page, code={} reason={}", parCode, parReason); + + std::ostringstream oss; + oss << "error.cgi?code=" << parCode << "&reason=" << parReason._to_integral(); + return make_redirect(int_to_status_code(parCode), oss.str()); + } } //namespace tawashi diff --git a/src/tawashi_implem/response.hpp b/src/tawashi_implem/response.hpp index 59c952c..faf2ffe 100644 --- a/src/tawashi_implem/response.hpp +++ b/src/tawashi_implem/response.hpp @@ -19,6 +19,8 @@ #include "mstch/mstch.hpp" #include "kakoune/safe_ptr.hh" +#include "http_header.hpp" +#include "error_reasons.hpp" #include #include #include @@ -42,11 +44,6 @@ namespace tawashi { void send(); protected: - enum Types { - ContentType, - Location - }; - Response ( const Kakoune::SafePtr& parSettings, std::ostream* parStreamOut, @@ -54,26 +51,24 @@ namespace tawashi { bool parWantRedis ); - void change_type (Types parRespType, std::string&& parValue); - const cgi::Env& cgi_env() const; const std::string& base_uri() const; virtual boost::string_ref page_basename() const = 0; redis::IncRedis& redis() const; const SettingsBag& settings() const; virtual std::string load_mustache() const; + HttpHeader make_redirect (HttpStatusCodes parCode, const std::string& parLocation); + HttpHeader make_error_redirect (uint16_t parCode, ErrorReasons parReason); private: - virtual void on_process(); + virtual HttpHeader on_process(); virtual void on_mustache_prepare (mstch::map& parContext); virtual std::string on_mustache_retrieve(); - std::string m_resp_value; Kakoune::SafePtr m_cgi_env; Kakoune::SafePtr m_settings; std::string m_website_root; std::string m_base_uri; - Types m_resp_type; std::unique_ptr m_redis; std::ostream* m_stream_out; bool m_header_sent; diff --git a/src/tawashi_implem/submit_paste_response.cpp b/src/tawashi_implem/submit_paste_response.cpp index cd6d434..7e2e8fe 100644 --- a/src/tawashi_implem/submit_paste_response.cpp +++ b/src/tawashi_implem/submit_paste_response.cpp @@ -97,10 +97,9 @@ namespace tawashi { ) : Response(parSettings, parStreamOut, parCgiEnv, true) { - this->change_type(Response::ContentType, "text/plain"); } - void SubmitPasteResponse::on_process() { + HttpHeader SubmitPasteResponse::on_process() { auto post = cgi::read_post(std::cin, cgi_env()); boost::string_ref pastie; boost::string_ref lang; @@ -114,8 +113,7 @@ namespace tawashi { } catch (const TawashiException& e) { statuslog->error(e.what()); - error_redirect(500, e.reason()); - return; + return make_error_redirect(500, e.reason()); } try { lang = get_value_from_post(post, make_string_ref(g_language_key)); @@ -128,50 +126,51 @@ namespace tawashi { const SettingsBag& settings = this->settings(); const auto max_sz = settings.as("max_pastie_size"); if (pastie.size() < settings.as("min_pastie_size")) { - error_redirect(431, ErrorReasons::PostLengthNotInRange); - return; + return make_error_redirect(431, ErrorReasons::PostLengthNotInRange); } if (max_sz and pastie.size() > max_sz) { if (settings.as("truncate_long_pasties")) { pastie = pastie.substr(0, max_sz); } else { - error_redirect(431, ErrorReasons::PostLengthNotInRange); - return; + return make_error_redirect(431, ErrorReasons::PostLengthNotInRange); } } //TODO: replace boost's lexical_cast with mine when I have some checks //over invalid inputs const uint32_t duration_int = std::max(std::min((duration.empty() ? 86400U : boost::lexical_cast(duration)), 2628000U), 1U); - boost::optional token = submit_to_redis(pastie, duration_int, lang); + StringOrHeader submit_result = submit_to_redis(pastie, duration_int, lang); + const auto& token = submit_result.first; if (token) { std::ostringstream oss; - oss << base_uri() << '/' << *token; + oss << *token; statuslog->info("Pastie token=\"{}\" redirect=\"{}\"", *token, oss.str()); if (not lang.empty()) oss << '?' << lang; - this->change_type(Response::Location, oss.str()); + return this->make_redirect(HttpStatusCodes::CodeNone, oss.str()); } else { statuslog->info("Empty pastie token (possibly due to a previous failure)"); - return; + return submit_result.second; } } - boost::optional SubmitPasteResponse::submit_to_redis (const boost::string_ref& parText, uint32_t parExpiry, const boost::string_ref& parLang) { + auto SubmitPasteResponse::submit_to_redis ( + const boost::string_ref& parText, + uint32_t parExpiry, + const boost::string_ref& parLang + ) -> StringOrHeader { auto& redis = this->redis(); if (not redis.is_connected()) { - error_redirect(503, ErrorReasons::RedisDisconnected); - return boost::optional(); + return std::make_pair(boost::optional(), make_error_redirect(503, ErrorReasons::RedisDisconnected)); } std::string ip_hash = hashed_ip(cgi_env().remote_addr()); if (redis.get(ip_hash)) { //please wait and submit again - error_redirect(429, ErrorReasons::UserFlooding); - return boost::optional(); + return std::make_pair(boost::optional(), make_error_redirect(429, ErrorReasons::UserFlooding)); } const auto next_id = redis.incr("paste_counter"); @@ -185,20 +184,9 @@ namespace tawashi { redis.set(ip_hash, ""); redis.expire(ip_hash, settings().as("resubmit_wait")); if (redis.expire(token, parExpiry)) - return boost::make_optional(token); + return std::make_pair(boost::make_optional(token), HttpHeader()); } - error_redirect(500, ErrorReasons::PastieNotSaved); - return boost::optional(); - } - - void SubmitPasteResponse::error_redirect (int parCode, ErrorReasons parReason) { - auto statuslog = spdlog::get("statuslog"); - assert(statuslog); - statuslog->info("Redirecting to error page, code={} reason={}", parCode, parReason); - - std::ostringstream oss; - oss << base_uri() << "/error.cgi?code=" << parCode << "&reason=" << parReason._to_integral(); - this->change_type(Response::Location, oss.str()); + return std::make_pair(boost::optional(), make_error_redirect(500, ErrorReasons::PastieNotSaved)); } } //namespace tawashi diff --git a/src/tawashi_implem/submit_paste_response.hpp b/src/tawashi_implem/submit_paste_response.hpp index bcfb4f1..5980c9c 100644 --- a/src/tawashi_implem/submit_paste_response.hpp +++ b/src/tawashi_implem/submit_paste_response.hpp @@ -18,11 +18,11 @@ #pragma once #include "response.hpp" -#include "error_reasons.hpp" #include #include #include #include +#include namespace tawashi { class SubmitPasteResponse : public Response { @@ -37,8 +37,8 @@ namespace tawashi { virtual boost::string_ref page_basename() const override { assert(false); return boost::string_ref(""); } private: - virtual void on_process() override; - boost::optional submit_to_redis (const boost::string_ref& parText, uint32_t parExpiry, const boost::string_ref& parLang); - void error_redirect (int parCode, ErrorReasons parReason); + typedef std::pair, HttpHeader> StringOrHeader; + virtual HttpHeader on_process() override; + StringOrHeader submit_to_redis (const boost::string_ref& parText, uint32_t parExpiry, const boost::string_ref& parLang); }; } //namespace tawashi