pike.git / lib / modules / Protocols.pmod / EngineIO.pmod

version» Context lines:

pike.git/lib/modules/Protocols.pmod/EngineIO.pmod:1:   /* -  * Clean-room Engine.IO implementation +  * Clean-room Engine.IO implementation for Pike.    */    -  + //! 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. + //! + //! The driver mainly is a wrapper around @[Protocol.WebSocket] with + //! the addition of two fallback mechanisms that work around limitations + //! imposed by firewalls and/or older browsers that prevent native + //! @[Protocol.WebSocket] connections from functioning. + //! + //! This module supports the following features: + //! @ul + //! @item It supports both UTF-8 and binary packets. + //! @item If both sides support @[Protocol.WebSocket], then + //! 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 + //! + //! In most cases, Engine.IO is not used directly in applications. Instead + //! one uses Socket.IO instead. + //! + //! @seealso + //! @[Protocol.SocketIO], @[Protocol.WebSocket], + //! @url{http://github.com/socketio/engine.io-protocol@}, + //! @url{http://socket.io/@} +    #pike __REAL_VERSION__      //#define EIO_DEBUG 1   //#define EIO_DEBUGMORE 1      //#define EIO_STATS 1 // Collect extra usage statistics      #define DRIVERNAME "Engine.IO"      #define DERROR(msg ...) ({sprintf(msg),backtrace()})
pike.git/lib/modules/Protocols.pmod/EngineIO.pmod:27:   #undef EIO_DEBUGMORE   #define PD(X ...) 0   #define PDT(X ...) 0   #define PT(X ...) (X)   #endif      #define SIDBYTES 16   #define TIMEBYTES 6      //! Global options for all EngineIO instances. + //! + //! @seealso + //! @[Socket.create()]   final mapping options = ([    "pingTimeout": 64*1000, // Safe for NAT    "pingInterval": 29*1000, // Allows for jitter    "allowUpgrades": 1,    "compressionLevel": 1, // gzip    "compressionThreshold": 256 // Compress when size>=   ]);    -  + //! Engine.IO protocol version.   constant protocol = 3; // EIO Protocol version      private enum {    // 0 1 2 3 4 5 6    BASE64='b', OPEN='0', CLOSE, PING, PONG, MESSAGE, UPGRADE, NOOP,    SENDQEMPTY, FORCECLOSE   };      // All EngineIO sessions indexed on sid.   private mapping(string:Socket) clients = ([]);      private Regexp acceptgzip = Regexp("(^|,)gzip(,|$)");   private Regexp xxsua = Regexp(";MSIE|Trident/");    - final void setoptions(mapping(string:mixed) _options) { -  options += _options; - } -  + //! @param _options + //! Optional options to override the defaults. + //! This parameter is passed down as is to the underlying + //! @[Socket]. + //!   //! @example   //! Sample minimal implementation of an EngineIO server farm:   //!   //!void echo(mixed id, string|Stdio.Buffer msg) {   //! id->write(msg);   //!}   //!   //!void wsrequest(array(string) protocols, object req) {   //! httprequest(req);   //!}
pike.git/lib/modules/Protocols.pmod/EngineIO.pmod:80:   //! client.write("Hello world!");   //! }   //! break;   //! }   //!}   //!   //!int main(int argc, array(string) argv)   //!{ Protocols.WebSocket.Port(httprequest, wsrequest, 80);   //! return -1;   //!} - final Socket farm(Protocols.WebSocket.Request req) { + //! + //! @seealso + //! @[Socket.create()] + final Socket farm(Protocols.WebSocket.Request req, void|mapping _options) {    string sid;    PD("Request %O\n", req.query);    if (sid = req.variables.sid) {    Socket client;    if (!(client = clients[sid]))    req.response_and_finish((["data":"Unknown sid",    "error":Protocols.HTTP.HTTP_GONE]));    else    client.onrequest(req);    } else -  return Socket(req); +  return Socket(req, _options);    return 0;   }    -  + //! Runs a single Engine.IO session. + class Socket { +  +  //! Contains the last request seen on this connection. +  //! Can be used to obtain cookies etc. +  final Protocols.WebSocket.Request request; +  +  //! The unique session identifier (in the Engine.IO docs referred +  //! to as simply: id). +  final string sid; +  +  private mixed id; // This is the callback parameter +  private mapping options; +  private Stdio.Buffer ci = Stdio.Buffer(); +  private function(mixed, string|Stdio.Buffer:void) read_cb; +  private function(mixed:void) close_cb; +  private Thread.Queue sendq = Thread.Queue(); +  private ADT.Queue recvq = ADT.Queue(); +  private string curtransport; +  private Transport conn; +  private Transport upgtransport; +  private enum {RUNNING = 0, PAUSED, SCLOSING, RCLOSING}; +  private int state = RUNNING; +     class Transport {    final function(int, void|string|Stdio.Buffer:void) read_cb; -  final Thread.Queue sendq; +     final protected int pingtimeout;       protected void create(Protocols.WebSocket.Request req) {    pingtimeout = options->pingTimeout/1000+1;    kickwatchdog();    }       private void droptimeout() {    remove_call_out(close);    }
pike.git/lib/modules/Protocols.pmod/EngineIO.pmod:220:    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 +  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();
pike.git/lib/modules/Protocols.pmod/EngineIO.pmod:277:    _req.response_and_finish((["data":"Unsupported method",    "error":Protocols.HTTP.HTTP_METHOD_INVALID,    "extra_heads":(["Allow":"GET,POST,OPTIONS"])]));    }    }       constant forcebinary = 0;       final void flush(void|int type, void|string|Stdio.Buffer msg) {    Thread.MutexKey lock; -  if (req && sendq && sendq.size() && (lock = getreq.trylock())) { +  if (req && sendq.size() && (lock = getreq.trylock())) {    Protocols.WebSocket.Request myreq;    array tosend;    if ((myreq = req) && sizeof(tosend = sendq.try_read_array())) {    req = 0;    lock = 0;    array m;    int anybinary = 0;    if (forcebinary && !forceascii)    foreach (tosend;; m)    if (!stringp(m[1])) {
pike.git/lib/modules/Protocols.pmod/EngineIO.pmod:310:    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); -  c->add((string)(1+1+sizeof(msg)))->add_int8(':')->add_int8(BASE64); +  c->add((string)(1+1+sizeof(msg))) +  ->add_int8(':')->add_int8(BASE64);    }    c->add_int8(type)->add(msg);    }    if (sizeof(c))    wrapfinish(myreq, c->read());    sendqempty();    } else    lock = 0;    }    }
pike.git/lib/modules/Protocols.pmod/EngineIO.pmod:427:    if (sizeof(sb)) {    string s = sb.get();    read_cb(s[0], s[1..]);    } else {    int type = bb->read_int8();    read_cb(type, bb->read_buffer(sizeof(bb)));    }    }   }    - //! Runs a single Engine.IO session. - class Socket { -  //! Contains the last request seen on this connection. -  //! Can be used to obtain cookies etc. -  final Protocols.WebSocket.Request request; -  //! The unique session identifier (in the Engine.IO docs referred -  //! to as simply id). -  final string sid; -  private mixed id; // This is the callback parameter -  private Stdio.Buffer ci = Stdio.Buffer(); -  private function(mixed, string|Stdio.Buffer:void) read_cb; -  private function(mixed:void) close_cb; -  private Thread.Queue sendq = Thread.Queue(); -  private ADT.Queue recvq = ADT.Queue(); -  private string curtransport; -  private Transport conn; -  private Transport upgtransport; -  private enum {RUNNING = 0, PAUSED, SCLOSING, RCLOSING}; -  private int state = RUNNING; -  +     //! Set initial argument on callbacks.    final void set_id(mixed _id) {    id = _id;    }       //! Retrieve initial argument on callbacks. Defaults to the Socket    //! object itself.    final mixed query_id() {    return id || this;    }
pike.git/lib/modules/Protocols.pmod/EngineIO.pmod:468:    //! As long as the read callback has not been set, all received messages    //! will be buffered.    final void set_callbacks(    void|function(mixed, string|Stdio.Buffer:void) _read_cb,    void|function(mixed:void) _close_cb) {    close_cb = _close_cb; // Set close callback first    read_cb = _read_cb; // to avoid losing the close event    flushrecvq();    }    -  //! Send text (string) or binary (Stdio.Buffer) messages. +  //! Send text @[string] or binary @[Stdio.Buffer] messages.    final void write(string|Stdio.Buffer ... msgs) {    if (state >= SCLOSING)    DUSERERROR("Socket already shutting down");    foreach (msgs;; string|Stdio.Buffer msg) {    PD("Queue %s %c:%O\n", sid, MESSAGE, (string)msg);    sendq.write(({MESSAGE, msg}));    }    if (state == RUNNING)    flush();    }
pike.git/lib/modules/Protocols.pmod/EngineIO.pmod:593:    flush();    break;    }    }    }       protected void destroy() {    close();    }    -  protected void create(Protocols.WebSocket.Request req) { +  //! @param _options +  //! Optional options to override the defaults. +  //! @mapping +  //! @member int "pingTimeout" +  //! If, the connection is idle for longer than this, the connection +  //! is terminated, unit in @expr{ms}. +  //! @member int "pingInterval" +  //! The browser-client will send a small ping message every +  //! @expr{pingInterval ms}. +  //! @member int "allowUpgrades" +  //! When @expr{true} (default), it allows the server to upgrade +  //! the connection to a real @[Protocol.WebSocket] connection. +  //! @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, +  void|mapping _options) {    request = req; -  +  options = .EngineIO.options; +  if (_options && sizeof(_options)) +  options += _options;    switch (curtransport = req.variables->transport) {    default:    req.response_and_finish((["data":"Unsupported transport",    "error":Protocols.HTTP.HTTP_UNSUPP_MEDIA]));    return;    case "websocket":    conn = WebSocket(req, req.websocket_accept(0));    break;    case "polling":    conn = req.variables.j ? JSONP(req) : XHR(req);    break;    }    conn.read_cb = recv; -  conn.sendq = sendq; +     ci->add(Crypto.Random.random_string(SIDBYTES-TIMEBYTES));    ci->add_hint(gethrtime(), TIMEBYTES);    sid = MIME.encode_base64(ci->read());    clients[sid] = this;    send(OPEN, Standards.JSON.encode(    (["sid":sid,    "upgrades":    options->allowUpgrades ? ({"websocket"}) : ({}),    "pingInterval":options->pingInterval,    "pingTimeout":options->pingTimeout
pike.git/lib/modules/Protocols.pmod/EngineIO.pmod:639:    conn.onrequest(req);    else    switch (s) {    default:    req.response_and_finish((["data":"Invalid transport",    "error":Protocols.HTTP.HTTP_UNSUPP_MEDIA]));    return 0;    case "websocket":    upgtransport = WebSocket(req, req.websocket_accept(0));    upgtransport.read_cb = upgrecv; -  upgtransport.sendq = sendq; +     }    }       private string _sprintf(int type, void|mapping flags) {    string res=UNDEFINED;    switch (type) {    case 'O':    res = sprintf(DRIVERNAME"(%s.%d,%s,%d,%d,%d,%d)",    sid, protocol, curtransport, state, sendq.size(),    recvq.is_empty(),sizeof(clients));    break;    }    return res;    }   }