19#include <sys/socket.h>
20#include <netinet/in.h>
26#include <websocketpp/config/core.hpp>
27#include <websocketpp/server.hpp>
29#define IODEBUG(...) while (0) { printerr (__VA_ARGS__); break; }
33struct WebSocketServerImpl;
38 static const size_t connection_read_buffer_size = 16384;
41using WppServer = websocketpp::server<CustomServerConfig>;
42using WppConnectionP = WppServer::connection_ptr;
43using WppConnection = WppConnectionP::element_type;
44using WppHdl = websocketpp::connection_hdl;
47operator!= (
const WppHdl &a,
const WppHdl &b)
49 return a.owner_before (b) || b.owner_before (a);
51static inline bool operator== (
const WppHdl &a,
const WppHdl &b) {
return !(a != b); }
62 Ase::LoopID source_id = Ase::LoopID::INVALID;
70 String server_url_, dir_, token_, see_other_;
79 Ase::LoopID listen_source_id_ = Ase::LoopID::INVALID;
81 void update_client_sources();
82 bool io_handler (
PollFD &pfd);
89 int listen_port ()
const override {
return listen_port_; }
90 String url ()
const override {
return server_url_; }
91 WppConnectionP wppconp (WppHdl hdl)
93 websocketpp::lib::error_code ec;
94 return wppserver_.get_con_from_hdl (hdl, ec);
97 http_dir (
const String &path)
override
107 aseignore.
open (Path::join (dir_,
".aseignore"));
109 if (!cxxline.
empty())
113 http_alias (
const String &webdir,
const String &path)
override
119 std::stable_sort (aliases_.begin(), aliases_.end(), [] (
const auto &a,
const auto &b) {
120 return a.first.size() > b.first.size();
124 map_url (
const String &urlpath)
override
136 for (
const auto &ignorepat : ignores_)
141 for (
const auto &pair : aliases_)
142 if (absurl == pair.first)
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());
150 return Path::join (dir_, absurl);
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);
162 server_url_ = fullurl;
165 listen_source_id_ = main_loop->exec_io_handler ([
this] (
PollFD &pfd) {
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();
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);
188 con->set_write_handler ([this, client_fd] (WppHdl hdl, char const *data, size_t len)
190 auto it = clients_.find (client_fd);
191 if (it != clients_.end()) {
192 it->second.out_buffer.append (data, len);
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();
198 clients_[client_fd] = cs;
199 IODEBUG (
"%s:%u:%s: accept(%d)\n", __FILE__, __LINE__, __func__, client_fd);
202 update_client_sources();
209 see_other (
const String &uri)
override
219 main_loop->cancel (&listen_source_id_);
220 ::close (listen_fd_);
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)
232WebSocketServerImpl::setup ()
234 wppserver_.set_user_agent (user_agent());
235 wppserver_.set_validate_handler ([
this] (WppHdl hdl)
237 WebSocketConnectionP conp = make_connection (hdl);
238 WppConnectionP cp = this->wppconp (hdl);
240 const bool is_authenticated = conp->authenticated (token_);
241 if (is_authenticated) {
242 const int index = conp->validate();
245 if (
size_t (index) < subprotocols.size())
247 cp->select_subprotocol (subprotocols[index]);
250 else if (subprotocols.size() == 0 && index == 0)
254 cp->set_status (websocketpp::http::status_code::forbidden);
255 conp->log_status (cp->get_response_code());
258 wppserver_.set_http_handler ([
this] (WppHdl hdl)
260 WebSocketConnectionP conp = make_connection (hdl);
262 conp->http_request();
264 wppserver_.clear_access_channels (websocketpp::log::alevel::all);
265 wppserver_.clear_error_channels (websocketpp::log::elevel::all);
269WebSocketServerImpl::io_handler (PollFD &pfd)
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())
278 const WppConnectionP con = it->second.con;
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);
284 client.out_buffer.erase (0, n);
285 else if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
287 it = clients_.find (fd);
291 if ((pfd.revents & PollFD::IN) &&
292 con->get_state() != websocketpp::session::state::closed) {
294 ssize_t n = ::recv (fd, buf,
sizeof (buf), 0);
295 IODEBUG (
"%s:%u:%s: recv(%d)=%d\n", __FILE__, __LINE__, __func__, fd, n);
298 con->read_all (buf, n);
305 else if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
307 it = clients_.find (fd);
310 if (pfd.revents & (PollFD::ERR | PollFD::HUP | PollFD::NVAL)) {
312 it = clients_.find (fd);
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;
320 pfd.events &= ~PollFD::OUT;
321 const bool closed = it->second.out_buffer.empty() && con->get_state() == websocketpp::session::state::closed;
327WebSocketServerImpl::update_client_sources()
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)
334 return this->io_handler (pfd);
335 }, fd,
"r", LoopPriority::NORMAL);
341 WppServer &wppserver;
345 WppConnectionP wppconp() {
return server->wppconp (hdl); }
350WebSocketConnection::log_status (
int intstatus,
const String &filepath)
352 const auto status = websocketpp::http::status_code::value (intstatus);
353 WppConnectionP cp = internals_.wppconp();
354 if (!cp || !(logflags_ & 16))
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",
364 cp->get_request().get_method(), resource, C0,
365 filepath.
empty() ?
"" :
" [" + filepath +
"]"));
368WebSocketConnection::WebSocketConnection (Internals &internals,
int logflags) :
369 internals_ (*new (internals_mem_) Internals (internals)), logflags_ (logflags)
371 static_assert (
sizeof (Internals) <=
sizeof (internals_mem_));
375WebSocketConnection::~WebSocketConnection()
377 internals_.~Internals();
381WebSocketConnection::is_open ()
const
383 return internals_.opened;
390 WppConnectionP cp = internals_.wppconp();
392 websocketpp::lib::error_code ec;
395 for (
const auto &[f, client] : internals_.server->clients_)
396 if (client.con == cp)
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);
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);
414 WppConnectionP cp = internals_.wppconp();
416 websocketpp::lib::error_code ec;
417 internals_.wppserver.send (internals_.hdl, blob, websocketpp::frame::opcode::binary, ec);
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);
429 for (
size_t i = 0; i < blob.
size(); i++)
431 if (i && 0 == i % 16)
443WebSocketConnection::get_info ()
445 WppConnectionP cp = internals_.wppconp();
446 const websocketpp::http::parser::request &rq = cp->get_request();
449 info.header = [headers] (
const String &header) {
450 const auto it = headers->find (header);
451 return it == headers->end() ?
"" : it->second;
454 for (
auto it : *headers)
455 IODEBUG (
"%s: %s\n", it.first, it.second);
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;
466 info.subs = cp->get_requested_subprotocols();
471WebSocketConnection::nickname ()
473 if (internals_.nickname_.
empty())
475 Info info = get_info();
476 const String ua = info.header (
"User-Agent");
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";
489 else if (
Re::search (R
"(\bElectron/)", ua) >= 0)
491 else if (
Re::search (R
"(\bChrome-Lighthouse\b)", ua) >= 0)
493 else if (
Re::search (R
"(\bChrome/)", ua) >= 0)
495 else if (
Re::search (R
"(\bSafari/)", ua) >= 0)
503 else if (
Re::search (R
"(\bPython-urllib/)", ua) >= 0)
507 internals_.nickname_ =
string_format (
"%s-%08x:%x", hh, uint32_t (hash ^ (hash >> 32)), info.rport);
509 return internals_.nickname_;
522 WppConnectionP cp = internals_.wppconp();
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;
542 if (internals_.server->dir_.empty())
544 WppConnectionP cp = internals_.wppconp();
545 const auto &parts =
string_split (cp->get_resource(),
"?");
546 const String query = cp->get_uri()->get_query();
550 auto set_response = [] (WppConnectionP cp, websocketpp::http::status_code::value status,
const String &title,
const String &msg)
554 "<html><head><title>%u %s</title></head>\n<body>\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());
562 cp->replace_header (
"Content-Type",
"text/html; charset=utf-8");
563 cp->set_status (status);
567 if (cp->get_request().get_method() ==
"GET" && parts[0] ==
"/~auth" &&
569 Re::search (
"\\btoken=" + internals_.server->token_ +
"\\b", query) >= 0)) {
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,
574 "Found",
"Redirecting to: <tt>" + target +
"</tt>");
575 log_status (cp->get_response_code());
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,
584 "Method Not Allowed",
"Authentication required.");
587 cp->replace_header (
"Location", see_other);
588 set_response (cp, websocketpp::http::status_code::see_other,
589 "See Other",
"<a href=\"" + see_other +
"\">" + see_other +
"</a>");
591 log_status (cp->get_response_code());
596 filepath = internals_.server->map_url (parts[0]);
600 filepath = Path::join (filepath,
"index.html");
603 if (cp->get_request().get_method() ==
"GET") {
605 const String accept_encoding = cp->get_request().get_header (
"Accept-Encoding");
607 bool fp =
false, fz =
false;
611 const String mimetype = WebSocketServer::mime_type (ext ? ext + 1 :
"",
true);
612 cp->append_header (
"Content-Type", mimetype);
615 const CacheType caching = mimetype ==
"text/css" ? CACHE_FOREVER : CACHE_AUTO;
618 cp->append_header (
"Cache-Control",
"no-store");
621 cp->append_header (
"Cache-Control",
"no-cache");
624 cp->append_header (
"Cache-Control",
"max-age=7, stale-while-revalidate=31536000");
627 cp->append_header (
"Cache-Control",
"max-age=31536000, public, immutable");
631 filepath = filepath +
".gz";
632 cp->append_header (
"Content-Encoding",
"gzip");
635 cp->set_body (blob.
string());
636 cp->set_status (websocketpp::http::status_code::ok);
639 set_response (cp, websocketpp::http::status_code::not_found,
641 "The requested URL was not found: <br /> <tt>" + cp->get_uri()->str() +
"</tt>");
644 set_response (cp, websocketpp::http::status_code::method_not_allowed,
645 "Method Not Allowed",
"");
647 log_status (cp->get_response_code());
652WebSocketServerImpl::make_connection (WppHdl hdl)
654 WppConnectionP cp = wppconp (hdl);
660 WebSocketConnectionP conp = make_con_ (internals, logflags_);
662 cp->set_http_handler ([conp] (WppHdl hdl) {
663 WebSocketConnection::Internals &internals_ = WebSocketServer::internals (*conp);
665 conp->http_request();
668 cp->set_fail_handler ([conp] (WppHdl hdl) {
669 WebSocketConnection::Internals &internals_ = WebSocketServer::internals (*conp);
673 cp->set_open_handler ([conp] (WppHdl hdl) {
674 WebSocketConnection::Internals &internals_ = WebSocketServer::internals (*conp);
676 internals_.server->ws_opened (conp);
678 cp->set_message_handler ([conp] (WppHdl hdl, WppServer::message_ptr msg) {
679 WebSocketConnection::Internals &internals_ = WebSocketServer::internals (*conp);
681 conp->message (msg->get_payload());
683 cp->set_close_handler ([conp] (WppHdl hdl) {
684 WebSocketConnection::Internals &internals_ = WebSocketServer::internals (*conp);
686 internals_.server->ws_closed (conp);
692WebSocketServerImpl::ws_opened (WebSocketConnectionP conp)
694 WebSocketConnection::Internals &internals_ = WebSocketServer::internals (*conp);
695 internals_.opened =
true;
696 opencons_.push_back (conp);
698 update_client_sources();
703WebSocketServerImpl::ws_closed (WebSocketConnectionP conp)
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);
713 for (
auto client_it = clients_.begin(); client_it != clients_.end(); ) {
714 if (client_it->second.con == wppconp) {
715 auto &[fd, client] = *client_it;
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);
728WebSocketServerImpl::reset()
731 for (ssize_t i = ssize_t (opencons_.size()) - 1; i >= 0; i--)
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);
741 for (
auto &[fd, client] : clients_) {
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());
750WebSocketServer::~WebSocketServer()
753WebSocketServer::SocketInfo
754WebSocketServer::bind_port (
const String &host,
int port)
756 SocketInfo sinfo { -1, host, port };
757 sinfo.fd =
socket (AF_INET, SOCK_STREAM, 0);
759 fatal_error (
"failed to create socket for: %s:%d: %s", host, port, strerror (errno));
763 setsockopt (sinfo.fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt));
764 addr.sin_port =
htons (port);
766 addr.sin_family = AF_INET;
767 if (host ==
"0.0.0.0")
768 addr.sin_addr.s_addr =
htonl (INADDR_ANY);
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);
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);
784WebSocketServer::create (
const MakeConnection &make,
int logflags,
const std::string &session_token)
790WebSocketServer::user_agent ()
799 return !!websocketpp::utf8_validator::validate (utf8string);
805WebSocketServer::mime_type (
const String &ext,
bool utf8)
808 static MimeMap mime_map = [] ()
815 for (
size_t i = 1; i < w.
size(); i++)
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];
825 auto it = mime_map.find (ext);
826 String mimetype = it != mime_map.
end() ? it->second :
"application/octet-stream";
829 if (mimetype ==
"text/html" || mimetype ==
"text/markdown" || mimetype ==
"text/plain")
830 mimetype +=
"; charset=utf-8";
Binary large object storage container.
static Blob from_file(const String &filename)
Create Blob by loading from filename.
String string()
Copy Blob data into a zero terminated string.
static String grep(const String ®ex, const String &input, int group=0, Flags=DEFAULT)
Find regex in input and return matching string.
static ssize_t search(const String ®ex, const String &input, Flags=DEFAULT)
Find regex in input and return match position >= 0 or return < 0 otherwise.
virtual void closed()
Pairs with opened().
virtual int validate()
Return true to allow opened().
virtual void http_request()
Independent of socket ops.
virtual void opened()
Pairs with closed().
bool send_binary(const String &blob)
Returns true if binary blob was sent.
virtual void message(const String &message)
Only if opened.
virtual void failed()
Never folloed by opened().
bool send_text(const String &message)
Returns true if text message was sent.
virtual bool authenticated(const std::string &token)
Return true to allow validate().
static bool utf8_validate(const std::string &utf8string)
Validate UTF-8 string with websocketpp::utf8_validator.
#define ASE_UNLIKELY(expr)
Compiler hint to optimize for expr evaluating to false.
#define assert_return(expr,...)
Return from the current function if expr is unmet and issue an assertion warning.
#define return_unless(cond,...)
Return silently if cond does not evaluate to true with return value ...
bool check(const String &file, const String &mode)
String simplify_abspath(const std::string &abspath_expression)
Remove extra slashes, './' and '../' from abspath_expression.
String abspath(const String &path, const String &incwd)
String normalize(const String &path)
Convert path to normal form.
The Anklang C++ API namespace.
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.
String string_from_int(int64 value)
Convert a 64bit signed integer into a string.
const char * string_find_word(const char *haystack, const char *word)
Find occurance of word in haystack.
@ 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.
const char * ase_version()
Provide a string containing the package version.
std::string String
Convenience alias for std::string.
bool string_startswith(const String &string, const String &fragment)
Returns whether string starts with fragment.
size_t hash(size_t seed, const T &v)
T regex_search(T... args)
T shared_from_this(T... args)
Mirrors struct pollfd for poll(3posix)