diff --git a/src/tawashi/CMakeLists.txt b/src/tawashi/CMakeLists.txt index 16564df..24bb117 100644 --- a/src/tawashi/CMakeLists.txt +++ b/src/tawashi/CMakeLists.txt @@ -32,6 +32,7 @@ add_library(${PROJECT_NAME} STATIC quick_submit_paste_response.cpp ip_utils.cpp mime_split.cpp + storage.cpp ) target_include_directories(${PROJECT_NAME} diff --git a/src/tawashi/pastie_response.cpp b/src/tawashi/pastie_response.cpp index 5daf7d5..39b6ac9 100644 --- a/src/tawashi/pastie_response.cpp +++ b/src/tawashi/pastie_response.cpp @@ -16,7 +16,7 @@ */ #include "pastie_response.hpp" -#include "incredis/incredis.hpp" +#include "storage.hpp" #include "settings_bag.hpp" #include "escapist.hpp" #include "cgi_env.hpp" @@ -75,13 +75,8 @@ namespace tawashi { } void PastieResponse::on_mustache_prepare (mstch::map& parContext) { - using opt_string = redis::IncRedis::opt_string; - using opt_string_list = redis::IncRedis::opt_string_list; - boost::string_ref token = cgi_env().request_uri_relative(); - auto& redis = this->redis(); - opt_string_list pastie_reply = redis.hmget(token, "pastie"); - opt_string pastie = (pastie_reply and not pastie_reply->empty() ? (*pastie_reply)[0] : opt_string()); + boost::optional pastie = this->storage().retrieve_pastie(token); if (not pastie) { m_pastie_not_found = true; diff --git a/src/tawashi/response.cpp b/src/tawashi/response.cpp index e1d4210..ce0333b 100644 --- a/src/tawashi/response.cpp +++ b/src/tawashi/response.cpp @@ -16,7 +16,6 @@ */ #include "response.hpp" -#include "incredis/incredis.hpp" #include "settings_bag.hpp" #include "tawashi_config.h" #include "duckhandy/stringize.h" @@ -62,23 +61,6 @@ namespace tawashi { } } - redis::IncRedis make_incredis (const tawashi::SettingsBag& parSettings) { - using redis::IncRedis; - - if (parSettings["redis_mode"] == "inet") { - return IncRedis( - parSettings.as("redis_server"), - parSettings.as("redis_port") - ); - } - else if (parSettings["redis_mode"] == "sock") { - return IncRedis(parSettings.as("redis_sock")); - } - else { - throw std::runtime_error("Unknown setting for \"redis_mode\", valid settings are \"inet\" or \"sock\""); - } - } - boost::optional load_whole_file (const std::string& parWebsiteRoot, const char* parSuffix, const boost::string_ref& parName, bool parThrow) { std::ostringstream oss; oss << parWebsiteRoot << parName << parSuffix; @@ -152,6 +134,7 @@ namespace tawashi { const Kakoune::SafePtr& parCgiEnv, bool parWantRedis ) : + m_storage(parSettings), //m_page_basename(fetch_page_basename(m_cgi_env)), m_cgi_env(parCgiEnv), m_settings(parSettings), @@ -163,8 +146,7 @@ namespace tawashi { assert(m_stream_out); if (parWantRedis) { - m_redis = std::make_unique(make_incredis(*parSettings)); - m_redis->connect(); + m_storage.connect_async(); } mstch::config::escape = &disable_mstch_escaping; @@ -200,14 +182,7 @@ namespace tawashi { {"languages", make_mstch_langmap(*m_settings)} }; - if (m_redis) { - SPDLOG_TRACE(statuslog, "Finalizing redis connection"); - m_redis->wait_for_connect(); - auto batch = m_redis->make_batch(); - batch.select(m_settings->as("redis_db")); - batch.client_setname("tawashi_v" STRINGIZE(VERSION_MAJOR) "." STRINGIZE(VERSION_MINOR) "." STRINGIZE(VERSION_PATCH)); - batch.throw_if_failed(); - } + m_storage.finalize_connection(); SPDLOG_TRACE(statuslog, "Raising event on_process"); HttpHeader http_header = this->on_process(); @@ -252,9 +227,9 @@ namespace tawashi { return *content; } - redis::IncRedis& Response::redis() const { - assert(m_redis); - return *m_redis; + const Storage& Response::storage() const { + assert(m_storage.is_connected()); + return m_storage; } const SettingsBag& Response::settings() const { diff --git a/src/tawashi/response.hpp b/src/tawashi/response.hpp index 7591543..7e16034 100644 --- a/src/tawashi/response.hpp +++ b/src/tawashi/response.hpp @@ -21,15 +21,12 @@ #include "kakoune/safe_ptr.hh" #include "http_header.hpp" #include "error_reasons.hpp" +#include "storage.hpp" #include #include #include #include -namespace redis { - class IncRedis; -} //namespace redis - namespace tawashi { class SettingsBag; @@ -54,7 +51,7 @@ namespace tawashi { 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 Storage& storage() const; const SettingsBag& settings() const; virtual std::string load_mustache() const; HttpHeader make_redirect (HttpStatusCodes parCode, const std::string& parLocation); @@ -65,11 +62,11 @@ namespace tawashi { virtual void on_mustache_prepare (mstch::map& parContext); virtual std::string on_mustache_retrieve(); + Storage m_storage; Kakoune::SafePtr m_cgi_env; Kakoune::SafePtr m_settings; std::string m_website_root; std::string m_base_uri; - std::unique_ptr m_redis; std::ostream* m_stream_out; }; } //namespace tawashi diff --git a/src/tawashi/storage.cpp b/src/tawashi/storage.cpp new file mode 100644 index 0000000..8c918fa --- /dev/null +++ b/src/tawashi/storage.cpp @@ -0,0 +1,131 @@ +/* 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 "storage.hpp" +#include "settings_bag.hpp" +#include "incredis/incredis.hpp" +#include "num_to_token.hpp" +#include "tawashi_config.h" +#include "duckhandy/stringize.h" +#include +#include +#include +#include +#include + +namespace tawashi { + namespace { + redis::IncRedis make_incredis (const SettingsBag& parSettings) { + using redis::IncRedis; + + if (parSettings["redis_mode"] == "inet") { + return IncRedis( + parSettings.as("redis_server"), + parSettings.as("redis_port") + ); + } + else if (parSettings["redis_mode"] == "sock") { + return IncRedis(parSettings.as("redis_sock")); + } + else { + throw std::runtime_error("Unknown setting for \"redis_mode\", valid settings are \"inet\" or \"sock\""); + } + } + + Storage::SubmissionResult make_submission_result (std::string&& parToken) { + return Storage::SubmissionResult { std::move(parToken), boost::optional() }; + } + + Storage::SubmissionResult make_submission_result (ErrorReasons parError) { + return Storage::SubmissionResult { std::string(), boost::make_optional(parError) }; + } + } //unnamed namespace + + Storage::Storage (const Kakoune::SafePtr& parSettings) : + m_redis(nullptr), + m_settings(parSettings) + { + } + + Storage::~Storage() = default; + + void Storage::connect_async() { + using redis::IncRedis; + + assert(not m_redis); + m_redis = std::make_unique(make_incredis(*m_settings)); + m_redis->connect(); + } + + bool Storage::is_connected() const { + return m_redis and m_redis->is_connected(); + } + + void Storage::finalize_connection() { + if (m_redis) { + SPDLOG_TRACE(spdlog::get("statuslog"), "Finalizing redis connection"); + m_redis->wait_for_connect(); + auto batch = m_redis->make_batch(); + batch.select(m_settings->as("redis_db")); + batch.client_setname("tawashi_v" STRINGIZE(VERSION_MAJOR) "." STRINGIZE(VERSION_MINOR) "." STRINGIZE(VERSION_PATCH)); + batch.throw_if_failed(); + } + } + + Storage::SubmissionResult Storage::submit_pastie ( + const boost::string_ref& parText, + uint32_t parExpiry, + const boost::string_ref& parLang, + const std::string& parRemoteIP + ) const { + if (not is_connected()) + return make_submission_result(ErrorReasons::RedisDisconnected); + + assert(m_redis); + auto& redis = *m_redis; + if (redis.get(parRemoteIP)) { + //please wait and submit again + return make_submission_result(ErrorReasons::UserFlooding); + } + + const auto next_id = redis.incr("paste_counter"); + std::string token = num_to_token(next_id); + assert(not token.empty()); + if (redis.hmset(token, + "pastie", parText, + "max_ttl", dhandy::lexical_cast(parExpiry), + "lang", parLang) + ) { + redis.set(parRemoteIP, ""); + redis.expire(parRemoteIP, m_settings->as("resubmit_wait")); + if (redis.expire(token, parExpiry)) + return make_submission_result(std::move(token)); + } + + return make_submission_result(ErrorReasons::PastieNotSaved); + } + + boost::optional Storage::retrieve_pastie (const boost::string_ref& parToken) const { + using opt_string = redis::IncRedis::opt_string; + using opt_string_list = redis::IncRedis::opt_string_list; + + opt_string_list pastie_reply = m_redis->hmget(parToken, "pastie"); + opt_string pastie = (pastie_reply and not pastie_reply->empty() ? (*pastie_reply)[0] : opt_string()); + + return pastie; + } +} //namespace tawashi diff --git a/src/tawashi/storage.hpp b/src/tawashi/storage.hpp new file mode 100644 index 0000000..9b02083 --- /dev/null +++ b/src/tawashi/storage.hpp @@ -0,0 +1,61 @@ +/* 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 "kakoune/safe_ptr.hh" +#include "error_reasons.hpp" +#include +#include +#include +#include +#include + +namespace redis { + class IncRedis; +} //namespace redis + +namespace tawashi { + class SettingsBag; + + class Storage { + public: + struct SubmissionResult { + std::string token; + boost::optional error; + }; + + explicit Storage (const Kakoune::SafePtr& parSettings); + ~Storage(); + + void connect_async(); + bool is_connected() const; + void finalize_connection(); + SubmissionResult submit_pastie ( + const boost::string_ref& parText, + uint32_t parExpiry, + const boost::string_ref& parLang, + const std::string& parRemoteIP + ) const; + + boost::optional retrieve_pastie (const boost::string_ref& parToken) const; + + private: + std::unique_ptr m_redis; + Kakoune::SafePtr m_settings; + }; +} //namespace tawashi diff --git a/src/tawashi/submit_paste_response.cpp b/src/tawashi/submit_paste_response.cpp index d4d2a89..c60c5f0 100644 --- a/src/tawashi/submit_paste_response.cpp +++ b/src/tawashi/submit_paste_response.cpp @@ -16,9 +16,8 @@ */ #include "submit_paste_response.hpp" -#include "incredis/incredis.hpp" +#include "storage.hpp" #include "cgi_post.hpp" -#include "num_to_token.hpp" #include "settings_bag.hpp" #include "duckhandy/compatibility.h" #include "duckhandy/lexical_cast.hpp" @@ -126,7 +125,7 @@ namespace tawashi { //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); - StringOrHeader submit_result = submit_to_redis(pastie, duration_int, lang); + StringOrHeader submit_result = submit_to_storage(pastie, duration_int, lang); const auto& token = submit_result.first; if (token) { @@ -144,37 +143,18 @@ namespace tawashi { } } - auto SubmitPasteResponse::submit_to_redis ( + auto SubmitPasteResponse::submit_to_storage ( const boost::string_ref& parText, uint32_t parExpiry, const boost::string_ref& parLang ) -> StringOrHeader { - auto& redis = this->redis(); - if (not redis.is_connected()) { - return std::make_pair(boost::optional(), make_error_redirect(ErrorReasons::RedisDisconnected)); - } - + auto& storage = this->storage(); std::string remote_ip = guess_real_remote_ip(cgi_env()); - if (redis.get(remote_ip)) { - //please wait and submit again - return std::make_pair(boost::optional(), make_error_redirect(ErrorReasons::UserFlooding)); - } - - const auto next_id = redis.incr("paste_counter"); - const std::string token = num_to_token(next_id); - assert(not token.empty()); - if (redis.hmset(token, - "pastie", parText, - "max_ttl", dhandy::lexical_cast(parExpiry), - "lang", parLang) - ) { - redis.set(remote_ip, ""); - redis.expire(remote_ip, settings().as("resubmit_wait")); - if (redis.expire(token, parExpiry)) - return std::make_pair(boost::make_optional(token), HttpHeader()); - } - - return std::make_pair(boost::optional(), make_error_redirect(ErrorReasons::PastieNotSaved)); + Storage::SubmissionResult submission_res = storage.submit_pastie(parText, parExpiry, parLang, remote_ip); + if (not submission_res.error) + return std::make_pair(boost::make_optional(std::move(submission_res.token)), HttpHeader()); + else + return std::make_pair(boost::optional(), make_error_redirect(*submission_res.error)); } HttpHeader SubmitPasteResponse::make_success_response (std::string&& parPastieParam) { diff --git a/src/tawashi/submit_paste_response.hpp b/src/tawashi/submit_paste_response.hpp index 0522fdf..1d9d431 100644 --- a/src/tawashi/submit_paste_response.hpp +++ b/src/tawashi/submit_paste_response.hpp @@ -41,6 +41,6 @@ namespace tawashi { 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); + StringOrHeader submit_to_storage (const boost::string_ref& parText, uint32_t parExpiry, const boost::string_ref& parLang); }; } //namespace tawashi