Mercurial > irccd
view plugins/links/links.cpp @ 705:4b5dba257d81
Plugin links: brand new plugin, closes #872 @4h
author | David Demelier <markand@malikania.fr> |
---|---|
date | Fri, 06 Jul 2018 22:10:10 +0200 |
parents | |
children | bd7feaa002cb |
line wrap: on
line source
/* * links.cpp -- links plugin * * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr> * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include <memory> #include <regex> #include <sstream> #include <string> #include <variant> #include <boost/algorithm/string/trim_all.hpp> #include <boost/dll.hpp> #include <boost/asio.hpp> #include <boost/asio/ssl.hpp> #include <boost/beast.hpp> #include <irccd/string_util.hpp> #include <irccd/daemon/irc.hpp> #include <irccd/daemon/irccd.hpp> #include <irccd/daemon/plugin.hpp> #include <irccd/daemon/server.hpp> using boost::asio::async_connect; using boost::asio::deadline_timer; using boost::asio::io_context; using boost::asio::ip::tcp; using boost::asio::ssl::context; using boost::asio::ssl::stream; using boost::asio::ssl::stream_base; using boost::beast::flat_buffer; using boost::beast::http::async_read; using boost::beast::http::async_write; using boost::beast::http::empty_body; using boost::beast::http::field; using boost::beast::http::request; using boost::beast::http::response; using boost::beast::http::string_body; using boost::beast::http::verb; using boost::beast::http::status; using boost::posix_time::seconds; using boost::system::error_code; using std::get; using std::enable_shared_from_this; using std::monostate; using std::move; using std::regex; using std::regex_match; using std::regex_search; using std::shared_ptr; using std::smatch; using std::string; using std::variant; namespace irccd { namespace { // {{{ globals // User options. struct config { static inline unsigned timeout{30U}; }; // User formats. struct formats { static inline string info{"#{title}"}; }; // }}} // {{{ url struct url { string protocol; string host; string path{"/"}; static auto parse(const string&) -> url; }; auto url::parse(const string& link) -> url { static const regex regex("^(https?):\\/\\/([^\\/\\?]+)(.*)$"); url ret; if (smatch match; regex_match(link, match, regex)) { ret.protocol = match[1]; ret.host = match[2]; if (match.length(3) > 0) ret.path = match[3]; if (ret.path[0] != '/') ret.path.insert(ret.path.begin(), '/'); } return ret; } // }}} // {{{ requester class requester : public enable_shared_from_this<requester> { private: using socket = variant<monostate, tcp::socket, stream<tcp::socket>>; shared_ptr<server> server_; string channel_; string origin_; url url_; context ctx_{context::sslv23}; socket socket_; flat_buffer buffer_; request<empty_body> req_; response<string_body> res_; deadline_timer timer_; tcp::resolver resolver_; void notify(const string&); void parse(); void handle_read(const error_code&); void read(); void handle_write(const error_code&); void write(); void handle_handshake(const error_code&); void handshake(); void handle_connect(const error_code&); void connect(const tcp::resolver::results_type&); void handle_resolve(const error_code&, const tcp::resolver::results_type&); void resolve(); void handle_timer(const error_code&); void timer(); void start(); requester(io_context&, shared_ptr<server>, string, string, url); public: static void run(io_context&, const message_event&); }; void requester::notify(const string& title) { string_util::subst subst; subst.keywords.emplace("channel", channel_); subst.keywords.emplace("nickname", irc::user::parse(origin_).nick()); subst.keywords.emplace("origin", origin_); subst.keywords.emplace("server", server_->get_name()); subst.keywords.emplace("title", title); server_->message(channel_, format(formats::info, subst)); } void requester::parse() { /* * Use a regex because Boost's XML parser is strict and many web pages may * have invalid or broken tags. */ static const regex regex("<title>([^<]+)<\\/title>"); string data(res_.body().data()); smatch match; if (regex_search(data, match, regex)) notify(match[1]); } void requester::handle_read(const error_code& code) { timer_.cancel(); if (code) return; // Request again in case of relocation. if (res_.result() == status::moved_permanently) { const string host(res_[field::location].data()); // Clean '\r\n' url_ = url::parse(boost::algorithm::trim_all_copy(host)); start(); } else parse(); } void requester::read() { const auto self = shared_from_this(); const auto wrap = [self] (auto code, auto) { self->handle_read(code); }; timer(); switch (socket_.index()) { case 1: async_read(get<1>(socket_), buffer_, res_, wrap); break; case 2: async_read(get<2>(socket_), buffer_, res_, wrap); break; default: break; } } void requester::handle_write(const error_code& code) { timer_.cancel(); if (!code) read(); } void requester::write() { req_.version(11); req_.method(verb::get); req_.target(url_.path); req_.set(field::host, url_.host); req_.set(field::user_agent, BOOST_BEAST_VERSION_STRING); const auto self = shared_from_this(); const auto wrap = [self] (auto code, auto) { self->handle_write(code); }; timer(); switch (socket_.index()) { case 1: async_write(get<1>(socket_), req_, wrap); break; case 2: async_write(get<2>(socket_), req_, wrap); break; default: break; } } void requester::handle_handshake(const error_code& code) { timer_.cancel(); if (!code) write(); } void requester::handshake() { const auto self = shared_from_this(); timer(); switch (socket_.index()) { case 1: handle_handshake(error_code()); break; case 2: get<2>(socket_).async_handshake(stream_base::client, [self] (auto code) { self->handle_handshake(code); }); break; default: break; } } void requester::handle_connect(const error_code& code) { timer_.cancel(); if (!code) handshake(); } void requester::connect(const tcp::resolver::results_type& eps) { const auto self = shared_from_this(); const auto wrap = [self] (auto code, auto) { self->handle_connect(code); }; timer(); switch (socket_.index()) { case 1: async_connect(get<1>(socket_), eps, wrap); break; case 2: async_connect(get<2>(socket_).lowest_layer(), eps, wrap); break; default: break; } } void requester::handle_resolve(const error_code& code, const tcp::resolver::results_type& eps) { timer_.cancel(); if (!code) connect(eps); } void requester::resolve() { auto self = shared_from_this(); timer(); resolver_.async_resolve(url_.host, url_.protocol, [self] (auto code, auto eps) { self->handle_resolve(code, eps); }); } void requester::handle_timer(const error_code& code) { // Force close sockets to cancel all pending operations. if (code && code != boost::asio::error::operation_aborted) socket_.emplace<monostate>(); } void requester::timer() { const auto self = shared_from_this(); timer_.expires_from_now(seconds(config::timeout)); timer_.async_wait([self] (auto code) { self->handle_timer(code); }); } void requester::start() { if (url_.protocol == "http") socket_.emplace<tcp::socket>(resolver_.get_io_service()); else socket_.emplace<stream<tcp::socket>>(resolver_.get_io_service(), ctx_); resolve(); } requester::requester(io_context& io, shared_ptr<server> server, string channel, string origin, url url) : server_(move(server)) , channel_(move(channel)) , origin_(move(origin)) , url_(move(url)) , timer_(io) , resolver_(io) { } void requester::run(io_context& io, const message_event& ev) { auto url = url::parse(ev.message); if (url.protocol.empty() || url.host.empty()) return; shared_ptr<requester>(new requester(io, ev.server, ev.channel, ev.origin, move(url)))->start(); } // }}} // {{{ links_plugin class links_plugin : public plugin { public: using plugin::plugin; void set_config(plugin_config) override; void set_formats(plugin_formats) override; void handle_message(irccd&, const message_event&) override; }; void links_plugin::set_config(plugin_config conf) { if (const auto v = string_util::to_uint(conf["timeout"]); v) config::timeout = *v; } void links_plugin::set_formats(plugin_formats formats) { if (const auto it = formats.find("info"); it != formats.end()) formats::info = it->second; } void links_plugin::handle_message(irccd& irccd, const message_event& ev) { requester::run(irccd.get_service(), ev); } // }}} } // !namespace extern "C" BOOST_SYMBOL_EXPORT links_plugin irccd_plugin_links; links_plugin irccd_plugin_links("links", ""); } // !irccd