Mercurial > irccd
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 |