From 731d18dd4c0684d30fdab327c5c052b141421cc6 Mon Sep 17 00:00:00 2001 From: Daniel Gruno Date: Wed, 11 Sep 2013 20:50:46 +0000 Subject: [PATCH] mod_lua: Add rudimentary WebSocket support. This is a WIP (emphasis on the W, I and P) and subject to change as the idea surrounding it evolves into something meaningful. But for now, WebSockets, yay! Please do review this! git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1522030 13f79535-47bb-0310-9956-ffa450edef68 --- CHANGES | 3 + docs/manual/mod/mod_lua.xml | 33 ++++ modules/lua/lua_request.c | 329 +++++++++++++++++++++++++++++++++++- 3 files changed, 363 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 3618e48247..0d897df033 100644 --- 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] diff --git a/docs/manual/mod/mod_lua.xml b/docs/manual/mod/mod_lua.xml index 3488d417ac..bc19549e6b 100644 --- a/docs/manual/mod/mod_lua.xml +++ b/docs/manual/mod/mod_lua.xml @@ -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) + +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 + + + +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) + + + +r:wswrite(line) -- Writes a frame to a WebSocket client: +r:wswrite("Hello, world!") + + + +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 + +
Logging Functions diff --git a/modules/lua/lua_request.c b/modules/lua/lua_request.c index e1a2bc7236..0b6a306a5a 100644 --- a/modules/lua/lua_request.c +++ b/modules/lua/lua_request.c @@ -28,8 +28,7 @@ #include "apr_thread_mutex.h" #include "apr_tables.h" #include "util_cookies.h" - -#include +#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"); -- 2.40.0