]> granicus.if.org Git - apache/commitdiff
mod_lua: Add rudimentary WebSocket support. This is a WIP (emphasis on the W, I and...
authorDaniel Gruno <humbedooh@apache.org>
Wed, 11 Sep 2013 20:50:46 +0000 (20:50 +0000)
committerDaniel Gruno <humbedooh@apache.org>
Wed, 11 Sep 2013 20:50:46 +0000 (20:50 +0000)
git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1522030 13f79535-47bb-0310-9956-ffa450edef68

CHANGES
docs/manual/mod/mod_lua.xml
modules/lua/lua_request.c

diff --git a/CHANGES b/CHANGES
index 3618e48247cec6a6c0733e27f612263d07222dc2..0d897df0330f19ff21cad25da5f90fb09d802ea1 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -1,6 +1,9 @@
                                                          -*- coding: utf-8 -*-
 Changes with Apache 2.5.0
 
+  *) mod_lua: Add rudimentary support for WebSocket interaction. This is 
+     currently request-bound and only supports the WS protocol. [Daniel Gruno]
+
   *) mod_lua: Add getcookie/setcookie for reading/writing HTTP cookies
      [Daniel Gruno]
 
index 3488d417ac56d3f9241dc5a9c74edf16200f9f99..bc19549e6b4531001a1f7c5a6c15ecc3d9ddfb6d 100644 (file)
@@ -976,6 +976,39 @@ r:setcookie(key, value, secure, expires) -- Sets a HTTP cookie, for instance:
 r:setcookie("foo", "bar and stuff", false, os.time() + 86400)
 </highlight>
 
+<highlight language="lua">
+r:wsupgrade() -- Upgrades a connection to WebSockets if possible (and requested):
+if r:wsupgrade() then -- if we can upgrade:
+    r:wswrite("Welcome to websockets!") -- write something to the client
+    r:wsclose()  -- goodbye!
+end
+</highlight>
+
+<highlight language="lua">
+r:wsread() -- Reads a WebSocket frame from a WebSocket upgraded connection (see above):
+           -- Currently, only the WS protocol is supported (no WSS support for reading)
+local line, isFinal = r:wsread() -- isFinal denotes whether this is the final frame.
+                                 -- If it isn't, then more frames can be read
+r:wswrite("You wrote: " .. line)
+</highlight>
+
+<highlight language="lua">
+r:wswrite(line) -- Writes a frame to a WebSocket client:
+r:wswrite("Hello, world!")
+</highlight>
+
+<highlight language="lua">
+r:wsclose() -- Closes a WebSocket request and terminates it for httpd:
+
+if r:wsupgrade() then
+    r:wswrite("Write something: ")
+    local line = r:wsread() or "nothing"
+    r:wswrite("You wrote: " .. line);
+    r:wswrite("Goodbye!")
+    r:wsclose()
+end
+</highlight>
+
 </section>
 
 <section id="logging"><title>Logging Functions</title>
index e1a2bc723683c851459e6287537b5d5c6d7a00cf..0b6a306a5a0093ce0cd5aeddff3f4e2d91973502 100644 (file)
@@ -28,8 +28,7 @@
 #include "apr_thread_mutex.h"
 #include "apr_tables.h"
 #include "util_cookies.h"
-
-#include <lua.h>
+#include "apr_want.h"
 
 extern apr_thread_mutex_t* lua_ivm_mutex;
 
@@ -1930,6 +1929,322 @@ static int lua_set_cookie(lua_State *L)
     return 0;
 }
 
+static apr_uint64_t ap_ntoh64(const apr_uint64_t *input)
+{
+    apr_uint64_t rval;
+    unsigned char *data = (unsigned char *)&rval;
+
+    data[0] = *input >> 56;
+    data[1] = *input >> 48;
+    data[2] = *input >> 40;
+    data[3] = *input >> 32;
+    data[4] = *input >> 24;
+    data[5] = *input >> 16;
+    data[6] = *input >> 8;
+    data[7] = *input >> 0;
+
+    return rval;
+}
+
+static int lua_websocket_greet(lua_State *L)
+{
+    const char *key = NULL;
+    unsigned char digest[APR_SHA1_DIGESTSIZE];
+    apr_sha1_ctx_t sha1;
+    char           *encoded;
+    int encoded_len;
+    request_rec *r = ap_lua_check_request_rec(L, 1);
+    key = apr_table_get(r->headers_in, "Sec-WebSocket-Key");
+    if (key != NULL) {
+        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+                    "Websocket: Got websocket key: %s", key);
+        key = apr_pstrcat(r->pool, key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 
+                NULL);
+        apr_sha1_init(&sha1);
+        apr_sha1_update(&sha1, key, strlen(key));
+        apr_sha1_final(digest, &sha1);
+        encoded_len = apr_base64_encode_len(APR_SHA1_DIGESTSIZE);
+        if (encoded_len) {
+            encoded = apr_palloc(r->pool, encoded_len);
+            encoded_len = apr_base64_encode(encoded, (char*) digest, APR_SHA1_DIGESTSIZE);
+            r->status = 101;
+            apr_table_set(r->headers_out, "Upgrade", "websocket");
+            apr_table_set(r->headers_out, "Connection", "Upgrade");
+            apr_table_set(r->headers_out, "Sec-WebSocket-Accept", encoded);
+            
+            /* Trick httpd into NOT using the chunked filter, IMPORTANT!!!111*/
+            apr_table_set(r->headers_out, "Transfer-Encoding", "chunked");
+            
+            r->clength = 0;
+            r->bytes_sent = 0;
+            r->read_chunked = 0;
+            ap_rflush(r);
+            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+                    "Websocket: Upgraded from HTTP to Websocket");
+            lua_pushboolean(L, 1);
+            return 1;
+        }
+    }
+    ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, 
+                    "Websocket: Upgrade from HTTP to Websocket failed");
+    return 0;
+}
+
+static int lua_websocket_read(lua_State *L) 
+{
+    apr_socket_t *sock;
+    apr_status_t rv;
+    int n = 0;
+    apr_size_t len = 1;
+    apr_size_t plen = 0;
+    unsigned short payload_short = 0;
+    apr_uint64_t payload_long = 0;
+    unsigned char *mask_bytes;
+    char byte;
+    int plaintext;
+    
+    
+    request_rec *r = ap_lua_check_request_rec(L, 1);
+    plaintext = ap_lua_ssl_is_https(r->connection) ? 0 : 1;
+    
+    if (!plaintext) {
+        ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, 
+                    "Websocket: WSS protocol reading not yet supported!");
+        return 0;
+    }
+    
+    mask_bytes = apr_pcalloc(r->pool, 4);
+    sock = ap_get_conn_socket(r->connection);
+
+    /* Get opcode and FIN bit */
+    rv = apr_socket_recv(sock, &byte, &len);
+    if (rv == APR_SUCCESS) {
+        unsigned char fin, opcode, mask, payload;
+        fin = byte >> 7;
+        opcode = (byte << 4) >> 4;
+        
+        /* Get the payload length and mask bit */
+        rv = apr_socket_recv(sock, &byte, &len);
+        if (rv == APR_SUCCESS) {
+            mask = byte >> 7;
+            payload = byte - 128;
+            plen = payload;
+            
+            /* Extended payload? */
+            if (payload == 126) {
+                len = 2;
+                rv = apr_socket_recv(sock, (char*) &payload_short, &len);
+                payload_short = ntohs(payload_short);
+                
+                if (rv == APR_SUCCESS) {
+                    plen = payload_short;
+                }
+                else {
+                    return 0;
+                }
+            }
+            /* Super duper extended payload? */
+            if (payload == 127) {
+                len = 8;
+                rv = apr_socket_recv(sock, (char*) &payload_long, &len);
+                if (rv == APR_SUCCESS) {
+                    plen = ap_ntoh64(&payload_long);
+                }
+                else {
+                    return 0;
+                }
+            }
+            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+                    "Websocket: Reading %lu (%s) bytes, masking is %s. %s", 
+                    plen,
+                    (payload >= 126) ? "extra payload" : "no extra payload", 
+                    mask ? "on" : "off", 
+                    fin ? "This is a final frame" : "more to follow");
+            if (mask) {
+                len = 4;
+                rv = apr_socket_recv(sock, (char*) mask_bytes, &len);
+                if (rv != APR_SUCCESS) {
+                    return 0;
+                }
+            }
+            if (plen < (HUGE_STRING_LEN*1024) && plen > 0) {
+                apr_size_t remaining = plen;
+                apr_size_t received;
+                apr_off_t at = 0;
+                char *buffer = apr_palloc(r->pool, plen+1);
+                buffer[plen] = 0;
+                
+                if (plaintext) {
+                    while (remaining > 0) {
+                        received = remaining;
+                        rv = apr_socket_recv(sock, buffer+at, &received);
+                        if (received > 0 ) {
+                            remaining -= received;
+                            at += received;
+                        }
+                    }
+                    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, 
+                    "Websocket: Frame contained %lu bytes, pushed to Lua stack", 
+                        at);
+                }
+                else {
+                    at = ap_get_client_block(r, buffer, plen);
+                    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, 
+                    "Websocket: SSL Frame contained %lu bytes, "\
+                            "pushed to Lua stack", 
+                        at);
+                }
+                if (mask) {
+                    for (n = 0; n < plen; n++) {
+                        buffer[n] ^= mask_bytes[n%4];
+                    }
+                }
+                
+                lua_pushlstring(L, buffer, (size_t) plen); /* push string to stack */
+                lua_pushboolean(L, fin); /* push FIN bit to stack as boolean */
+                return 2;
+            }
+            
+            
+            /* Decide if we need to react to the opcode or not */
+            if (opcode == 0x09) { /* ping */
+                char frame[2];
+                plen = 2;
+                frame[0] = 0x8A;
+                frame[1] = 0;
+                apr_socket_send(sock, frame, &plen); /* Pong! */
+                lua_websocket_read(L); /* read the next frame instead */
+            }
+        }
+    }
+    return 0;
+}
+
+
+static int lua_websocket_write(lua_State *L) 
+{
+    const char *string;
+    apr_status_t rv;
+    size_t len;
+    int raw = 0;
+    char prelude;
+    request_rec *r = ap_lua_check_request_rec(L, 1);
+    
+    if (lua_isboolean(L, 3)) {
+        raw = lua_toboolean(L, 3);
+    }
+    string = lua_tolstring(L, 2, &len);
+    
+    if (raw != 1) {
+        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+                        "Websocket: Writing framed message to client");
+        
+        prelude = 0x81; /* text frame, FIN */
+        ap_rputc(prelude, r);
+        if (len < 126) {
+            ap_rputc(len, r);
+        } 
+        else if (len < 65535) {
+            unsigned short c = len;
+            ap_rputc(126, r); 
+            c = htons(c);
+            ap_rwrite((char*) &c, 2, r);
+        }
+        else {
+            apr_uint64_t llen = len;
+            ap_rputc(127, r);
+            llen = ap_ntoh64(&len); /* ntoh doubles as hton */
+            ap_rwrite((char*) &llen, 8, r);
+        }
+    }
+    else {
+        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+                        "Websocket: Writing raw message to client");
+    }
+    ap_rwrite(string, len, r);
+    rv = ap_rflush(r);
+    if (rv == APR_SUCCESS) {
+        lua_pushboolean(L, 1);
+    }
+    else {
+        lua_pushboolean(L, 0);
+    }
+    return 1;
+}
+
+
+static int lua_websocket_close(lua_State *L) 
+{
+    apr_socket_t *sock;
+    apr_size_t plen;
+    char prelude[2];
+    request_rec *r = ap_lua_check_request_rec(L, 1);
+    sock = ap_get_conn_socket(r->connection);
+    
+    /* Send a header that says: socket is closing. */
+    prelude[0] = 0x88; /* closing socket opcode */
+    prelude[1] = 0; /* zero length frame */
+    plen = 2;
+    apr_socket_send(sock, prelude, &plen);
+    
+    /* Close up tell the MPM and filters to back off */
+    apr_socket_close(sock);
+    r->output_filters = NULL;
+    r->connection->keepalive = AP_CONN_CLOSE;
+    return 0;
+}
+
+static int lua_websocket_ping(lua_State *L) 
+{
+    apr_socket_t *sock;
+    apr_size_t plen;
+    char prelude[2];
+    apr_status_t rv;
+    request_rec *r = ap_lua_check_request_rec(L, 1);
+    sock = ap_get_conn_socket(r->connection);
+    
+    /* Send a header that says: PING. */
+    prelude[0] = 0x89; /* ping  opcode */
+    prelude[1] = 0;
+    plen = 2;
+    apr_socket_send(sock, prelude, &plen);
+    
+    
+    /* Get opcode and FIN bit from pong */
+    plen = 2;
+    rv = apr_socket_recv(sock, prelude, &plen);
+    if (rv == APR_SUCCESS) {
+        unsigned char opcode = prelude[0];
+        unsigned char len = prelude[1];
+        unsigned char mask = len >> 7;
+        if (mask) len -= 128;
+        plen = len;
+        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+                        "Websocket: Got PONG opcode: %x", opcode);
+        if (opcode == 0x8A) {
+            lua_pushboolean(L, 1);
+        }
+        else {
+            lua_pushboolean(L, 0);
+        }
+        if (plen > 0) {
+            ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, 
+                        "Websocket: Reading %lu bytes of PONG", plen);
+            return 1;
+        }
+        if (mask) {
+            plen = 2;
+            apr_socket_recv(sock, prelude, &plen);
+            plen = 2;
+            apr_socket_recv(sock, prelude, &plen);
+        }
+    }
+    else {
+        lua_pushboolean(L, 0);
+    }
+    return 1;
+}
+
 
 #define APLUA_REQ_TRACE(lev) static int req_trace##lev(lua_State *L)  \
 {                                                               \
@@ -2285,6 +2600,16 @@ void ap_lua_load_request_lmodule(lua_State *L, apr_pool_t *p)
                  makefun(&lua_get_cookie, APL_REQ_FUNTYPE_LUACFUN, p));
     apr_hash_set(dispatch, "setcookie", APR_HASH_KEY_STRING,
                  makefun(&lua_set_cookie, APL_REQ_FUNTYPE_LUACFUN, p));
+    apr_hash_set(dispatch, "wsupgrade", APR_HASH_KEY_STRING,
+                 makefun(&lua_websocket_greet, APL_REQ_FUNTYPE_LUACFUN, p));
+    apr_hash_set(dispatch, "wsread", APR_HASH_KEY_STRING,
+                 makefun(&lua_websocket_read, APL_REQ_FUNTYPE_LUACFUN, p));
+    apr_hash_set(dispatch, "wswrite", APR_HASH_KEY_STRING,
+                 makefun(&lua_websocket_write, APL_REQ_FUNTYPE_LUACFUN, p));
+    apr_hash_set(dispatch, "wsclose", APR_HASH_KEY_STRING,
+                 makefun(&lua_websocket_close, APL_REQ_FUNTYPE_LUACFUN, p));
+    apr_hash_set(dispatch, "wsping", APR_HASH_KEY_STRING,
+                 makefun(&lua_websocket_ping, APL_REQ_FUNTYPE_LUACFUN, p));
     
     lua_pushlightuserdata(L, dispatch);
     lua_setfield(L, LUA_REGISTRYINDEX, "Apache2.Request.dispatch");