f69a2b2016-09-19Stephen R. van den Berg /*
922d162016-09-23Stephen R. van den Berg  * Clean-room Engine.IO implementation for Pike.
f69a2b2016-09-19Stephen R. van den Berg  */
922d162016-09-23Stephen R. van den Berg //! This is an implementation of the Engine.IO server-side communication's //! driver. It basically is a real-time bidirectional packet-oriented //! communication's protocol for communicating between a webbrowser //! and a server.
9568292016-11-06Stephen R. van den Berg //!
d6f2472016-10-03Pontus Östlund //! The driver mainly is a wrapper around @[Protocols.WebSocket] with
922d162016-09-23Stephen R. van den Berg //! the addition of two fallback mechanisms that work around limitations //! imposed by firewalls and/or older browsers that prevent native
d6f2472016-10-03Pontus Östlund //! @[Protocols.WebSocket] connections from functioning.
922d162016-09-23Stephen R. van den Berg //! //! This module supports the following features: //! @ul
9568292016-11-06Stephen R. van den Berg //! @item //! It supports both UTF-8 and binary packets. //! @item //! If both sides support @[Protocols.WebSocket], then
922d162016-09-23Stephen R. van den Berg //! packet sizes are essentially unlimited. //! The fallback methods have a limit on packet sizes from browser //! to server, determined by the maximum POST request size the involved //! network components allow. //! @endul
9568292016-11-06Stephen R. van den Berg //!
922d162016-09-23Stephen R. van den Berg //! In most cases, Engine.IO is not used directly in applications. Instead //! one uses Socket.IO instead.
9568292016-11-06Stephen R. van den Berg //! //! @example //! Sample small implementation of an EngineIO server farm: //! @code //! class mysocket { //! inherit Web.EngineIO.Socket; //! //! void echo(string|Stdio.Buffer msg) { //! write(0, msg); //! } //! } //! //! void wsrequest(array(string) protocols, object req) { //! httprequest(req); //! } //! //! void httprequest(object req) { //! switch (req.not_query) { //! case "/engine.io/": //! mysocket client = Web.EngineIO.farm(mysocket, req); //! if (client) { //! client.open(client.echo); //! client.write(0, "Hello world!"); //! } //! break; //! } //! } //! //! int main(int argc, array(string) argv) { //! Protocols.WebSocket.Port(httprequest, wsrequest, 80); //! return -1; //! } //! @endcode //!
922d162016-09-23Stephen R. van den Berg //! @seealso
9568292016-11-06Stephen R. van den Berg //! @[farm], @[Web.SocketIO], @[Protocols.WebSocket],
922d162016-09-23Stephen R. van den Berg //! @url{http://github.com/socketio/engine.io-protocol@}, //! @url{http://socket.io/@}
f69a2b2016-09-19Stephen R. van den Berg #pike __REAL_VERSION__
9568292016-11-06Stephen R. van den Berg #pragma dynamic_dot
f69a2b2016-09-19Stephen R. van den Berg 
20859d2016-09-20Stephen R. van den Berg //#define EIO_DEBUG 1 //#define EIO_DEBUGMORE 1
f69a2b2016-09-19Stephen R. van den Berg 
20859d2016-09-20Stephen R. van den Berg //#define EIO_STATS 1 // Collect extra usage statistics
f69a2b2016-09-19Stephen R. van den Berg  #define DRIVERNAME "Engine.IO" #define DERROR(msg ...) ({sprintf(msg),backtrace()}) #define SERROR(msg ...) (sprintf(msg)) #define USERERROR(msg) throw(msg) #define SUSERERROR(msg ...) USERERROR(SERROR(msg))
d75c0e2016-09-22Stephen R. van den Berg #define DUSERERROR(msg ...) USERERROR(DERROR(msg))
f69a2b2016-09-19Stephen R. van den Berg  #ifdef EIO_DEBUG #define PD(X ...) werror(X)
d75c0e2016-09-22Stephen R. van den Berg #define PDT(X ...) (werror(X), \ werror(describe_backtrace(PT(backtrace()))))
f69a2b2016-09-19Stephen R. van den Berg  // PT() puts this in the backtrace #define PT(X ...) (lambda(object _this){return (X);}(this)) #else #undef EIO_DEBUGMORE #define PD(X ...) 0
d75c0e2016-09-22Stephen R. van den Berg #define PDT(X ...) 0
f69a2b2016-09-19Stephen R. van den Berg #define PT(X ...) (X) #endif
20859d2016-09-20Stephen R. van den Berg #define SIDBYTES 16 #define TIMEBYTES 6
f69a2b2016-09-19Stephen R. van den Berg 
9568292016-11-06Stephen R. van den Berg constant emptyarray = ({}); //! Engine.IO protocol version. constant protocol = 4; // EIO Protocol version
f69a2b2016-09-19Stephen R. van den Berg //! Global options for all EngineIO instances.
922d162016-09-23Stephen R. van den Berg //! //! @seealso
2ef9492016-09-25Stephen R. van den Berg //! @[Socket.create()], @[Gz.compress()]
f69a2b2016-09-19Stephen R. van den Berg final mapping options = ([ "pingTimeout": 64*1000, // Safe for NAT "pingInterval": 29*1000, // Allows for jitter
2ef9492016-09-25Stephen R. van den Berg #if constant(Gz.deflate)
79c5572016-09-29Stephen R. van den Berg  "compressionLevel": 3, // Setting to zero disables.
2ef9492016-09-25Stephen R. van den Berg  "compressionStrategy": Gz.DEFAULT_STRATEGY, "compressionThreshold": 256, // Compress when size>= "compressionWindowSize": 15, // LZ77 window 2^x (8..15)
9568292016-11-06Stephen R. van den Berg  "compressionHeuristics": Protocols.WebSocket.HEURISTICS_COMPRESS,
2ef9492016-09-25Stephen R. van den Berg #endif
79c5572016-09-29Stephen R. van den Berg  "allowUpgrades": 1,
9568292016-11-06Stephen R. van den Berg  "server_mtu": 4*1024, "server_textmtu": 8*1024, "server_inflight": 64*1024, "server_delayack": 20, // 20ms.
f69a2b2016-09-19Stephen R. van den Berg ]);
9568292016-11-06Stephen R. van den Berg final multiset clientoptions = (< "EIO", "pingTimeout", "server_mtu", "server_textmtu", "server_inflight", "server_delayack", >);
f69a2b2016-09-19Stephen R. van den Berg  private enum {
9568292016-11-06Stephen R. van den Berg  // 0 1 2 3 4 5 6 7 BASE64='b', OPEN='0', CLOSE, PING, PONG, MESSAGE, UPGRADE, NOOP, ACK, SENDQEMPTY, SHUTDOWN
f69a2b2016-09-19Stephen R. van den Berg }; // All EngineIO sessions indexed on sid.
5836402016-09-22Stephen R. van den Berg private mapping(string:Socket) clients = ([]);
f69a2b2016-09-19Stephen R. van den Berg  private Regexp acceptgzip = Regexp("(^|,)gzip(,|$)"); private Regexp xxsua = Regexp(";MSIE|Trident/");
2ef8e72016-09-26Stephen R. van den Berg //! @param options
922d162016-09-23Stephen R. van den Berg //! Optional options to override the defaults. //! This parameter is passed down as is to the underlying //! @[Socket]. //! //! @seealso //! @[Socket.create()]
9568292016-11-06Stephen R. van den Berg final Socket farm(object createtype, Protocols.WebSocket.Request req, void|mapping(string:mixed) options) {
f69a2b2016-09-19Stephen R. van den Berg  string sid; PD("Request %O\n", req.query); if (sid = req.variables.sid) {
5836402016-09-22Stephen R. van den Berg  Socket client;
f69a2b2016-09-19Stephen R. van den Berg  if (!(client = clients[sid]))
d75c0e2016-09-22Stephen R. van den Berg  req.response_and_finish((["data":"Unknown sid",
f69a2b2016-09-19Stephen R. van den Berg  "error":Protocols.HTTP.HTTP_GONE])); else
d75c0e2016-09-22Stephen R. van den Berg  client.onrequest(req);
f69a2b2016-09-19Stephen R. van den Berg  } else
9568292016-11-06Stephen R. van den Berg  return createtype(req, options);
f69a2b2016-09-19Stephen R. van den Berg  return 0; }
922d162016-09-23Stephen R. van den Berg //! Runs a single Engine.IO session. class Socket {
f69a2b2016-09-19Stephen R. van den Berg 
922d162016-09-23Stephen R. van den Berg  //! Contains the last request seen on this connection. //! Can be used to obtain cookies etc. final Protocols.WebSocket.Request request;
f69a2b2016-09-19Stephen R. van den Berg 
922d162016-09-23Stephen R. van den Berg  //! The unique session identifier (in the Engine.IO docs referred //! to as simply: id). final string sid;
f69a2b2016-09-19Stephen R. van den Berg 
9568292016-11-06Stephen R. van den Berg  final mapping options;
922d162016-09-23Stephen R. van den Berg  private Stdio.Buffer ci = Stdio.Buffer();
9568292016-11-06Stephen R. van den Berg  private function(string|Stdio.Buffer:void) read_cb; private function(:void) close_cb; private function(:void) lowmark_cb; private PriorityQueue sendq = PriorityQueue();
922d162016-09-23Stephen R. van den Berg  private Transport conn; private Transport upgtransport; private enum {RUNNING = 0, PAUSED, SCLOSING, RCLOSING}; private int state = RUNNING;
9568292016-11-06Stephen R. van den Berg  private int curpacketno = -1; private int scheduleack = 0; final int sendqbytesize() { return sendq.bytesize; } #define PACKETLEN(v) (1 + 2 + 1 + (sizeof(v) >= 2 && sizeof((v)[1]))) #define ACKRANGE (1 << 7 << 7) // Two 7 bit bytes final void flushack(void|int noflush) { if (scheduleack) { remove_call_out(flushack); scheduleack = 0; string msg = sprintf("%c%c", curpacketno & 0x7f, curpacketno >> 7); if (noflush) sendq.write(({ACK, msg})); else send(ACK, msg); } } final void sendack() { if (options->EIO > 3 && !scheduleack) { scheduleack = 1; call_out(flushack, options->server_delayack); } } class PriorityQueue { inherit Thread.Queue; final int bytesize; private Thread.Queue priority = Thread.Queue(); private Thread.Queue unacked = Thread.Queue(); private int lastacked = -1; int insync = 1; final int write(array v) { switch (v[0]) { case MESSAGE: case CLOSE: bytesize += PACKETLEN(v); return ::write(v); } return priority.write(v); } final int size() { return priority.size() + ::size(); } final array try_read() { array v = priority.try_read(); if (v) return v; if (insync && unacked.size() < ACKRANGE / 2 - 1 && (v = ::try_read())) unacked.write(v); return v; } final void gotack(int packetno) { array v; // Accept half the range in flight while ((packetno - lastacked & ACKRANGE - 1) < ACKRANGE / 2) { lastacked = (lastacked + 1) & ACKRANGE - 1; if (v = unacked.try_read()) bytesize -= PACKETLEN(v); } PD("Gotack %s %d left %d\n", sid, packetno, bytesize); if (!insync) { ::write(@(unacked.try_read_array() + ::try_read_array())); insync = 1; } if (lowmark_cb) lowmark_cb(); } }
f69a2b2016-09-19Stephen R. van den Berg 
922d162016-09-23Stephen R. van den Berg  class Transport { final function(int, void|string|Stdio.Buffer:void) read_cb; final protected int pingtimeout;
f69a2b2016-09-19Stephen R. van den Berg 
9568292016-11-06Stephen R. van den Berg  protected void create(Protocols.WebSocket.Request req, function(int, void|string|Stdio.Buffer:void) read_cb) { this->read_cb = read_cb; pingtimeout = (options->pingTimeout + options->pingInterval)/1000+1;
922d162016-09-23Stephen R. van den Berg  kickwatchdog(); }
f69a2b2016-09-19Stephen R. van den Berg 
922d162016-09-23Stephen R. van den Berg  private void droptimeout() { remove_call_out(close); }
f69a2b2016-09-19Stephen R. van den Berg 
c071bc2017-11-05Henrik Grubbström (Grubba)  protected void _destruct() {
9568292016-11-06Stephen R. van den Berg  shutdown();
922d162016-09-23Stephen R. van den Berg  }
f69a2b2016-09-19Stephen R. van den Berg 
922d162016-09-23Stephen R. van den Berg  final protected void kickwatchdog() { droptimeout(); call_out(close, pingtimeout); }
9568292016-11-06Stephen R. van den Berg  //! Shutdown the transport silently. void shutdown() {
922d162016-09-23Stephen R. van den Berg  droptimeout();
9568292016-11-06Stephen R. van den Berg  } //! Close the transport properly, notify the far end. void close() { shutdown(); if (read_cb) read_cb(SHUTDOWN);
922d162016-09-23Stephen R. van den Berg  } final protected void sendqempty() { if (!sendq.size()) read_cb(SENDQEMPTY); }
f69a2b2016-09-19Stephen R. van den Berg  }
922d162016-09-23Stephen R. van den Berg  class Polling {
a5bde72016-09-27Stephen R. van den Berg  inherit Transport;
922d162016-09-23Stephen R. van den Berg 
9568292016-11-06Stephen R. van den Berg  constant name = "polling";
922d162016-09-23Stephen R. van den Berg  private int forceascii; final protected Stdio.Buffer c = Stdio.Buffer(); final protected Stdio.Buffer ci = Stdio.Buffer(); final protected Protocols.WebSocket.Request req; private mapping headers = ([]); private Thread.Mutex getreq = Thread.Mutex(); private Thread.Mutex exclpost = Thread.Mutex();
2ef9492016-09-25Stephen R. van den Berg  #if constant(Gz.deflate)
922d162016-09-23Stephen R. van den Berg  private Gz.File gzfile; #endif protected string noop; protected void getbody(Protocols.WebSocket.Request _req);
79c5572016-09-29Stephen R. van den Berg  protected void wrapfinish(Protocols.WebSocket.Request req, string body, mapping(string:mixed) options);
922d162016-09-23Stephen R. van den Berg 
79c5572016-09-29Stephen R. van den Berg  protected void respfinish(Protocols.WebSocket.Request req, string body, int isbin, mapping(string:mixed) options) {
922d162016-09-23Stephen R. van den Berg  mapping|string comprheads;
79c5572016-09-29Stephen R. van den Berg  if (!sizeof(body)) body = noop; // Engine.IO does not like empty frames
2ef9492016-09-25Stephen R. van den Berg  #if constant(Gz.deflate)
79c5572016-09-29Stephen R. van den Berg  if (gzfile && options->compressionLevel>0 && sizeof(body) >= options->compressionThreshold
922d162016-09-23Stephen R. van den Berg  && (comprheads = req.request_headers["accept-encoding"]) && acceptgzip.match(comprheads)) { Stdio.FakeFile f = Stdio.FakeFile("", "wb"); gzfile.open(f, "wb");
79c5572016-09-29Stephen R. van den Berg  gzfile.setparams(options->compressionLevel, options->compressionStrategy, options->compressionWindowSize);
922d162016-09-23Stephen R. van den Berg  gzfile.write(body); gzfile.close(); comprheads = headers; if (sizeof(body)>f.stat().size) { body = (string)f; comprheads += (["Content-Encoding":"gzip"]); } } else #endif comprheads = headers; req.response_and_finish(([ "data":body,
79c5572016-09-29Stephen R. van den Berg  "type":isbin ? "application/octet-stream" : "text/plain;charset=UTF-8",
922d162016-09-23Stephen R. van den Berg  "extra_heads":comprheads])); }
9568292016-11-06Stephen R. van den Berg  protected void create(Protocols.WebSocket.Request _req, function(int, void|string|Stdio.Buffer:void) read_cb) {
922d162016-09-23Stephen R. van den Berg  noop = sprintf("1:%c", NOOP); forceascii = !zero_type(_req.variables->b64); ci->set_error_mode(1);
2ef9492016-09-25Stephen R. van den Berg  #if constant(Gz.deflate)
9568292016-11-06Stephen R. van den Berg  if (options->compressionLevel)
2ef9492016-09-25Stephen R. van den Berg  gzfile = Gz.File();
922d162016-09-23Stephen R. van den Berg  #endif
9568292016-11-06Stephen R. van den Berg  ::create(_req, read_cb);
922d162016-09-23Stephen R. van den Berg  if (_req.request_headers.origin) { headers["Access-Control-Allow-Credentials"] = "true"; headers["Access-Control-Allow-Origin"] = _req.request_headers.origin; } else headers["Access-Control-Allow-Origin"] = "*"; // prevent XSS warnings on IE string ua = _req.request_headers["user-agent"]; if (ua && xxsua.match(ua)) headers["X-XSS-Protection"] = "0"; onrequest(_req); } final void onrequest(Protocols.WebSocket.Request _req) { kickwatchdog(); switch (_req.request_type) { case "GET": flush(); req = _req; flush(); break; case "OPTIONS": headers["Access-Control-Allow-Headers"] = "Content-Type"; _req.response_and_finish((["data":"ok","extra_heads":headers])); break; case "POST": { getbody(_req); // Strangely enough, EngineIO 3 does not communicate back // over the POST request, so wrap it up. function ackpost() { _req.response_and_finish((["data":"ok","type":"text/plain", "extra_heads":headers])); }; #if constant(Thread.Thread) // This requires _req to remain unaltered for the remainder of // this function. Thread.Thread(ackpost); // Lower latency by doing it parallel #else ackpost(); #endif do { Thread.MutexKey lock; if (lock = exclpost.trylock()) { int type; string|Stdio.Buffer res; Stdio.Buffer.RewindKey rewind; rewind = ci->rewind_on_error(); while (sizeof(ci)) { if (catch { int len, isbin; if ((isbin = ci->read_int8()) <= 1) { for (len = 0; (type = ci->read_int8())!=0xff; len=len*10+type); len--; type = ci->read_int8() + (isbin ? OPEN : 0); res = isbin ? ci->read_buffer(len): ci->read(len); } else { ci->unread(1); len = ci->sscanf("%d:")[0]-1;
2bc3972016-09-22Stephen R. van den Berg  type = ci->read_int8();
922d162016-09-23Stephen R. van den Berg  if (type == BASE64) { type = ci->read_int8(); res = Stdio.Buffer(MIME.decode_base64(ci->read(len))); } else res = ci->read(len); } }) break; else { rewind.update(); if (stringp(res)) res = utf8_to_string(res); read_cb(type, res); }
2bc3972016-09-22Stephen R. van den Berg  }
922d162016-09-23Stephen R. van den Berg  lock = 0; } else break; } while (sizeof(ci)); break; } default: _req.response_and_finish((["data":"Unsupported method", "error":Protocols.HTTP.HTTP_METHOD_INVALID, "extra_heads":(["Allow":"GET,POST,OPTIONS"])]));
f69a2b2016-09-19Stephen R. van den Berg  } }
922d162016-09-23Stephen R. van den Berg  constant forcebinary = 0; final void flush(void|int type, void|string|Stdio.Buffer msg) { Thread.MutexKey lock;
9568292016-11-06Stephen R. van den Berg  mapping(string:mixed) opts = options; Protocols.WebSocket.Request myreq; if (type) { if (lock = getreq.trylock()) { myreq = req; req = 0; lock = 0; } if (myreq) wrapfinish(myreq, sprintf("%d:%c%s", 1+sizeof(msg), type, msg), opts); } else if (req && sendq.size() && sendq.bytesize < opts->server_inflight && (lock = getreq.trylock())) {
922d162016-09-23Stephen R. van den Berg  array tosend;
9568292016-11-06Stephen R. van den Berg  flushack(1); if ((myreq = req) && (tosend = sendq.try_read())) {
922d162016-09-23Stephen R. van den Berg  req = 0;
9568292016-11-06Stephen R. van den Berg  array m = tosend; tosend = emptyarray; do tosend += ({m}); while (sendq.bytesize < opts->server_inflight && (m = sendq.try_read()));
922d162016-09-23Stephen R. van den Berg  lock = 0; int anybinary = 0; if (forcebinary && !forceascii) foreach (tosend;; m)
79c5572016-09-29Stephen R. van den Berg  if (sizeof(m)>=2 && !stringp(m[1])) {
922d162016-09-23Stephen R. van den Berg  anybinary = 1; break; } foreach (tosend;; m) { type = m[0];
79c5572016-09-29Stephen R. van den Berg  msg = ""; switch(sizeof(m)) { case 3: if (m[2])
9568292016-11-06Stephen R. van den Berg  opts += m[2];
79c5572016-09-29Stephen R. van den Berg  case 2: msg = m[1] || ""; }
922d162016-09-23Stephen R. van den Berg  if (!anybinary && stringp(msg)) {
44f4aa2016-11-27Stephen R. van den Berg  msg = string_to_utf8(msg);
6563e12017-02-01Martin Nilsson  c->add((string)(1+sizeof(msg)), ':');
922d162016-09-23Stephen R. van den Berg  } else if (!forceascii) { if (stringp(msg)) c->add_int8(0); else { type -= OPEN; c->add_int8(1); } foreach ((string)(1+sizeof(msg));; int i) c->add_int8(i-'0'); c->add_int8(0xff); } else { msg = MIME.encode_base64(msg->read(), 1);
6563e12017-02-01Martin Nilsson  c->add((string)(1+1+sizeof(msg)), ':', BASE64);
44d5b32016-09-22Stephen R. van den Berg  }
6563e12017-02-01Martin Nilsson  c->add(type, msg);
44d5b32016-09-22Stephen R. van den Berg  }
922d162016-09-23Stephen R. van den Berg  if (sizeof(c))
9568292016-11-06Stephen R. van den Berg  wrapfinish(myreq, c->read(), opts);
922d162016-09-23Stephen R. van den Berg  sendqempty(); } else lock = 0; }
2bc3972016-09-22Stephen R. van den Berg  }
f69a2b2016-09-19Stephen R. van den Berg  }
922d162016-09-23Stephen R. van den Berg  class XHR {
a5bde72016-09-27Stephen R. van den Berg  inherit Polling;
922d162016-09-23Stephen R. van den Berg  constant forcebinary = 1;
f69a2b2016-09-19Stephen R. van den Berg 
922d162016-09-23Stephen R. van den Berg  final protected void getbody(Protocols.WebSocket.Request _req) { ci->add(_req.body_raw); }
f69a2b2016-09-19Stephen R. van den Berg 
922d162016-09-23Stephen R. van den Berg  final protected
79c5572016-09-29Stephen R. van den Berg  void wrapfinish(Protocols.WebSocket.Request req, string body, mapping(string:mixed) options) { respfinish(req, body, String.range(body)[1]==0xff, options);
922d162016-09-23Stephen R. van den Berg  }
f69a2b2016-09-19Stephen R. van den Berg  }
922d162016-09-23Stephen R. van den Berg  class JSONP {
a5bde72016-09-27Stephen R. van den Berg  inherit Polling;
f69a2b2016-09-19Stephen R. van den Berg 
922d162016-09-23Stephen R. van den Berg  private string head;
f69a2b2016-09-19Stephen R. van den Berg 
9568292016-11-06Stephen R. van den Berg  protected void create(Protocols.WebSocket.Request req, function(int, void|string|Stdio.Buffer:void) read_cb) { ::create(req, read_cb);
922d162016-09-23Stephen R. van den Berg  head = "___eio[" + (int)req.variables->j + "]("; noop = head+"\""+::noop+"\");"; }
f69a2b2016-09-19Stephen R. van den Berg 
922d162016-09-23Stephen R. van den Berg  final protected void getbody(Protocols.WebSocket.Request _req) { string d; if (d = _req.variables->d) // Reverse-map some braindead escape mechanisms ci->add(replace(d,({"\r\n","\\n"}),({"\r","\n"}))); }
f69a2b2016-09-19Stephen R. van den Berg 
922d162016-09-23Stephen R. van den Berg  final protected
79c5572016-09-29Stephen R. van den Berg  void wrapfinish(Protocols.WebSocket.Request req, string body, mapping(string:mixed) options) {
6563e12017-02-01Martin Nilsson  c->add(head, Standards.JSON.encode(body), ");");
79c5572016-09-29Stephen R. van den Berg  respfinish(req, c->read(), 0, options);
922d162016-09-23Stephen R. van den Berg  }
f69a2b2016-09-19Stephen R. van den Berg  }
922d162016-09-23Stephen R. van den Berg  class WebSocket {
a5bde72016-09-27Stephen R. van den Berg  inherit Transport;
f69a2b2016-09-19Stephen R. van den Berg 
9568292016-11-06Stephen R. van den Berg  constant name = "websocket";
922d162016-09-23Stephen R. van den Berg  private Protocols.WebSocket.Connection con; private Stdio.Buffer bb = Stdio.Buffer(); private String.Buffer sb = String.Buffer();
f69a2b2016-09-19Stephen R. van den Berg 
922d162016-09-23Stephen R. van den Berg  protected void create(Protocols.WebSocket.Request req,
9568292016-11-06Stephen R. van den Berg  function(int, void|string|Stdio.Buffer:void) read_cb,
922d162016-09-23Stephen R. van den Berg  Protocols.WebSocket.Connection _con) { con = _con; con.onmessage = recv;
9568292016-11-06Stephen R. van den Berg  ::create(req, read_cb);
922d162016-09-23Stephen R. van den Berg  }
f69a2b2016-09-19Stephen R. van den Berg 
922d162016-09-23Stephen R. van den Berg  final void flush(void|int type, void|string|Stdio.Buffer msg) {
9568292016-11-06Stephen R. van den Berg  mapping(string:mixed) opts = options;
922d162016-09-23Stephen R. van den Berg  void sendit() {
9568292016-11-06Stephen R. van den Berg  Protocols.WebSocket.Frame f = stringp(msg) ? Protocols.WebSocket.Frame(Protocols.WebSocket.FRAME_TEXT, sprintf("%c%s", type, msg)) : Protocols.WebSocket.Frame(Protocols.WebSocket.FRAME_BINARY,
79c5572016-09-29Stephen R. van den Berg  sprintf("%c%s", type - OPEN, msg->read()));
9568292016-11-06Stephen R. van den Berg  f.options += opts; if (catch(con.send(f))) { con.close(); shutdown(); }
922d162016-09-23Stephen R. van den Berg  };
9568292016-11-06Stephen R. van den Berg  if (msg)
922d162016-09-23Stephen R. van den Berg  sendit();
9568292016-11-06Stephen R. van den Berg  else { array m; if (sendq.size() && sendq.bytesize < opts->server_inflight) flushack(1); while (sendq.bytesize < opts->server_inflight && (m = sendq.try_read())) { opts = options; type = m[0]; msg = ""; switch(sizeof(m)) { case 3: if (m[2]) opts += m[2]; case 2: msg = m[1] || "";
922d162016-09-23Stephen R. van den Berg  }
9568292016-11-06Stephen R. van den Berg  sendit();
44d5b32016-09-22Stephen R. van den Berg  }
922d162016-09-23Stephen R. van den Berg  sendqempty();
2bc3972016-09-22Stephen R. van den Berg  } }
f69a2b2016-09-19Stephen R. van den Berg 
922d162016-09-23Stephen R. van den Berg  private void recv(Protocols.WebSocket.Frame f) { kickwatchdog(); switch (f.opcode) { case Protocols.WebSocket.FRAME_TEXT:
d75c0e2016-09-22Stephen R. van den Berg  sb.add(f.text);
922d162016-09-23Stephen R. van den Berg  break; case Protocols.WebSocket.FRAME_BINARY:
f69a2b2016-09-19Stephen R. van den Berg  bb->add(f.data);
922d162016-09-23Stephen R. van den Berg  break; case Protocols.WebSocket.FRAME_CONTINUATION: if (sizeof(sb)) sb.add(f.text); else bb->add(f.data); break; default: return;
f69a2b2016-09-19Stephen R. van den Berg  }
9568292016-11-06Stephen R. van den Berg  if (f.fin && read_cb)
922d162016-09-23Stephen R. van den Berg  if (sizeof(sb)) { string s = sb.get(); read_cb(s[0], s[1..]); } else { int type = bb->read_int8();
689cc12016-09-27Stephen R. van den Berg  read_cb(type + OPEN, bb->read_buffer(sizeof(bb)));
922d162016-09-23Stephen R. van den Berg  } }
f69a2b2016-09-19Stephen R. van den Berg  }
9568292016-11-06Stephen R. van den Berg  //! Set callbacks and open socket. final void open( void|function(string|Stdio.Buffer:void) _read_cb, void|function(:void) _close_cb, void|function(:void) _lowmark_cb) { close_cb = _close_cb; lowmark_cb = _lowmark_cb; read_cb = _read_cb; send(OPEN, Standards.JSON.encode( (["sid": sid,
44f4aa2016-11-27Stephen R. van den Berg  "EIO": this::options->EIO || protocol,
9568292016-11-06Stephen R. van den Berg  "upgrades":
44f4aa2016-11-27Stephen R. van den Berg  this::options->allowUpgrades ? ({"websocket"}) : ({}), "pingInterval": this::options->pingInterval, "pingTimeout": this::options->pingTimeout
9568292016-11-06Stephen R. van den Berg  ])));
f69a2b2016-09-19Stephen R. van den Berg  }
922d162016-09-23Stephen R. van den Berg  //! Send text @[string] or binary @[Stdio.Buffer] messages.
79c5572016-09-29Stephen R. van den Berg  final void write(mapping(string:mixed) options, string|Stdio.Buffer ... msgs) {
f69a2b2016-09-19Stephen R. van den Berg  if (state >= SCLOSING)
d75c0e2016-09-22Stephen R. van den Berg  DUSERERROR("Socket already shutting down");
f69a2b2016-09-19Stephen R. van den Berg  foreach (msgs;; string|Stdio.Buffer msg) { PD("Queue %s %c:%O\n", sid, MESSAGE, (string)msg);
79c5572016-09-29Stephen R. van den Berg  sendq.write(({MESSAGE, msg, options}));
f69a2b2016-09-19Stephen R. van den Berg  } if (state == RUNNING)
44d5b32016-09-22Stephen R. van den Berg  flush();
f69a2b2016-09-19Stephen R. van den Berg  } private void send(int type, void|string|Stdio.Buffer msg) { PD("Queue %s %c:%O\n", sid, type, (string)(msg || ""));
79c5572016-09-29Stephen R. van den Berg  sendq.write(({type, msg}));
20859d2016-09-20Stephen R. van den Berg  switch (state) { case RUNNING: case SCLOSING:
44d5b32016-09-22Stephen R. van den Berg  flush();
20859d2016-09-20Stephen R. van den Berg  }
f69a2b2016-09-19Stephen R. van den Berg  }
44d5b32016-09-22Stephen R. van den Berg  private void flush() {
9568292016-11-06Stephen R. van den Berg  mixed err; if (err = catch(conn.flush())) { PD("Flush failed %O\n", describe_backtrace(err)); catch(conn.shutdown());
7cac5c2016-09-24Stephen R. van den Berg  if (upgtransport) catch(upgtransport.close()); }
44d5b32016-09-22Stephen R. van den Berg  }
20859d2016-09-20Stephen R. van den Berg  //! Close the socket signalling the other side. final void close() {
f69a2b2016-09-19Stephen R. van den Berg  if (state < SCLOSING) {
20859d2016-09-20Stephen R. van den Berg  if (close_cb)
9568292016-11-06Stephen R. van den Berg  close_cb();
f69a2b2016-09-19Stephen R. van den Berg  PT("Send close, state %O\n", state); state = SCLOSING;
20859d2016-09-20Stephen R. van den Berg  catch(send(CLOSE));
f69a2b2016-09-19Stephen R. van den Berg  } }
20859d2016-09-20Stephen R. van den Berg  private void rclose() { close(); state = RCLOSING; m_delete(clients, sid); }
f69a2b2016-09-19Stephen R. van den Berg  private void clearcallback() { close_cb = 0;
9568292016-11-06Stephen R. van den Berg  lowmark_cb = 0;
f69a2b2016-09-19Stephen R. van den Berg  read_cb = 0; // Sort of a race, if multithreading
b6ae972016-09-23Stephen R. van den Berg  upgtransport = conn = 0;
f69a2b2016-09-19Stephen R. van den Berg  }
20859d2016-09-20Stephen R. van den Berg  private void recv(int type, void|string|Stdio.Buffer msg) {
f69a2b2016-09-19Stephen R. van den Berg #ifndef EIO_DEBUGMORE if (type!=SENDQEMPTY) #endif PD("Received %s %c:%O\n", sid, type, (string)msg); switch (type) {
9568292016-11-06Stephen R. van den Berg  default: // Protocol error PD("Protocol error %s %c\n", sid, type);
20859d2016-09-20Stephen R. van den Berg  close();
f69a2b2016-09-19Stephen R. van den Berg  break;
9568292016-11-06Stephen R. van den Berg  case OPEN: catch { mixed m = Standards.JSON.decode(msg); PD("Received %s client options %O\n", sid, msg); if (mappingp(m)) {
44f4aa2016-11-27Stephen R. van den Berg  this::options += m &= clientoptions; PD("Resulting %s options %O\n", sid, this::options);
9568292016-11-06Stephen R. van den Berg  sendack(); } }; break;
f69a2b2016-09-19Stephen R. van den Berg  case CLOSE:
20859d2016-09-20Stephen R. van den Berg  rclose();
2bc3972016-09-22Stephen R. van den Berg  if (!sendq.size())
f69a2b2016-09-19Stephen R. van den Berg  clearcallback(); break; case SENDQEMPTY: if (state == RCLOSING) clearcallback(); break;
9568292016-11-06Stephen R. van den Berg  case SHUTDOWN:
20859d2016-09-20Stephen R. van den Berg  rclose();
f69a2b2016-09-19Stephen R. van den Berg  clearcallback(); break; case PING: send(PONG, msg); break; case MESSAGE:
9568292016-11-06Stephen R. van den Berg  curpacketno = curpacketno + 1 & ACKRANGE - 1; sendack(); if (read_cb) read_cb(msg); break; case ACK: if (sizeof(msg) != 2) close(); // Protocol error
f69a2b2016-09-19Stephen R. van den Berg  else {
9568292016-11-06Stephen R. van den Berg  if (!stringp(msg)) msg = msg->read(); sendq.gotack(msg[1] << 7 | msg[0]);
f69a2b2016-09-19Stephen R. van den Berg  } break; } } private void upgrecv(int type, string|Stdio.Buffer msg) {
9568292016-11-06Stephen R. van den Berg  PD("UPGReceived %s %c:%O\n", sid, type, (string)msg);
f69a2b2016-09-19Stephen R. van den Berg  switch (type) { default: // Protocol error or CLOSE
9568292016-11-06Stephen R. van den Berg  if (upgtransport) { upgtransport.shutdown(); upgtransport = 0; if (state == PAUSED) state = RUNNING; if (conn) flush(); } case SENDQEMPTY:
f69a2b2016-09-19Stephen R. van den Berg  break; case PING: state = PAUSED;
2bc3972016-09-22Stephen R. van den Berg  if (!sendq.size())
f69a2b2016-09-19Stephen R. van den Berg  send(NOOP);
44d5b32016-09-22Stephen R. van den Berg  flush();
d75c0e2016-09-22Stephen R. van den Berg  upgtransport.flush(PONG, msg);
f69a2b2016-09-19Stephen R. van den Berg  break; case UPGRADE: {
d75c0e2016-09-22Stephen R. van den Berg  upgtransport.read_cb = recv;
9568292016-11-06Stephen R. van den Berg  conn.read_cb = 0; conn.shutdown();
5836402016-09-22Stephen R. van den Berg  conn = upgtransport;
9568292016-11-06Stephen R. van den Berg  sendq.insync = 0; sendack(); flush();
f69a2b2016-09-19Stephen R. van den Berg  if (state == PAUSED) state = RUNNING; upgtransport = 0;
44d5b32016-09-22Stephen R. van den Berg  flush();
f69a2b2016-09-19Stephen R. van den Berg  break; } } }
c071bc2017-11-05Henrik Grubbström (Grubba)  protected void _destruct() {
20859d2016-09-20Stephen R. van den Berg  close();
f69a2b2016-09-19Stephen R. van den Berg  }
2ef8e72016-09-26Stephen R. van den Berg  //! @param options
922d162016-09-23Stephen R. van den Berg  //! Optional options to override the defaults. //! @mapping //! @member int "pingTimeout" //! If, the connection is idle for longer than this, the connection
d6f2472016-10-03Pontus Östlund  //! is terminated, unit in @expr{ms@}.
922d162016-09-23Stephen R. van den Berg  //! @member int "pingInterval" //! The browser-client will send a small ping message every
d6f2472016-10-03Pontus Östlund  //! @expr{pingInterval ms@}.
922d162016-09-23Stephen R. van den Berg  //! @member int "allowUpgrades"
d6f2472016-10-03Pontus Östlund  //! When @expr{true@} (default), it allows the server to upgrade //! the connection to a real @[Protocols.WebSocket] connection.
922d162016-09-23Stephen R. van den Berg  //! @member int "compressionLevel" //! The gzip compressionlevel used to compress packets. //! @member int "compressionThreshold" //! Packets smaller than this will not be compressed. //! @endmapping protected void create(Protocols.WebSocket.Request req,
2ef8e72016-09-26Stephen R. van den Berg  void|mapping options) {
5836402016-09-22Stephen R. van den Berg  request = req;
44f4aa2016-11-27Stephen R. van den Berg  this::options = .EngineIO.options;
2ef8e72016-09-26Stephen R. van den Berg  if (options && sizeof(options))
44f4aa2016-11-27Stephen R. van den Berg  this::options += options;
9568292016-11-06Stephen R. van den Berg  switch (req.variables->transport) {
f69a2b2016-09-19Stephen R. van den Berg  default:
d75c0e2016-09-22Stephen R. van den Berg  req.response_and_finish((["data":"Unsupported transport",
f69a2b2016-09-19Stephen R. van den Berg  "error":Protocols.HTTP.HTTP_UNSUPP_MEDIA])); return; case "websocket":
9568292016-11-06Stephen R. van den Berg  conn = WebSocket(req, recv, req.websocket_accept(0,
44f4aa2016-11-27Stephen R. van den Berg  ({ Protocols.WebSocket.permessagedeflate(options) })));
f69a2b2016-09-19Stephen R. van den Berg  break; case "polling":
9568292016-11-06Stephen R. van den Berg  conn = (req.variables.j ? JSONP : XHR)(req, recv);
f69a2b2016-09-19Stephen R. van den Berg  break; } ci->add(Crypto.Random.random_string(SIDBYTES-TIMEBYTES)); ci->add_hint(gethrtime(), TIMEBYTES); sid = MIME.encode_base64(ci->read());
20859d2016-09-20Stephen R. van den Berg  clients[sid] = this;
f69a2b2016-09-19Stephen R. van den Berg  PD("New EngineIO sid: %O\n", sid); }
5836402016-09-22Stephen R. van den Berg  //! Handle request, and returns a new Socket object if it's a new
f69a2b2016-09-19Stephen R. van den Berg  //! connection. final void onrequest(Protocols.WebSocket.Request req) { string s;
5836402016-09-22Stephen R. van den Berg  request = req;
9568292016-11-06Stephen R. van den Berg  if ((s = req.variables->transport) == conn.name)
5836402016-09-22Stephen R. van den Berg  conn.onrequest(req);
9568292016-11-06Stephen R. van den Berg  else { PD("Transport %s %O\n", sid, s);
f69a2b2016-09-19Stephen R. van den Berg  switch (s) {
9568292016-11-06Stephen R. van den Berg  case "polling": if (options->EIO > 3) { upgtransport = (req.variables.j ? JSONP : XHR)(req, upgrecv); upgtransport.flush(PONG, "probe"); break; }
f69a2b2016-09-19Stephen R. van den Berg  default:
d75c0e2016-09-22Stephen R. van den Berg  req.response_and_finish((["data":"Invalid transport",
f69a2b2016-09-19Stephen R. van den Berg  "error":Protocols.HTTP.HTTP_UNSUPP_MEDIA]));
9568292016-11-06Stephen R. van den Berg  return;
f69a2b2016-09-19Stephen R. van den Berg  case "websocket":
9568292016-11-06Stephen R. van den Berg  upgtransport = WebSocket(req, upgrecv, req.websocket_accept(0,
44f4aa2016-11-27Stephen R. van den Berg  ({Protocols.WebSocket.permessagedeflate(options) })));
f69a2b2016-09-19Stephen R. van den Berg  }
9568292016-11-06Stephen R. van den Berg  }
f69a2b2016-09-19Stephen R. van den Berg  } private string _sprintf(int type, void|mapping flags) { string res=UNDEFINED;
20859d2016-09-20Stephen R. van den Berg  switch (type) {
f69a2b2016-09-19Stephen R. van den Berg  case 'O':
9568292016-11-06Stephen R. van den Berg  res = sprintf(DRIVERNAME"(%s.%d,%s,%d,%d,%d)", sid, protocol, conn.name, state, sendq.size(), sizeof(clients));
f69a2b2016-09-19Stephen R. van den Berg  break; } return res; } }