Anklang-0.3.0-1416-gfa8ce8d2 anklang-0.3.0-1416-gfa8ce8d2
ASE — Anklang Sound Engine (C++)

« « « Anklang Documentation
Loading...
Searching...
No Matches
websocket.cc
Go to the documentation of this file.
1 // This Source Code Form is licensed MPL-2.0: http://mozilla.org/MPL/2.0
2#include "websocket.hh"
3#include "platform.hh"
4#include "strings.hh"
5#include "formatter.hh"
6#include "path.hh"
7#include "blob.hh"
8#include "utils.hh"
9#include "regex.hh"
10#include "randomhash.hh"
11#include "internal.hh"
12#include "main.hh"
13#include <regex>
14#include <fstream>
15#include <map>
16#include <vector>
17
18// POSIX Network Headers
19#include <sys/socket.h>
20#include <netinet/in.h>
21#include <arpa/inet.h>
22#include <fcntl.h>
23#include <poll.h>
24#include <unistd.h>
25
26#include <websocketpp/config/core.hpp>
27#include <websocketpp/server.hpp>
28
29#define IODEBUG(...) while (0) { printerr (__VA_ARGS__); break; }
30
31namespace Ase {
32
33struct WebSocketServerImpl;
34using WebSocketServerImplP = std::shared_ptr<WebSocketServerImpl>;
35
36// Use core config with sync concurrency so send() is thread-safe
37struct CustomServerConfig : public websocketpp::config::core {
38 static const size_t connection_read_buffer_size = 16384;
39};
40
41using WppServer = websocketpp::server<CustomServerConfig>;
42using WppConnectionP = WppServer::connection_ptr;
43using WppConnection = WppConnectionP::element_type;
44using WppHdl = websocketpp::connection_hdl;
45
46static inline bool
47operator!= (const WppHdl &a, const WppHdl &b)
48{
49 return a.owner_before (b) || b.owner_before (a);
50}
51static inline bool operator== (const WppHdl &a, const WppHdl &b) { return !(a != b); }
52
53// State for main_loop integration
55 int fd;
56 WppConnectionP con;
57 std::string out_buffer;
58 std::string local_ip;
59 int local_port = 0;
60 std::string remote_ip;
61 int remote_port = 0;
62 Ase::LoopID source_id = Ase::LoopID::INVALID;
63};
64
65// == WebSocketServerImpl ==
69 WppServer wppserver_;
70 String server_url_, dir_, token_, see_other_;
72 ConVec opencons_;
73 RegexVector ignores_;
74 MakeConnection make_con_;
75 int logflags_ = 0;
76 int listen_port_ = 0;
78 int listen_fd_ = -1;
79 Ase::LoopID listen_source_id_ = Ase::LoopID::INVALID;
80
81 void update_client_sources();
82 bool io_handler (PollFD &pfd);
83
84 void setup ();
85 WebSocketConnectionP make_connection (WppHdl hdl);
86 WebSocketConnection* force_con (WppHdl);
87 friend class WebSocketServer;
88public:
89 int listen_port () const override { return listen_port_; }
90 String url () const override { return server_url_; }
91 WppConnectionP wppconp (WppHdl hdl)
92 {
93 websocketpp::lib::error_code ec;
94 return wppserver_.get_con_from_hdl (hdl, ec); // nullptr if (ec)
95 }
96 void
97 http_dir (const String &path) override
98 {
99 assert_return (listen_fd_ < 0);
100 dir_ = Path::normalize (path);
101 aliases_.clear();
102 ignores_.clear();
103
104 // compile .aseignore
105 String cxxline;
106 std::ifstream aseignore;
107 aseignore.open (Path::join (dir_, ".aseignore"));
108 while (!aseignore.fail() && std::getline (aseignore, cxxline))
109 if (!cxxline.empty())
110 ignores_.push_back (std::regex (cxxline));
111 }
112 void
113 http_alias (const String &webdir, const String &path) override
114 {
115 assert_return (!dir_.empty());
116 String aliaspath = Path::normalize (Path::abspath (path, dir_));
117 aliases_.push_back (std::make_pair (webdir, aliaspath));
118 // sort by URL length, longest URLs come first
119 std::stable_sort (aliases_.begin(), aliases_.end(), [] (const auto &a, const auto &b) {
120 return a.first.size() > b.first.size();
121 });
122 }
123 String
124 map_url (const String &urlpath) override
125 {
126 if (dir_.empty())
127 return "";
128
129 // decode URL, also uncovers '.' and '/'
130 String absurl = string_url_decode (urlpath);
131
132 // normalize '.' and '..' dirs
133 absurl = Path::simplify_abspath (absurl);
134
135 // ignore urls
136 for (const auto &ignorepat : ignores_)
137 if (std::regex_search (absurl, ignorepat))
138 return "";
139
140 // map URL to sorted aliases, prefers longest match
141 for (const auto &pair : aliases_)
142 if (absurl == pair.first)
143 return pair.second;
144 else if (absurl.size() > pair.first.size() &&
145 absurl[pair.first.size()] == '/' &&
146 strncmp (absurl.c_str(), pair.first.c_str(), pair.first.size()) == 0)
147 return Path::join (pair.second, absurl.c_str() + pair.first.size());
148
149 // fallback to root
150 return Path::join (dir_, absurl);
151 }
152 void
153 listen (const SocketInfo &sinfo, const UnlistenCB &ulcb) override
154 {
155 assert_return (listen_fd_ < 0);
156 listen_fd_ = sinfo.fd;
157 listen_port_ = sinfo.port;
158 if (::listen (listen_fd_, 128) < 0)
159 fatal_error ("failed to listen on %s:%d, fd=%d: %s", sinfo.host, sinfo.port, listen_fd_, strerror (errno));
160 fcntl (listen_fd_, F_SETFL, O_NONBLOCK);
161 String fullurl = string_format ("http://%s:%d/", sinfo.host.empty() ? "127.0.0.1" : sinfo.host, listen_port_);
162 server_url_ = fullurl;
163 setup();
164 // Register listen fd with main_loop
165 listen_source_id_ = main_loop->exec_io_handler ([this] (PollFD &pfd) {
166 // Handle new connections
167 if (pfd.revents & PollFD::IN) {
168 sockaddr_in client_addr;
169 socklen_t client_len = sizeof (client_addr);
170 int client_fd = accept (listen_fd_, (sockaddr*) &client_addr, &client_len);
171 if (client_fd >= 0) {
172 fcntl (client_fd, F_SETFL, O_NONBLOCK);
173 sockaddr_in local_addr;
174 socklen_t local_len = sizeof (local_addr);
175 getsockname (client_fd, (sockaddr*) &local_addr, &local_len);
176 WppConnectionP con = wppserver_.get_connection();
177 ClientState cs;
178 cs.fd = client_fd;
179 cs.con = con;
180 char ip_str[INET_ADDRSTRLEN];
181 inet_ntop (AF_INET, &(local_addr.sin_addr), ip_str, INET_ADDRSTRLEN);
182 cs.local_ip = ip_str;
183 cs.local_port = ntohs (local_addr.sin_port);
184 inet_ntop (AF_INET, &(client_addr.sin_addr), ip_str, INET_ADDRSTRLEN);
185 cs.remote_ip = ip_str;
186 cs.remote_port = ntohs (client_addr.sin_port);
187 // Wire up the iostream transport's write handler
188 con->set_write_handler ([this, client_fd] (WppHdl hdl, char const *data, size_t len)
189 {
190 auto it = clients_.find (client_fd);
191 if (it != clients_.end()) {
192 it->second.out_buffer.append (data, len);
193 main_loop->wakeup();
194 }
195 IODEBUG ("%s:%u:%s: fd=%d write_handler: len=%d\n", __FILE__, __LINE__, __func__, it != clients_.end() ? it->first : -1, len);
196 return websocketpp::lib::error_code();
197 });
198 clients_[client_fd] = cs;
199 IODEBUG ("%s:%u:%s: accept(%d)\n", __FILE__, __LINE__, __func__, client_fd);
200 con->start();
201 // Update client sources to include the new connection
202 update_client_sources();
203 }
204 }
205 return true; // keep handler active
206 }, listen_fd_, "r", LoopPriority::NORMAL);
207 }
208 void
209 see_other (const String &uri) override
210 {
211 // TODO: not thread-safe
212 see_other_ = uri;
213 }
214 void
215 shutdown () override
216 {
217 // Cancel loop sources
218 reset();
219 main_loop->cancel (&listen_source_id_);
220 ::close (listen_fd_);
221 listen_fd_ = -1;
222 }
223 void ws_opened (WebSocketConnectionP);
224 void ws_closed (WebSocketConnectionP);
225 void reset () override;
226 WebSocketServerImpl (const MakeConnection &make_con, int logflags, const std::string &token) :
227 token_ (token), make_con_ (make_con), logflags_ (logflags)
228 {}
229};
230
231void
232WebSocketServerImpl::setup ()
233{
234 wppserver_.set_user_agent (user_agent());
235 wppserver_.set_validate_handler ([this] (WppHdl hdl)
236 {
237 WebSocketConnectionP conp = make_connection (hdl);
238 WppConnectionP cp = this->wppconp (hdl);
239 return_unless (conp && cp, false);
240 const bool is_authenticated = conp->authenticated (token_);
241 if (is_authenticated) {
242 const int index = conp->validate();
243 if (index >= 0) {
244 const std::vector<std::string> &subprotocols = cp->get_requested_subprotocols();
245 if (size_t (index) < subprotocols.size())
246 {
247 cp->select_subprotocol (subprotocols[index]);
248 return true;
249 }
250 else if (subprotocols.size() == 0 && index == 0)
251 return true;
252 }
253 }
254 cp->set_status (websocketpp::http::status_code::forbidden);
255 conp->log_status (cp->get_response_code());
256 return false;
257 });
258 wppserver_.set_http_handler ([this] (WppHdl hdl)
259 {
260 WebSocketConnectionP conp = make_connection (hdl);
261 if (conp)
262 conp->http_request();
263 });
264 wppserver_.clear_access_channels (websocketpp::log::alevel::all);
265 wppserver_.clear_error_channels (websocketpp::log::elevel::all);
266}
267
268bool
269WebSocketServerImpl::io_handler (PollFD &pfd)
270{
271 const int fd = pfd.fd;
272 IODEBUG ("%s:%u:%s: io-ready(%d): r=%d w=%d other=0x%x\n", __FILE__, __LINE__, __func__, fd,
273 pfd.revents & PollFD::IN, pfd.revents & PollFD::OUT,
274 pfd.revents & ~(PollFD::IN | PollFD::OUT));
275 auto it = clients_.find (fd);
276 if (it == clients_.end())
277 return false; // client gone
278 const WppConnectionP con = it->second.con; // owning copy, reentrant close may erase the client
279 if (it->second.out_buffer.size()) {
280 auto &client = it->second;
281 ssize_t n = ::send (fd, client.out_buffer.data(), client.out_buffer.size(), 0);
282 IODEBUG ("%s:%u:%s: send(%d,%d)\n", __FILE__, __LINE__, __func__, fd, n);
283 if (n > 0)
284 client.out_buffer.erase (0, n);
285 else if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
286 con->fatal_error();
287 it = clients_.find (fd);
288 return_unless (it != clients_.end(), false);
289 }
290 }
291 if ((pfd.revents & PollFD::IN) &&
292 con->get_state() != websocketpp::session::state::closed) {
293 char buf[8192];
294 ssize_t n = ::recv (fd, buf, sizeof (buf), 0);
295 IODEBUG ("%s:%u:%s: recv(%d)=%d\n", __FILE__, __LINE__, __func__, fd, n);
296 if (n > 0) {
297 try {
298 con->read_all (buf, n);
299 } catch (...) {
300 con->fatal_error(); // websocketpp error
301 }
302 }
303 else if (n == 0)
304 con->eof();
305 else if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
306 con->fatal_error(); // read EIO
307 it = clients_.find (fd);
308 return_unless (it != clients_.end(), false);
309 }
310 if (pfd.revents & (PollFD::ERR | PollFD::HUP | PollFD::NVAL)) { // 0x8 0x10 0x20
311 con->fatal_error(); // fd EIO
312 it = clients_.find (fd);
313 return_unless (it != clients_.end(), false);
314 }
315 if (con->get_state() == websocketpp::session::state::closed)
316 IODEBUG ("%s:%u:%s: closed fd=%d ob=%d io_errors=0x%x\n", __FILE__, __LINE__, __func__, fd, it->second.out_buffer.size(), pfd.revents & (PollFD::ERR | PollFD::HUP | PollFD::NVAL));
317 if (it->second.out_buffer.size())
318 pfd.events |= PollFD::OUT;
319 else
320 pfd.events &= ~PollFD::OUT;
321 const bool closed = it->second.out_buffer.empty() && con->get_state() == websocketpp::session::state::closed;
322 return !closed; // keep handler if not closed
323}
324
325// Update client sources when clients connect/disconnect
326void
327WebSocketServerImpl::update_client_sources()
328{
329 // Register missing client fds
330 for (auto &[fd, client] : clients_)
331 if (client.source_id == Ase::LoopID::INVALID)
332 client.source_id = main_loop->exec_io_handler ([this] (PollFD &pfd)
333 {
334 return this->io_handler (pfd);
335 }, fd, "r", LoopPriority::NORMAL);
336}
337
338// == WebSocketConnection ==
341 WppServer &wppserver;
342 WppHdl hdl;
343 String nickname_;
344 bool opened = false;
345 WppConnectionP wppconp() { return server->wppconp (hdl); }
346 friend struct WebSocketServerImpl;
347};
348
349void
350WebSocketConnection::log_status (int intstatus, const String &filepath)
351{
352 const auto status = websocketpp::http::status_code::value (intstatus);
353 WppConnectionP cp = internals_.wppconp();
354 if (!cp || !(logflags_ & 16))
355 return;
356 using namespace AnsiColors;
357 const bool rejected = (status >= 400 && status <= 499) || status == 303;
358 const String C1 = rejected ? color (FG_RED) : "";
359 const String C0 = rejected ? color (RESET) : "";
360 String resource = cp->get_resource();
361 resource = Re::sub ("\\btoken=[^;?]+", "token=<………>", resource);
362 log (string_format ("%s%d %s %s%s%s",
363 C1, status,
364 cp->get_request().get_method(), resource, C0,
365 filepath.empty() ? "" : " [" + filepath + "]"));
366}
367
368WebSocketConnection::WebSocketConnection (Internals &internals, int logflags) :
369 internals_ (*new (internals_mem_) Internals (internals)), logflags_ (logflags)
370{
371 static_assert (sizeof (Internals) <= sizeof (internals_mem_));
372 assert_return (internals_.server != nullptr);
373}
374
375WebSocketConnection::~WebSocketConnection()
376{
377 internals_.~Internals();
378}
379
380bool
381WebSocketConnection::is_open () const
382{
383 return internals_.opened;
384}
385
386bool
388{
389 assert_return (!message.empty(), false);
390 WppConnectionP cp = internals_.wppconp();
391 return_unless (cp, false);
392 websocketpp::lib::error_code ec;
393 // Find client fd for debug logging
394 int fd = -1;
395 for (const auto &[f, client] : internals_.server->clients_)
396 if (client.con == cp)
397 { fd = f; break; }
398 IODEBUG ("%s:%u:%s: fd=%d message=%d\n", __FILE__, __LINE__, __func__, fd, message.size());
399 internals_.wppserver.send (internals_.hdl, message, websocketpp::frame::opcode::text, ec);
400 if (ec)
401 {
402 if (logflags_ > 0)
403 log (string_format ("Error: %s: %s", __func__, ec.message()));
404 websocketpp::lib::error_code ec2;
405 cp->close (websocketpp::close::status::going_away, "", ec2);
406 return false;
407 }
408 return true;
409}
410
411bool
413{
414 WppConnectionP cp = internals_.wppconp();
415 return_unless (cp, false);
416 websocketpp::lib::error_code ec;
417 internals_.wppserver.send (internals_.hdl, blob, websocketpp::frame::opcode::binary, ec);
418 if (ec)
419 {
420 if (logflags_ > 0)
421 log (string_format ("Error: %s: %s", __func__, ec.message()));
422 websocketpp::lib::error_code ec2;
423 cp->close (websocketpp::close::status::going_away, "", ec2);
424 return false; // invalid connection or send failed
425 }
426 if (ASE_UNLIKELY (logflags_ & 256))
427 {
428 String hex;
429 for (size_t i = 0; i < blob.size(); i++)
430 {
431 if (i && 0 == i % 16)
432 hex += "\n ";
433 else if (0 == i % 8)
434 hex += " ";
435 hex += string_format (" %02x", blob[i]);
436 }
437 log (string_format ("⇜ Blob: len=%d hash=%016x\n%s", blob.size(), fnv1a_consthash64 (blob.data(), blob.size()), hex));
438 }
439 return true; // connection alive and message queued
440}
441
443WebSocketConnection::get_info ()
444{
445 WppConnectionP cp = internals_.wppconp();
446 const websocketpp::http::parser::request &rq = cp->get_request();
447 auto headers = std::make_shared<websocketpp::http::parser::header_list> (rq.get_headers());
448 Info info;
449 info.header = [headers] (const String &header) {
450 const auto it = headers->find (header);
451 return it == headers->end() ? "" : it->second;
452 };
453 if (0)
454 for (auto it : *headers)
455 IODEBUG ("%s: %s\n", it.first, it.second);
456
457 // Lookup IP/Port from our manual ClientState map
458 for (const auto &[fd, client] : internals_.server->clients_)
459 if (client.con == cp) {
460 info.local = client.local_ip;
461 info.lport = client.local_port;
462 info.remote = client.remote_ip;
463 info.rport = client.remote_port;
464 break;
465 }
466 info.subs = cp->get_requested_subprotocols();
467 return info;
468}
469
470String
471WebSocketConnection::nickname ()
472{
473 if (internals_.nickname_.empty())
474 {
475 Info info = get_info();
476 const String ua = info.header ("User-Agent");
477 String s = info.local + ":" + string_from_int (info.lport) + "\n" +
478 info.remote + ":" + string_from_int (info.rport * 0) + "\n" +
479 ua + "\n" +
480 info.header ("Accept-Encoding") + "\n" +
481 info.header ("Accept-Language") + "\n" +
482 info.header ("sec-ch-ua") + "\n" +
483 info.header ("sec-ch-ua-mobile") + "\n" +
484 info.header ("sec-gpc") + "\n";
485 String hh;
486 uint64_t hash = string_hash64 (s);
487 if (Re::search (R"(\bFirefox/)", ua) >= 0)
488 hh = "FF";
489 else if (Re::search (R"(\bElectron/)", ua) >= 0)
490 hh = "El";
491 else if (Re::search (R"(\bChrome-Lighthouse\b)", ua) >= 0)
492 hh = "Lh";
493 else if (Re::search (R"(\bChrome/)", ua) >= 0)
494 hh = "CR";
495 else if (Re::search (R"(\bSafari/)", ua) >= 0)
496 hh = "Sa";
497 else if (Re::search (R"(\bWget/)", ua) >= 0)
498 hh = "Wg";
499 else if (Re::search (R"(\bw3m/)", ua) >= 0)
500 hh = "W3";
501 else if (Re::search (R"(\bLynx/)", ua) >= 0)
502 hh = "Ly";
503 else if (Re::search (R"(\bPython-urllib/)", ua) >= 0)
504 hh = "Py";
505 else
506 hh = "NA";
507 internals_.nickname_ = string_format ("%s-%08x:%x", hh, uint32_t (hash ^ (hash >> 32)), info.rport);
508 }
509 return internals_.nickname_;
510}
511
512int WebSocketConnection::validate () { return -1; }
513void WebSocketConnection::failed () { /*if (logflags_ & 2) log (__func__);*/ }
514void WebSocketConnection::opened () { if (logflags_ & 4) log (__func__); }
515void WebSocketConnection::message (const String &message) { if (logflags_ & 8) log (__func__); }
516void WebSocketConnection::closed () { if (logflags_ & 4) log (__func__); }
517void WebSocketConnection::log (const String &message) { printerr ("%s\n", message);}
518
519bool
521{
522 WppConnectionP cp = internals_.wppconp();
523 if (!cp)
524 return false;
525 if (token.empty())
526 return true;
527 const String cookie_header = cp->get_request().get_header ("Cookie");
528 const String cookie_token = Re::grep ("\\bsession_auth=([^;?]+)", cookie_header, 1);
529 return cookie_token == token;
530}
531
532enum CacheType {
533 CACHE_NEVER,
534 CACHE_AUTO,
535 CACHE_BRIEFLY,
536 CACHE_FOREVER,
537};
538
539void
541{
542 if (internals_.server->dir_.empty())
543 return;
544 WppConnectionP cp = internals_.wppconp();
545 const auto &parts = string_split (cp->get_resource(), "?");
546 const String query = cp->get_uri()->get_query();
547 String filepath;
548
549 // Helpers
550 auto set_response = [] (WppConnectionP cp, websocketpp::http::status_code::value status, const String &title, const String &msg)
551 {
552 const String body =
553 string_format ("<!DOCTYPE html>\n"
554 "<html><head><title>%u %s</title></head>\n<body>\n"
555 "<h1>%s</h1>\n\n"
556 "%s\n"
557 "<hr><address>%s</address>\n"
558 "<hr></body></html>\n", status, title, title,
559 msg.size() ? "<p>" + msg + "</p>" : "",
560 WebSocketServer::user_agent());
561 cp->set_body (body);
562 cp->replace_header ("Content-Type", "text/html; charset=utf-8");
563 cp->set_status (status);
564 };
565
566 // GET ~auth
567 if (cp->get_request().get_method() == "GET" && parts[0] == "/~auth" &&
568 (authenticated (internals_.server->token_) ||
569 Re::search ("\\btoken=" + internals_.server->token_ + "\\b", query) >= 0)) {
570 const std::string target = string_format ("http://localhost:%u/", cp->get_port());
571 cp->replace_header ("Set-Cookie", "session_auth=" + internals_.server->token_ + "; Path=/; HttpOnly");
572 cp->replace_header ("Location", target);
573 set_response (cp, websocketpp::http::status_code::found, // 302 Found
574 "Found", "Redirecting to: <tt>" + target + "</tt>");
575 log_status (cp->get_response_code());
576 return;
577 }
578
579 // Validate Cookie session_auth
580 if (!authenticated (internals_.server->token_)) {
581 const std::string see_other = internals_.server->see_other_;
582 if (see_other.empty())
583 set_response (cp, websocketpp::http::status_code::method_not_allowed, // 405 Method Not Allowed
584 "Method Not Allowed", "Authentication required.");
585 else {
586 if (string_startswith (see_other, "http"))
587 cp->replace_header ("Location", see_other);
588 set_response (cp, websocketpp::http::status_code::see_other, // 303 See Other
589 "See Other", "<a href=\"" + see_other + "\">" + see_other + "</a>");
590 }
591 log_status (cp->get_response_code());
592 return;
593 }
594
595 // find file for URL
596 filepath = internals_.server->map_url (parts[0]);
597
598 // map directories to index.html
599 if (!filepath.empty() && Path::check (filepath, "dx"))
600 filepath = Path::join (filepath, "index.html");
601
602 // GET
603 if (cp->get_request().get_method() == "GET") {
604 // serve existing files
605 const String accept_encoding = cp->get_request().get_header ("Accept-Encoding");
606 const bool canzip = nullptr != string_find_word (accept_encoding.c_str(), "gzip");
607 bool fp = false, fz = false;
608 if (!filepath.empty() && ((fp = Path::check (filepath, "fr")) || (fz = canzip && Path::check (filepath + ".gz", "fr"))))
609 {
610 const char *ext = strrchr (filepath.c_str(), '.');
611 const String mimetype = WebSocketServer::mime_type (ext ? ext + 1 : "", true);
612 cp->append_header ("Content-Type", mimetype);
613 // Use CACHE_FOREVER for text/css to reduce FOUC (flashing of unstyled content)
614 // with shadowRoot stylesheet links in Chrome-115. CACHE_AUTO worsens FOUC.
615 const CacheType caching = mimetype == "text/css" ? CACHE_FOREVER : CACHE_AUTO;
616 switch (caching) {
617 case CACHE_NEVER: // response must not be stored in cache
618 cp->append_header ("Cache-Control", "no-store");
619 break;
620 case CACHE_AUTO: // force response revalidation if content stored in cache
621 cp->append_header ("Cache-Control", "no-cache"); // max-age=0, must-revalidate
622 break;
623 case CACHE_BRIEFLY: // needs validation support: https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests
624 cp->append_header ("Cache-Control", "max-age=7, stale-while-revalidate=31536000");
625 break;
626 case CACHE_FOREVER: // keep content forever in cache
627 cp->append_header ("Cache-Control", "max-age=31536000, public, immutable");
628 break;
629 }
630 if (fz) {
631 filepath = filepath + ".gz";
632 cp->append_header ("Content-Encoding", "gzip");
633 }
634 Blob blob = Blob::from_file (filepath);
635 cp->set_body (blob.string());
636 cp->set_status (websocketpp::http::status_code::ok); // 200 OK
637 }
638 else // 404
639 set_response (cp, websocketpp::http::status_code::not_found, // 404 Not Found
640 "Not Found",
641 "The requested URL was not found: <br /> <tt>" + cp->get_uri()->str() + "</tt>");
642 }
643 else // 405
644 set_response (cp, websocketpp::http::status_code::method_not_allowed, // 405 Method Not Allowed
645 "Method Not Allowed", "");
646
647 log_status (cp->get_response_code());
648}
649
650// == WebSocketServerImpl ==
652WebSocketServerImpl::make_connection (WppHdl hdl)
653{
654 WppConnectionP cp = wppconp (hdl);
655 assert_return (cp, nullptr);
658 wppserver_, hdl,
659 };
660 WebSocketConnectionP conp = make_con_ (internals, logflags_);
661 // capturing conp in handler contexts keeps it alive long enough for calls
662 cp->set_http_handler ([conp] (WppHdl hdl) {
663 WebSocketConnection::Internals &internals_ = WebSocketServer::internals (*conp);
664 assert_return (hdl == internals_.hdl);
665 conp->http_request();
666 });
667 // cp->set_validate_handler (...); - too late, validate_handler calls make_connection()
668 cp->set_fail_handler ([conp] (WppHdl hdl) {
669 WebSocketConnection::Internals &internals_ = WebSocketServer::internals (*conp);
670 assert_return (hdl == internals_.hdl);
671 conp->failed();
672 });
673 cp->set_open_handler ([conp] (WppHdl hdl) {
674 WebSocketConnection::Internals &internals_ = WebSocketServer::internals (*conp);
675 assert_return (hdl == internals_.hdl);
676 internals_.server->ws_opened (conp);
677 });
678 cp->set_message_handler ([conp] (WppHdl hdl, WppServer::message_ptr msg) {
679 WebSocketConnection::Internals &internals_ = WebSocketServer::internals (*conp);
680 assert_return (hdl == internals_.hdl);
681 conp->message (msg->get_payload());
682 });
683 cp->set_close_handler ([conp] (WppHdl hdl) {
684 WebSocketConnection::Internals &internals_ = WebSocketServer::internals (*conp);
685 assert_return (hdl == internals_.hdl);
686 internals_.server->ws_closed (conp);
687 });
688 return conp;
689}
690
691void
692WebSocketServerImpl::ws_opened (WebSocketConnectionP conp)
693{
694 WebSocketConnection::Internals &internals_ = WebSocketServer::internals (*conp);
695 internals_.opened = true;
696 opencons_.push_back (conp);
697 // Update client sources to include the new connection
698 update_client_sources();
699 conp->opened ();
700}
701
702void
703WebSocketServerImpl::ws_closed (WebSocketConnectionP conp)
704{
705 WebSocketConnection::Internals &internals_ = WebSocketServer::internals (*conp);
706 WppConnectionP wppconp = internals_.wppconp();
707 if (internals_.opened) {
708 internals_.opened = false;
709 auto it = std::find (opencons_.begin(), opencons_.end(), conp);
710 if (it != opencons_.end())
711 opencons_.erase (it);
712 // Find and remove the client from clients_
713 for (auto client_it = clients_.begin(); client_it != clients_.end(); ) {
714 if (client_it->second.con == wppconp) {
715 auto &[fd, client] = *client_it;
716 ::close (fd);
717 main_loop->cancel (&client.source_id);
718 IODEBUG ("%s:%u:%s: closed fd=%d ob=%d\n", __FILE__, __LINE__, __func__, fd, client.out_buffer.size());
719 client_it = clients_.erase (client_it);
720 } else
721 ++client_it;
722 }
723 conp->closed();
724 }
725}
726
727void
728WebSocketServerImpl::reset()
729{
730 // stop open connections
731 for (ssize_t i = ssize_t (opencons_.size()) - 1; i >= 0; i--)
732 {
733 WebSocketConnectionP conp = opencons_[i];
734 WebSocketConnection::Internals &internals_ = WebSocketServer::internals (*conp);
735 WppConnectionP cp = internals_.wppconp();
736 websocketpp::lib::error_code ec;
737 cp->close (websocketpp::close::status::going_away, "", ec); // omit_handshake
738 }
739
740 // cancel client sources and close raw sockets
741 for (auto &[fd, client] : clients_) {
742 ::close (fd);
743 main_loop->cancel (&client.source_id);
744 IODEBUG ("%s:%u:%s: closed fd=%d ob=%d\n", __FILE__, __LINE__, __func__, fd, client.out_buffer.size());
745 }
746 clients_.clear();
747}
748
749// == WebSocketServer ==
750WebSocketServer::~WebSocketServer()
751{}
752
753WebSocketServer::SocketInfo
754WebSocketServer::bind_port (const String &host, int port)
755{
756 SocketInfo sinfo { -1, host, port };
757 sinfo.fd = socket (AF_INET, SOCK_STREAM, 0);
758 if (sinfo.fd < 0)
759 fatal_error ("failed to create socket for: %s:%d: %s", host, port, strerror (errno));
760 sockaddr_in addr{};
761 int opt = 1;
762 if (port > 0) {
763 setsockopt (sinfo.fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt));
764 addr.sin_port = htons (port);
765 }
766 addr.sin_family = AF_INET;
767 if (host == "0.0.0.0")
768 addr.sin_addr.s_addr = htonl (INADDR_ANY);
769 else
770 {
771 addr.sin_addr.s_addr = inet_addr (host.c_str());
772 if (addr.sin_addr.s_addr == INADDR_NONE)
773 addr.sin_addr.s_addr = htonl (INADDR_LOOPBACK);
774 }
775 if (bind (sinfo.fd, (sockaddr*) &addr, sizeof (addr)) < 0)
776 fatal_error ("failed to bind to %s:%d, fd=%d: %s", host, port, sinfo.fd, strerror (errno));
777 socklen_t len = sizeof (addr);
778 if (getsockname (sinfo.fd, (sockaddr*) &addr, &len) == 0)
779 sinfo.port = ntohs (addr.sin_port);
780 return sinfo;
781}
782
783WebSocketServerP
784WebSocketServer::create (const MakeConnection &make, int logflags, const std::string &session_token)
785{
786 return std::make_shared<WebSocketServerImpl> (make, logflags, session_token);
787}
788
789String
790WebSocketServer::user_agent ()
791{
792 return String ("AnklangSynthEngine/") + ase_version();
793}
794
796bool
798{
799 return !!websocketpp::utf8_validator::validate (utf8string);
800}
801
802#include "mime-types.hh" // static const char mime_types[];
803
804String
805WebSocketServer::mime_type (const String &ext, bool utf8)
806{
808 static MimeMap mime_map = [] ()
809 {
810 MimeMap mime_map;
811 // mime_types, list of "mime/type ext ext2\n" lines
812 for (const String &line : string_split (mime_types, "\n"))
813 {
814 const StringS w = string_split (line, " ");
815 for (size_t i = 1; i < w.size(); i++)
816 if (!w[i].empty())
817 {
818 if (mime_map.end() != mime_map.find (w[i]))
819 warning ("mime-types.hh: duplicate extension: %s", w[i]);
820 mime_map[w[i]] = w[0];
821 }
822 }
823 return mime_map;
824 } ();
825 auto it = mime_map.find (ext);
826 String mimetype = it != mime_map.end() ? it->second : "application/octet-stream";
827 if (utf8)
828 {
829 if (mimetype == "text/html" || mimetype == "text/markdown" || mimetype == "text/plain")
830 mimetype += "; charset=utf-8";
831 }
832 return mimetype;
833}
834
835} // Ase
T c_str(T... args)
Binary large object storage container.
Definition blob.hh:11
static Blob from_file(const String &filename)
Create Blob by loading from filename.
Definition blob.cc:196
String string()
Copy Blob data into a zero terminated string.
Definition blob.cc:117
static String grep(const String &regex, const String &input, int group=0, Flags=DEFAULT)
Find regex in input and return matching string.
Definition regex.cc:225
static ssize_t search(const String &regex, const String &input, Flags=DEFAULT)
Find regex in input and return match position >= 0 or return < 0 otherwise.
Definition regex.cc:217
virtual void closed()
Pairs with opened().
Definition websocket.cc:516
virtual int validate()
Return true to allow opened().
Definition websocket.cc:512
virtual void http_request()
Independent of socket ops.
Definition websocket.cc:540
virtual void opened()
Pairs with closed().
Definition websocket.cc:514
bool send_binary(const String &blob)
Returns true if binary blob was sent.
Definition websocket.cc:412
virtual void message(const String &message)
Only if opened.
Definition websocket.cc:515
virtual void failed()
Never folloed by opened().
Definition websocket.cc:513
bool send_text(const String &message)
Returns true if text message was sent.
Definition websocket.cc:387
virtual bool authenticated(const std::string &token)
Return true to allow validate().
Definition websocket.cc:520
static bool utf8_validate(const std::string &utf8string)
Validate UTF-8 string with websocketpp::utf8_validator.
Definition websocket.cc:797
T clear(T... args)
#define ASE_UNLIKELY(expr)
Compiler hint to optimize for expr evaluating to false.
Definition cxxaux.hh:45
T data(T... args)
T empty(T... args)
T end(T... args)
errno
T fail(T... args)
fcntl
T find(T... args)
T getline(T... args)
htons
inet_addr
#define assert_return(expr,...)
Return from the current function if expr is unmet and issue an assertion warning.
Definition internal.hh:28
#define return_unless(cond,...)
Return silently if cond does not evaluate to true with return value ...
Definition internal.hh:72
log
T make_pair(T... args)
bool check(const String &file, const String &mode)
Definition path.cc:625
String simplify_abspath(const std::string &abspath_expression)
Remove extra slashes, './' and '../' from abspath_expression.
Definition path.cc:886
String abspath(const String &path, const String &incwd)
Definition path.cc:134
String normalize(const String &path)
Convert path to normal form.
Definition path.cc:86
The Anklang C++ API namespace.
Definition api.hh:8
std::string string_format(const char *format, const Args &...args) __attribute__((__format__(__printf__
Format a string similar to sprintf(3) with support for std::string and std::ostringstream convertible...
StringS string_split(const String &string, const String &splitter, size_t maxn)
Split a string, using splitter as delimiter.
Definition strings.cc:343
String string_from_int(int64 value)
Convert a 64bit signed integer into a string.
Definition strings.cc:604
const char * string_find_word(const char *haystack, const char *word)
Find occurance of word in haystack.
Definition strings.cc:520
@ NORMAL
Normal importantance, GUI event processing, RPC.
String string_url_decode(const String &urlstr, const bool form_url_encoded)
Decode URL %-sequences in a string, decode '+' if form_url_encoded.
Definition strings.cc:965
const char * ase_version()
Provide a string containing the package version.
Definition platform.cc:803
std::string String
Convenience alias for std::string.
Definition cxxaux.hh:34
bool string_startswith(const String &string, const String &fragment)
Returns whether string starts with fragment.
Definition strings.cc:846
size_t hash(size_t seed, const T &v)
T open(T... args)
T push_back(T... args)
T regex_search(T... args)
setsockopt
shutdown
T size(T... args)
socket
T stable_sort(T... args)
typedef uint64_t
strerror
strncmp
strrchr
Mirrors struct pollfd for poll(3posix)
Definition loop.hh:14
@ IN
RDNORM || RDBAND.
Definition loop.hh:20
typedef ssize_t