comparison 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
comparison
equal deleted inserted replaced
704:13381b9b9215 705:4b5dba257d81
1 /*
2 * links.cpp -- links plugin
3 *
4 * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
5 *
6 * Permission to use, copy, modify, and/or distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18
19 #include <memory>
20 #include <regex>
21 #include <sstream>
22 #include <string>
23 #include <variant>
24
25 #include <boost/algorithm/string/trim_all.hpp>
26
27 #include <boost/dll.hpp>
28
29 #include <boost/asio.hpp>
30 #include <boost/asio/ssl.hpp>
31
32 #include <boost/beast.hpp>
33
34 #include <irccd/string_util.hpp>
35
36 #include <irccd/daemon/irc.hpp>
37 #include <irccd/daemon/irccd.hpp>
38 #include <irccd/daemon/plugin.hpp>
39 #include <irccd/daemon/server.hpp>
40
41 using boost::asio::async_connect;
42 using boost::asio::deadline_timer;
43 using boost::asio::io_context;
44 using boost::asio::ip::tcp;
45 using boost::asio::ssl::context;
46 using boost::asio::ssl::stream;
47 using boost::asio::ssl::stream_base;
48
49 using boost::beast::flat_buffer;
50 using boost::beast::http::async_read;
51 using boost::beast::http::async_write;
52 using boost::beast::http::empty_body;
53 using boost::beast::http::field;
54 using boost::beast::http::request;
55 using boost::beast::http::response;
56 using boost::beast::http::string_body;
57 using boost::beast::http::verb;
58 using boost::beast::http::status;
59
60 using boost::posix_time::seconds;
61 using boost::system::error_code;
62
63 using std::get;
64 using std::enable_shared_from_this;
65 using std::monostate;
66 using std::move;
67 using std::regex;
68 using std::regex_match;
69 using std::regex_search;
70 using std::shared_ptr;
71 using std::smatch;
72 using std::string;
73 using std::variant;
74
75 namespace irccd {
76
77 namespace {
78
79 // {{{ globals
80
81 // User options.
82 struct config {
83 static inline unsigned timeout{30U};
84 };
85
86 // User formats.
87 struct formats {
88 static inline string info{"#{title}"};
89 };
90
91 // }}}
92
93 // {{{ url
94
95 struct url {
96 string protocol;
97 string host;
98 string path{"/"};
99
100 static auto parse(const string&) -> url;
101 };
102
103 auto url::parse(const string& link) -> url
104 {
105 static const regex regex("^(https?):\\/\\/([^\\/\\?]+)(.*)$");
106
107 url ret;
108
109 if (smatch match; regex_match(link, match, regex)) {
110 ret.protocol = match[1];
111 ret.host = match[2];
112
113 if (match.length(3) > 0)
114 ret.path = match[3];
115 if (ret.path[0] != '/')
116 ret.path.insert(ret.path.begin(), '/');
117 }
118
119 return ret;
120 }
121
122 // }}}
123
124 // {{{ requester
125
126 class requester : public enable_shared_from_this<requester> {
127 private:
128 using socket = variant<monostate, tcp::socket, stream<tcp::socket>>;
129
130 shared_ptr<server> server_;
131 string channel_;
132 string origin_;
133
134 url url_;
135 context ctx_{context::sslv23};
136 socket socket_;
137 flat_buffer buffer_;
138 request<empty_body> req_;
139 response<string_body> res_;
140 deadline_timer timer_;
141 tcp::resolver resolver_;
142
143 void notify(const string&);
144 void parse();
145 void handle_read(const error_code&);
146 void read();
147 void handle_write(const error_code&);
148 void write();
149 void handle_handshake(const error_code&);
150 void handshake();
151 void handle_connect(const error_code&);
152 void connect(const tcp::resolver::results_type&);
153 void handle_resolve(const error_code&, const tcp::resolver::results_type&);
154 void resolve();
155 void handle_timer(const error_code&);
156 void timer();
157 void start();
158
159 requester(io_context&, shared_ptr<server>, string, string, url);
160
161 public:
162 static void run(io_context&, const message_event&);
163 };
164
165 void requester::notify(const string& title)
166 {
167 string_util::subst subst;
168
169 subst.keywords.emplace("channel", channel_);
170 subst.keywords.emplace("nickname", irc::user::parse(origin_).nick());
171 subst.keywords.emplace("origin", origin_);
172 subst.keywords.emplace("server", server_->get_name());
173 subst.keywords.emplace("title", title);
174
175 server_->message(channel_, format(formats::info, subst));
176 }
177
178 void requester::parse()
179 {
180 /*
181 * Use a regex because Boost's XML parser is strict and many web pages may
182 * have invalid or broken tags.
183 */
184 static const regex regex("<title>([^<]+)<\\/title>");
185
186 string data(res_.body().data());
187 smatch match;
188
189 if (regex_search(data, match, regex))
190 notify(match[1]);
191 }
192
193 void requester::handle_read(const error_code& code)
194 {
195 timer_.cancel();
196
197 if (code)
198 return;
199
200 // Request again in case of relocation.
201 if (res_.result() == status::moved_permanently) {
202 const string host(res_[field::location].data());
203
204 // Clean '\r\n'
205 url_ = url::parse(boost::algorithm::trim_all_copy(host));
206 start();
207 } else
208 parse();
209 }
210
211 void requester::read()
212 {
213 const auto self = shared_from_this();
214 const auto wrap = [self] (auto code, auto) {
215 self->handle_read(code);
216 };
217
218 timer();
219
220 switch (socket_.index()) {
221 case 1:
222 async_read(get<1>(socket_), buffer_, res_, wrap);
223 break;
224 case 2:
225 async_read(get<2>(socket_), buffer_, res_, wrap);
226 break;
227 default:
228 break;
229 }
230 }
231
232 void requester::handle_write(const error_code& code)
233 {
234 timer_.cancel();
235
236 if (!code)
237 read();
238 }
239
240 void requester::write()
241 {
242 req_.version(11);
243 req_.method(verb::get);
244 req_.target(url_.path);
245 req_.set(field::host, url_.host);
246 req_.set(field::user_agent, BOOST_BEAST_VERSION_STRING);
247
248 const auto self = shared_from_this();
249 const auto wrap = [self] (auto code, auto) {
250 self->handle_write(code);
251 };
252
253 timer();
254
255 switch (socket_.index()) {
256 case 1:
257 async_write(get<1>(socket_), req_, wrap);
258 break;
259 case 2:
260 async_write(get<2>(socket_), req_, wrap);
261 break;
262 default:
263 break;
264 }
265 }
266
267 void requester::handle_handshake(const error_code& code)
268 {
269 timer_.cancel();
270
271 if (!code)
272 write();
273 }
274
275 void requester::handshake()
276 {
277 const auto self = shared_from_this();
278
279 timer();
280
281 switch (socket_.index()) {
282 case 1:
283 handle_handshake(error_code());
284 break;
285 case 2:
286 get<2>(socket_).async_handshake(stream_base::client, [self] (auto code) {
287 self->handle_handshake(code);
288 });
289 break;
290 default:
291 break;
292 }
293 }
294
295 void requester::handle_connect(const error_code& code)
296 {
297 timer_.cancel();
298
299 if (!code)
300 handshake();
301 }
302
303 void requester::connect(const tcp::resolver::results_type& eps)
304 {
305 const auto self = shared_from_this();
306 const auto wrap = [self] (auto code, auto) {
307 self->handle_connect(code);
308 };
309
310 timer();
311
312 switch (socket_.index()) {
313 case 1:
314 async_connect(get<1>(socket_), eps, wrap);
315 break;
316 case 2:
317 async_connect(get<2>(socket_).lowest_layer(), eps, wrap);
318 break;
319 default:
320 break;
321 }
322 }
323
324 void requester::handle_resolve(const error_code& code, const tcp::resolver::results_type& eps)
325 {
326 timer_.cancel();
327
328 if (!code)
329 connect(eps);
330 }
331
332 void requester::resolve()
333 {
334 auto self = shared_from_this();
335
336 timer();
337 resolver_.async_resolve(url_.host, url_.protocol, [self] (auto code, auto eps) {
338 self->handle_resolve(code, eps);
339 });
340 }
341
342 void requester::handle_timer(const error_code& code)
343 {
344 // Force close sockets to cancel all pending operations.
345 if (code && code != boost::asio::error::operation_aborted)
346 socket_.emplace<monostate>();
347 }
348
349 void requester::timer()
350 {
351 const auto self = shared_from_this();
352
353 timer_.expires_from_now(seconds(config::timeout));
354 timer_.async_wait([self] (auto code) {
355 self->handle_timer(code);
356 });
357 }
358
359 void requester::start()
360 {
361 if (url_.protocol == "http")
362 socket_.emplace<tcp::socket>(resolver_.get_io_service());
363 else
364 socket_.emplace<stream<tcp::socket>>(resolver_.get_io_service(), ctx_);
365
366 resolve();
367 }
368
369 requester::requester(io_context& io,
370 shared_ptr<server> server,
371 string channel,
372 string origin,
373 url url)
374 : server_(move(server))
375 , channel_(move(channel))
376 , origin_(move(origin))
377 , url_(move(url))
378 , timer_(io)
379 , resolver_(io)
380 {
381 }
382
383 void requester::run(io_context& io, const message_event& ev)
384 {
385 auto url = url::parse(ev.message);
386
387 if (url.protocol.empty() || url.host.empty())
388 return;
389
390 shared_ptr<requester>(new requester(io, ev.server, ev.channel, ev.origin, move(url)))->start();
391 }
392
393 // }}}
394
395 // {{{ links_plugin
396
397 class links_plugin : public plugin {
398 public:
399 using plugin::plugin;
400
401 void set_config(plugin_config) override;
402 void set_formats(plugin_formats) override;
403 void handle_message(irccd&, const message_event&) override;
404 };
405
406 void links_plugin::set_config(plugin_config conf)
407 {
408 if (const auto v = string_util::to_uint(conf["timeout"]); v)
409 config::timeout = *v;
410 }
411
412 void links_plugin::set_formats(plugin_formats formats)
413 {
414 if (const auto it = formats.find("info"); it != formats.end())
415 formats::info = it->second;
416 }
417
418 void links_plugin::handle_message(irccd& irccd, const message_event& ev)
419 {
420 requester::run(irccd.get_service(), ev);
421 }
422
423 // }}}
424
425 } // !namespace
426
427 extern "C" BOOST_SYMBOL_EXPORT links_plugin irccd_plugin_links;
428
429 links_plugin irccd_plugin_links("links", "");
430
431 } // !irccd