diff --git a/changes-entries/lua-fixes.txt b/changes-entries/lua-fixes.txt new file mode 100644 index 00000000000..28b68f80a5a --- /dev/null +++ b/changes-entries/lua-fixes.txt @@ -0,0 +1,3 @@ + *) mod_lua: Fix websocket frame handling in r:wsread() and r:wspeek(), + r:requestbody() size limits, r:regex() with 25 capture groups and + r:set_document_root(). [Joe Orton] diff --git a/changes-entries/lua-wsread-memory.txt b/changes-entries/lua-wsread-memory.txt new file mode 100644 index 00000000000..3a87227b360 --- /dev/null +++ b/changes-entries/lua-wsread-memory.txt @@ -0,0 +1,2 @@ + *) mod_lua: Cap memory use of r:wsread(), r:requestbody(), r:regex() + and other request methods. [Joe Orton] diff --git a/modules/lua/lua_request.c b/modules/lua/lua_request.c index 6843679006b..19599b5e655 100644 --- a/modules/lua/lua_request.c +++ b/modules/lua/lua_request.c @@ -28,6 +28,7 @@ #include "apr_thread_mutex.h" #include "apr_tables.h" #include "util_cookies.h" +#include "util_charset.h" #define APR_WANT_BYTEFUNC #include "apr_want.h" @@ -42,8 +43,14 @@ APLOG_USE_MODULE(lua); #define MODLUA_MAX_REG_MATCH 25 #endif +/* The longest string r:base64_encode() and r:base64_decode() accept. */ +#ifndef MODLUA_MAX_BASE64 +#define MODLUA_MAX_BASE64 (HUGE_STRING_LEN*1024) +#endif + typedef char *(*req_field_string_f) (request_rec * r); typedef int (*req_field_int_f) (request_rec * r); +typedef apr_int64_t (*req_field_int64_f) (request_rec * r); typedef req_table_t *(*req_field_apr_table_f) (request_rec * r); @@ -230,6 +237,8 @@ static int req_aprtable2luatable_cb_len(void *l, const char *key, requests. Used for multipart POST data. ======================================================================================================================= */ +/* Reads the request body into *rbuf. Returns OK, DONE if there is + * no body, or an HTTP status. */ static int lua_read_body(request_rec *r, const char **rbuf, apr_off_t *size, apr_off_t maxsize) { @@ -244,24 +253,40 @@ static int lua_read_body(request_rec *r, const char **rbuf, apr_off_t *size, if (ap_should_client_block(r)) { /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ - apr_off_t len_read = -1; - apr_off_t rpos = 0; + char argsbuffer[HUGE_STRING_LEN]; + long len_read; apr_off_t length = r->remaining; + apr_size_t total; + apr_bucket_brigade *bb; + char *buf; /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ if (maxsize != 0 && length > maxsize) { - return APR_EINCOMPLETE; /* Only room for incomplete data chunk :( */ + return HTTP_REQUEST_ENTITY_TOO_LARGE; + } + /* The body and its NUL must fit in an apr_size_t. */ + if ((apr_uint64_t) length >= APR_SIZE_MAX) { + return HTTP_REQUEST_ENTITY_TOO_LARGE; } - *rbuf = (const char *) apr_pcalloc(r->pool, (apr_size_t) (length) + 1); - while ((rpos < length) - && (len_read = ap_get_client_block(r, (char *) *rbuf + rpos, - length - rpos)) > 0) { - rpos += len_read; + + /* Collect the body as it arrives rather than allocating the + * Content-Length up front. */ + bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + while ((len_read = ap_get_client_block(r, argsbuffer, + sizeof(argsbuffer))) > 0) { + apr_brigade_write(bb, NULL, NULL, argsbuffer, len_read); } if (len_read < 0) { - return APR_EINCOMPLETE; + apr_brigade_destroy(bb); + return HTTP_BAD_REQUEST; } - *size = rpos; + apr_brigade_length(bb, 1, size); + total = (apr_size_t) *size; + buf = apr_palloc(r->pool, total + 1); + apr_brigade_flatten(bb, buf, &total); + buf[total] = '\0'; + apr_brigade_destroy(bb); + *rbuf = buf; } else { rc = DONE; @@ -277,9 +302,12 @@ static int lua_read_body(request_rec *r, const char **rbuf, apr_off_t *size, * and writes to a file. * ======================================================================================================================= */ -static apr_status_t lua_write_body(request_rec *r, apr_file_t *file, apr_off_t *size) +/* Stores the request body in the file filename. Returns OK, DONE if + * there is no body, or an HTTP status. */ +static int lua_write_body(request_rec *r, const char *filename, + apr_off_t *size, apr_off_t maxsize) { - apr_status_t rc = OK; + int rc; *size = 0; @@ -291,22 +319,36 @@ static apr_status_t lua_write_body(request_rec *r, apr_file_t *file, apr_off_t * len_read, rpos = 0; apr_off_t length = r->remaining; + apr_file_t *file; - *size = length; + if (maxsize != 0 && length > maxsize) { + return HTTP_REQUEST_ENTITY_TOO_LARGE; + } + if (apr_file_open(&file, filename, + APR_FOPEN_CREATE | APR_FOPEN_WRITE + | APR_FOPEN_TRUNCATE, + APR_FPROT_OS_DEFAULT, r->pool) != APR_SUCCESS) { + return HTTP_INTERNAL_SERVER_ERROR; + } while ((len_read = ap_get_client_block(r, argsbuffer, sizeof(argsbuffer))) > 0) { if ((rpos + len_read) > length) - rsize = (apr_size_t) length - rpos; + rsize = length - rpos; else rsize = len_read; - rc = apr_file_write_full(file, argsbuffer, (apr_size_t) rsize, - NULL); - if (rc != APR_SUCCESS) - return rc; + if (apr_file_write_full(file, argsbuffer, (apr_size_t) rsize, + NULL) != APR_SUCCESS) { + apr_file_close(file); + return HTTP_INTERNAL_SERVER_ERROR; + } rpos += rsize; } + apr_file_close(file); + if (len_read < 0) + return HTTP_BAD_REQUEST; + *size = rpos; } else { rc = DONE; @@ -442,7 +484,7 @@ static int req_parsebody(lua_State *L) size = (apr_size_t) len; buffer = apr_palloc(r->pool, size + 1); apr_brigade_flatten(pair->value, buffer, &size); - buffer[len] = 0; + buffer[size] = 0; req_aprtable2luatable_cb(L, pair->name, buffer); } } @@ -467,40 +509,37 @@ static int lua_ap_requestbody(lua_State *L) if (r) { apr_off_t size; - if (maxSize > 0 && r->remaining > maxSize) { - lua_pushnil(L); - lua_pushliteral(L, "Request body was larger than the permitted size."); - return 2; - } + int rc; if (r->method_number != M_POST && r->method_number != M_PUT) return (0); if (!filename) { const char *data; - if (lua_read_body(r, &data, &size, maxSize) != OK) + rc = lua_read_body(r, &data, &size, maxSize); + if (rc == HTTP_REQUEST_ENTITY_TOO_LARGE) { + lua_pushnil(L); + lua_pushliteral(L, "Request body was larger than the permitted size."); + return 2; + } + if (rc != OK) return (0); lua_pushlstring(L, data, (size_t) size); lua_pushinteger(L, (lua_Integer) size); return (2); } else { - apr_status_t rc; - apr_file_t *file; - - rc = apr_file_open(&file, filename, APR_CREATE | APR_FOPEN_WRITE, - APR_FPROT_OS_DEFAULT, r->pool); + rc = lua_write_body(r, filename, &size, maxSize); lua_settop(L, 0); - if (rc == APR_SUCCESS) { - rc = lua_write_body(r, file, &size); - apr_file_close(file); - if (rc != OK) { - lua_pushboolean(L, 0); - return 1; - } - lua_pushinteger(L, (lua_Integer) size); - return (1); - } else + if (rc == HTTP_REQUEST_ENTITY_TOO_LARGE) { + lua_pushnil(L); + lua_pushliteral(L, "Request body was larger than the permitted size."); + return 2; + } + if (rc != OK) { lua_pushboolean(L, 0); + return 1; + } + lua_pushinteger(L, (lua_Integer) size); return (1); } } @@ -546,12 +585,44 @@ static int req_add_output_filter(lua_State *L) return 0; } +/* Per-request state for the temporary pool of a call from Lua. */ +typedef struct { + apr_pool_t *pool; +} lua_temp_ctx_t; + +static lua_temp_ctx_t *lua_temp_ctx(request_rec *r) +{ + lua_temp_ctx_t *ctx; + + apr_pool_userdata_get((void **)&ctx, "mod_lua_temp", r->pool); + if (ctx == NULL) { + ctx = apr_pcalloc(r->pool, sizeof *ctx); + apr_pool_userdata_setn(ctx, "mod_lua_temp", NULL, r->pool); + } + return ctx; +} + +/* Returns a pool which lasts for the current call of a request method + * from Lua, created on first use. req_call() destroys it when the + * call returns, since a script may call a method repeatedly for the + * life of the request. */ +static apr_pool_t *lua_temp_pool(request_rec *r) +{ + lua_temp_ctx_t *ctx = lua_temp_ctx(r); + + if (ctx->pool == NULL) { + apr_pool_create(&ctx->pool, r->pool); + apr_pool_tag(ctx->pool, "lua_temp"); + } + return ctx->pool; +} + /* wrap ap_construct_url as r:construct_url(String) */ static int req_construct_url(lua_State *L) { request_rec *r = ap_lua_check_request_rec(L, 1); const char *name = luaL_checkstring(L, 2); - lua_pushstring(L, ap_construct_url(r->pool, name, r)); + lua_pushstring(L, ap_construct_url(lua_temp_pool(r), name, r)); return 1; } @@ -560,7 +631,7 @@ static int req_escape_html(lua_State *L) { request_rec *r = ap_lua_check_request_rec(L, 1); const char *s = luaL_checkstring(L, 2); - lua_pushstring(L, ap_escape_html(r->pool, s)); + lua_pushstring(L, ap_escape_html(lua_temp_pool(r), s)); return 1; } @@ -706,49 +777,50 @@ static int req_assbackwards_field(request_rec *r) return r->assbackwards; } +/* Returns the req_table_t for table t, created once per request and + * kept as r->pool userdata under key, since a script may read the + * field repeatedly for the life of the request. */ +static req_table_t *req_table(request_rec *r, apr_table_t *t, + const char *key, const char *n) +{ + req_table_t *rt; + + apr_pool_userdata_get((void **)&rt, key, r->pool); + if (rt == NULL || rt->t != t) { + rt = apr_palloc(r->pool, sizeof *rt); + rt->r = r; + rt->t = t; + rt->n = n; + apr_pool_userdata_setn(rt, key, NULL, r->pool); + } + return rt; +} + static req_table_t* req_headers_in(request_rec *r) { - req_table_t* t = apr_palloc(r->pool, sizeof(req_table_t)); - t->r = r; - t->t = r->headers_in; - t->n = "headers_in"; - return t; + return req_table(r, r->headers_in, "mod_lua_headers_in", "headers_in"); } static req_table_t* req_headers_out(request_rec *r) { - req_table_t* t = apr_palloc(r->pool, sizeof(req_table_t)); - t->r = r; - t->t = r->headers_out; - t->n = "headers_out"; - return t; + return req_table(r, r->headers_out, "mod_lua_headers_out", "headers_out"); } static req_table_t* req_err_headers_out(request_rec *r) { - req_table_t* t = apr_palloc(r->pool, sizeof(req_table_t)); - t->r = r; - t->t = r->err_headers_out; - t->n = "err_headers_out"; - return t; + return req_table(r, r->err_headers_out, "mod_lua_err_headers_out", + "err_headers_out"); } static req_table_t* req_subprocess_env(request_rec *r) { - req_table_t* t = apr_palloc(r->pool, sizeof(req_table_t)); - t->r = r; - t->t = r->subprocess_env; - t->n = "subprocess_env"; - return t; + return req_table(r, r->subprocess_env, "mod_lua_subprocess_env", + "subprocess_env"); } static req_table_t* req_notes(request_rec *r) { - req_table_t* t = apr_palloc(r->pool, sizeof(req_table_t)); - t->r = r; - t->t = r->notes; - t->n = "notes"; - return t; + return req_table(r, r->notes, "mod_lua_notes", "notes"); } static int req_ssl_is_https_field(request_rec *r) @@ -773,16 +845,42 @@ static int lua_ap_rflush (lua_State *L) { } +/* A string built from a set of flags, kept as r->pool userdata under + * key and rebuilt only when the flags change, since a script may read + * the field repeatedly for the life of the request. */ +typedef struct { + int flags; + const char *s; +} req_flags_string_t; + +static req_flags_string_t *req_flags_string(request_rec *r, const char *key) +{ + req_flags_string_t *fs; + + apr_pool_userdata_get((void **)&fs, key, r->pool); + if (fs == NULL) { + fs = apr_pcalloc(r->pool, sizeof *fs); + apr_pool_userdata_setn(fs, key, NULL, r->pool); + } + return fs; +} + static const char* lua_ap_options(request_rec* r) { int opts; + req_flags_string_t *fs = req_flags_string(r, "mod_lua_options"); opts = ap_allow_options(r); - return apr_psprintf(r->pool, "%s %s %s %s %s %s", (opts&OPT_INDEXES) ? "Indexes" : "", (opts&OPT_INCLUDES) ? "Includes" : "", (opts&OPT_SYM_LINKS) ? "FollowSymLinks" : "", (opts&OPT_EXECCGI) ? "ExecCGI" : "", (opts&OPT_MULTI) ? "MultiViews" : "", (opts&OPT_ALL) == OPT_ALL ? "All" : "" ); + if (fs->s == NULL || fs->flags != opts) { + fs->flags = opts; + fs->s = apr_psprintf(r->pool, "%s %s %s %s %s %s", (opts&OPT_INDEXES) ? "Indexes" : "", (opts&OPT_INCLUDES) ? "Includes" : "", (opts&OPT_SYM_LINKS) ? "FollowSymLinks" : "", (opts&OPT_EXECCGI) ? "ExecCGI" : "", (opts&OPT_MULTI) ? "MultiViews" : "", (opts&OPT_ALL) == OPT_ALL ? "All" : "" ); + } + return fs->s; } static const char* lua_ap_allowoverrides(request_rec* r) { int opts; + req_flags_string_t *fs; opts = ap_allow_overrides(r); if ( (opts & OR_ALL) == OR_ALL) { return "All"; @@ -790,13 +888,17 @@ static const char* lua_ap_allowoverrides(request_rec* r) else if (opts == OR_NONE) { return "None"; } - return apr_psprintf(r->pool, "%s %s %s %s %s", (opts & OR_LIMIT) ? "Limit" : "", (opts & OR_OPTIONS) ? "Options" : "", (opts & OR_FILEINFO) ? "FileInfo" : "", (opts & OR_AUTHCFG) ? "AuthCfg" : "", (opts & OR_INDEXES) ? "Indexes" : "" ); - + fs = req_flags_string(r, "mod_lua_allowoverrides"); + if (fs->s == NULL || fs->flags != opts) { + fs->flags = opts; + fs->s = apr_psprintf(r->pool, "%s %s %s %s %s", (opts & OR_LIMIT) ? "Limit" : "", (opts & OR_OPTIONS) ? "Options" : "", (opts & OR_FILEINFO) ? "FileInfo" : "", (opts & OR_AUTHCFG) ? "AuthCfg" : "", (opts & OR_INDEXES) ? "Indexes" : "" ); + } + return fs->s; } -static int lua_ap_started(request_rec* r) +static apr_int64_t lua_ap_started(request_rec* r) { - return (int)(ap_scoreboard_image->global->restart_time / 1000000); + return apr_time_sec(ap_scoreboard_image->global->restart_time); } static const char* lua_ap_basic_auth_pw(request_rec* r) @@ -806,9 +908,9 @@ static const char* lua_ap_basic_auth_pw(request_rec* r) return pw ? pw : ""; } -static int lua_ap_limit_req_body(request_rec* r) +static apr_int64_t lua_ap_limit_req_body(request_rec* r) { - return (int) ap_get_limit_req_body(r); + return ap_get_limit_req_body(r); } static int lua_ap_is_initial_req(request_rec *r) @@ -827,26 +929,34 @@ static int lua_ap_sendfile(lua_State *L) apr_finfo_t file_info; const char *filename; request_rec *r; + apr_pool_t *p; luaL_checktype(L, 1, LUA_TUSERDATA); luaL_checktype(L, 2, LUA_TSTRING); r = ap_lua_check_request_rec(L, 1); filename = lua_tostring(L, 2); - apr_stat(&file_info, filename, APR_FINFO_MIN, r->pool); - if (file_info.filetype == APR_NOFILE || file_info.filetype == APR_DIR) { + p = lua_temp_pool(r); + if (apr_stat(&file_info, filename, APR_FINFO_MIN, p) != APR_SUCCESS + || file_info.filetype == APR_NOFILE || file_info.filetype == APR_DIR) { lua_pushboolean(L, 0); } else { - apr_size_t sent; apr_status_t rc; apr_file_t *file; - rc = apr_file_open(&file, filename, APR_READ, APR_OS_DEFAULT, - r->pool); + rc = apr_file_open(&file, filename, APR_READ, APR_OS_DEFAULT, p); if (rc == APR_SUCCESS) { - ap_send_fd(file, r, 0, (apr_size_t)file_info.size, &sent); - apr_file_close(file); - lua_pushinteger(L, sent); + apr_bucket_brigade *bb; + + /* As ap_send_fd(), but with the brigade in the call pool. + * A filter which sets the file aside moves it to its own + * pool, so destroying this one does not close it. */ + bb = apr_brigade_create(p, r->connection->bucket_alloc); + apr_brigade_insert_file(bb, file, 0, file_info.size, p); + rc = ap_pass_brigade(r->output_filters, bb); + apr_brigade_cleanup(bb); + lua_pushinteger(L, rc == APR_SUCCESS + ? (lua_Integer) file_info.size : 0); } else { lua_pushboolean(L, 0); @@ -871,9 +981,10 @@ static int lua_apr_b64encode(lua_State *L) r = ap_lua_check_request_rec(L, 1); luaL_checktype(L, 2, LUA_TSTRING); plain = lua_tolstring(L, 2, &plain_len); + luaL_argcheck(L, plain_len <= MODLUA_MAX_BASE64, 2, "string too long"); encoded_len = apr_base64_encode_len(plain_len); if (encoded_len) { - encoded = apr_palloc(r->pool, encoded_len); + encoded = apr_palloc(lua_temp_pool(r), encoded_len); encoded_len = apr_base64_encode(encoded, plain, plain_len); if (encoded_len > 0 && encoded[encoded_len - 1] == '\0') encoded_len--; @@ -896,9 +1007,11 @@ static int lua_apr_b64decode(lua_State *L) r = ap_lua_check_request_rec(L, 1); luaL_checktype(L, 2, LUA_TSTRING); encoded = lua_tolstring(L, 2, &encoded_len); + luaL_argcheck(L, encoded_len <= MODLUA_MAX_BASE64, 2, + "string too long"); decoded_len = apr_base64_decode_len(encoded); if (decoded_len) { - plain = apr_palloc(r->pool, decoded_len); + plain = apr_palloc(lua_temp_pool(r), decoded_len); decoded_len = apr_base64_decode(plain, encoded); if (decoded_len > 0 && plain[decoded_len - 1] == '\0') decoded_len--; @@ -921,7 +1034,7 @@ static int lua_ap_unescape(lua_State *L) r = ap_lua_check_request_rec(L, 1); luaL_checktype(L, 2, LUA_TSTRING); escaped = lua_tolstring(L, 2, &x); - plain = apr_pstrdup(r->pool, escaped); + plain = apr_pstrdup(lua_temp_pool(r), escaped); y = ap_unescape_urlencoded(plain); if (!y) { lua_pushstring(L, plain); @@ -942,7 +1055,7 @@ static int lua_ap_escape(lua_State *L) r = ap_lua_check_request_rec(L, 1); luaL_checktype(L, 2, LUA_TSTRING); plain = lua_tolstring(L, 2, &x); - escaped = ap_escape_urlencoded(r->pool, plain); + escaped = ap_escape_urlencoded(lua_temp_pool(r), plain); lua_pushstring(L, escaped); return 1; } @@ -952,15 +1065,24 @@ static int lua_ap_escape(lua_State *L) */ static int lua_apr_md5(lua_State *L) { + unsigned char digest[APR_MD5_DIGESTSIZE]; + apr_md5_ctx_t md5; const char *buffer; - char *result; + char result[APR_MD5_DIGESTSIZE * 2 + 1]; size_t len; - request_rec *r; - r = ap_lua_check_request_rec(L, 1); + ap_lua_check_request_rec(L, 1); luaL_checktype(L, 2, LUA_TSTRING); buffer = lua_tolstring(L, 2, &len); - result = ap_md5_binary(r->pool, (const unsigned char *)buffer, len); + /* As ap_md5_binary(), which takes an int length. */ + apr_md5_init(&md5); +#if APR_CHARSET_EBCDIC + apr_md5_set_xlate(&md5, ap_hdrs_to_ascii); +#endif + apr_md5_update(&md5, buffer, len); + apr_md5_final(digest, &md5); + + ap_bin2hex(digest, sizeof(digest), result); lua_pushstring(L, result); return 1; } @@ -973,16 +1095,21 @@ static int lua_apr_sha1(lua_State *L) unsigned char digest[APR_SHA1_DIGESTSIZE]; apr_sha1_ctx_t sha1; const char *buffer; - char *result; + char result[APR_SHA1_DIGESTSIZE * 2 + 1]; size_t len; - request_rec *r; - r = ap_lua_check_request_rec(L, 1); + ap_lua_check_request_rec(L, 1); luaL_checktype(L, 2, LUA_TSTRING); - result = apr_pcalloc(r->pool, sizeof(digest) * 2 + 1); buffer = lua_tolstring(L, 2, &len); apr_sha1_init(&sha1); - apr_sha1_update(&sha1, buffer, len); + /* apr_sha1_update() takes an unsigned int length. */ + while (len > 0) { + unsigned int n = len > UINT_MAX ? UINT_MAX : (unsigned int) len; + + apr_sha1_update(&sha1, buffer, n); + buffer += n; + len -= n; + } apr_sha1_final(digest, &sha1); ap_bin2hex(digest, sizeof(digest), result); @@ -1001,11 +1128,11 @@ static int lua_apr_htpassword(lua_State *L) r = ap_lua_check_request_rec(L, 1); luaL_checktype(L, 2, LUA_TSTRING); - ctx.passwd = apr_pstrdup(r->pool, lua_tostring(L, 2)); ctx.alg = luaL_optinteger(L, 3, ALG_APMD5); ctx.cost = luaL_optinteger(L, 4, 0); - ctx.pool = r->pool; - ctx.out = apr_pcalloc(r->pool, MAX_PASSWD_LEN); + ctx.pool = lua_temp_pool(r); + ctx.passwd = apr_pstrdup(ctx.pool, lua_tostring(L, 2)); + ctx.out = apr_pcalloc(ctx.pool, MAX_PASSWD_LEN); ctx.out_len = MAX_PASSWD_LEN; if (mk_password_hash(&ctx)) { lua_pushboolean(L, 0); @@ -1118,7 +1245,8 @@ static int lua_ap_mpm_query(lua_State *L) y; x = lua_tointeger(L, 1); - ap_mpm_query(x, &y); + if (ap_mpm_query(x, &y) != APR_SUCCESS) + return 0; lua_pushinteger(L, y); return 1; } @@ -1133,6 +1261,7 @@ static int lua_ap_expr(lua_State *L) const char *expr, *err; ap_expr_info_t res; + apr_pool_t *p; luaL_checktype(L, 1, LUA_TUSERDATA); luaL_checktype(L, 2, LUA_TSTRING); @@ -1145,7 +1274,8 @@ static int lua_ap_expr(lua_State *L) res.line_number = 0; res.module_index = APLOG_MODULE_INDEX; - err = ap_expr_parse(r->pool, r->pool, &res, expr, NULL); + p = lua_temp_pool(r); + err = ap_expr_parse(p, p, &res, expr, NULL); if (!err) { x = ap_expr_exec(r, &res, &err); lua_pushboolean(L, x); @@ -1170,20 +1300,19 @@ static int lua_ap_expr(lua_State *L) */ static int lua_ap_regex(lua_State *L) { - request_rec *r; int i, rv, flags; const char *pattern, *source; - char *err; + char err[256]; ap_regex_t regex; ap_regmatch_t matches[MODLUA_MAX_REG_MATCH+1]; luaL_checktype(L, 1, LUA_TUSERDATA); luaL_checktype(L, 2, LUA_TSTRING); luaL_checktype(L, 3, LUA_TSTRING); - r = ap_lua_check_request_rec(L, 1); + ap_lua_check_request_rec(L, 1); source = lua_tostring(L, 2); pattern = lua_tostring(L, 3); flags = luaL_optinteger(L, 4, 0); @@ -1191,24 +1320,24 @@ static int lua_ap_regex(lua_State *L) rv = ap_regcomp(®ex, pattern, flags); if (rv) { lua_pushboolean(L, 0); - err = apr_palloc(r->pool, 256); - ap_regerror(rv, ®ex, err, 256); + ap_regerror(rv, ®ex, err, sizeof err); lua_pushstring(L, err); return 2; } if (regex.re_nsub > MODLUA_MAX_REG_MATCH) { lua_pushboolean(L, 0); - err = apr_palloc(r->pool, 64); - apr_snprintf(err, 64, + apr_snprintf(err, sizeof err, "regcomp found %d matches; only %d allowed.", regex.re_nsub, MODLUA_MAX_REG_MATCH); + ap_regfree(®ex); lua_pushstring(L, err); return 2; } - rv = ap_regexec(®ex, source, MODLUA_MAX_REG_MATCH, matches, 0); + rv = ap_regexec(®ex, source, MODLUA_MAX_REG_MATCH+1, matches, 0); if (rv == AP_REG_NOMATCH) { + ap_regfree(®ex); lua_pushboolean(L, 0); return 1; } @@ -1217,14 +1346,14 @@ static int lua_ap_regex(lua_State *L) for (i = 0; i <= regex.re_nsub; i++) { lua_pushinteger(L, i); if (matches[i].rm_so >= 0 && matches[i].rm_eo >= 0) - lua_pushstring(L, - apr_pstrndup(r->pool, source + matches[i].rm_so, - matches[i].rm_eo - matches[i].rm_so)); + lua_pushlstring(L, source + matches[i].rm_so, + matches[i].rm_eo - matches[i].rm_so); else lua_pushnil(L); lua_settable(L, -3); } + ap_regfree(®ex); return 1; } @@ -1294,7 +1423,7 @@ static int lua_ap_scoreboard_process(lua_State *L) static int lua_ap_scoreboard_worker(lua_State *L) { int i, j; - worker_score *ws_record = NULL; + worker_score ws_buf, *ws_record = &ws_buf; request_rec *r = NULL; luaL_checktype(L, 1, LUA_TUSERDATA); @@ -1306,10 +1435,8 @@ static int lua_ap_scoreboard_worker(lua_State *L) i = lua_tointeger(L, 2); j = lua_tointeger(L, 3); - ws_record = apr_palloc(r->pool, sizeof *ws_record); - - ap_copy_scoreboard_worker(ws_record, i, j); - if (ws_record) { + if (ap_get_scoreboard_worker_from_indexes(i, j) != NULL) { + ap_copy_scoreboard_worker(ws_record, i, j); lua_newtable(L); lua_pushstring(L, "access_count"); @@ -1461,7 +1588,7 @@ static int lua_ap_runtime_dir_relative(lua_State *L) luaL_checktype(L, 1, LUA_TUSERDATA); r = ap_lua_check_request_rec(L, 1); file = luaL_optstring(L, 2, "."); - lua_pushstring(L, ap_runtime_dir_relative(r->pool, file)); + lua_pushstring(L, ap_runtime_dir_relative(lua_temp_pool(r), file)); return 1; } @@ -1478,7 +1605,7 @@ static int lua_ap_set_document_root(lua_State *L) luaL_checktype(L, 2, LUA_TSTRING); r = ap_lua_check_request_rec(L, 1); root = lua_tostring(L, 2); - ap_set_document_root(r, root); + ap_set_document_root(r, apr_pstrdup(r->pool, root)); return 0; } @@ -1498,7 +1625,7 @@ static int lua_ap_getdir(lua_State *L) luaL_checktype(L, 2, LUA_TSTRING); r = ap_lua_check_request_rec(L, 1); directory = lua_tostring(L, 2); - if (apr_dir_open(&thedir, directory, r->pool) == APR_SUCCESS) { + if (apr_dir_open(&thedir, directory, lua_temp_pool(r)) == APR_SUCCESS) { int i = 0; lua_newtable(L); do { @@ -1645,7 +1772,8 @@ static int lua_ap_set_context_info(lua_State *L) prefix = lua_tostring(L, 2); luaL_checktype(L, 3, LUA_TSTRING); document_root = lua_tostring(L, 3); - ap_set_context_info(r, prefix, document_root); + ap_set_context_info(r, apr_pstrdup(r->pool, prefix), + apr_pstrdup(r->pool, document_root)); return 0; } @@ -1671,7 +1799,7 @@ static int lua_ap_os_escape_path(lua_State *L) path = lua_tostring(L, 2); if (lua_isboolean(L, 3)) partial = lua_toboolean(L, 3); - returnValue = ap_os_escape_path(r->pool, path, partial); + returnValue = ap_os_escape_path(lua_temp_pool(r), path, partial); lua_pushstring(L, returnValue); return 1; } @@ -1693,7 +1821,7 @@ static int lua_ap_escape_logitem(lua_State *L) r = ap_lua_check_request_rec(L, 1); luaL_checktype(L, 2, LUA_TSTRING); str = lua_tostring(L, 2); - returnValue = ap_escape_logitem(r->pool, str); + returnValue = ap_escape_logitem(lua_temp_pool(r), str); lua_pushstring(L, returnValue); return 1; } @@ -1868,6 +1996,27 @@ static int lua_ap_usleep(lua_State *L) /* END dispatch methods for request_rec fields */ +/* Calls the request method in upvalue 1 for the request in upvalue 2, + * then destroys any temporary pool it used. The pool of an outer call + * is restored afterwards, since a method can run filters which call + * back into Lua for the same request. */ +static int req_call(lua_State *L) +{ + const req_fun_t *rft = lua_touserdata(L, lua_upvalueindex(1)); + request_rec *r = lua_touserdata(L, lua_upvalueindex(2)); + lua_temp_ctx_t *ctx = lua_temp_ctx(r); + apr_pool_t *outer = ctx->pool; + int n; + + ctx->pool = NULL; + n = ((lua_CFunction)rft->fun)(L); + if (ctx->pool) { + apr_pool_destroy(ctx->pool); + } + ctx->pool = outer; + return n; +} + static int req_dispatch(lua_State *L) { apr_hash_t *dispatch; @@ -1895,11 +2044,12 @@ static int req_dispatch(lua_State *L) } case APL_REQ_FUNTYPE_LUACFUN:{ - lua_CFunction func = (lua_CFunction)rft->fun; ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01487) "request_rec->dispatching %s -> lua_CFunction", name); - lua_pushcfunction(L, func); + lua_pushlightuserdata(L, rft); + lua_pushlightuserdata(L, r); + lua_pushcclosure(L, req_call, 2); return 1; } case APL_REQ_FUNTYPE_STRING:{ @@ -1920,6 +2070,20 @@ static int req_dispatch(lua_State *L) lua_pushinteger(L, rs); return 1; } + case APL_REQ_FUNTYPE_INT64:{ + req_field_int64_f func = (req_field_int64_f)rft->fun; + apr_int64_t rs; + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, + "request_rec->dispatching %s -> int64", name); + rs = (*func) (r); + /* lua_Integer may be narrower, as with Lua 5.1 on a + * 32-bit platform. */ + if (sizeof(lua_Integer) >= sizeof(rs)) + lua_pushinteger(L, (lua_Integer) rs); + else + lua_pushnumber(L, (lua_Number) rs); + return 1; + } case APL_REQ_FUNTYPE_BOOLEAN:{ req_field_int_f func = (req_field_int_f)rft->fun; int rs; @@ -1994,7 +2158,7 @@ static int lua_ivm_get(lua_State *L) lua_ivm_object *object = NULL; request_rec *r = ap_lua_check_request_rec(L, 1); key = luaL_checkstring(L, 2); - raw_key = apr_pstrcat(r->pool, "lua_ivm_", key, NULL); + raw_key = apr_pstrcat(lua_temp_pool(r), "lua_ivm_", key, NULL); apr_global_mutex_lock(lua_ivm_mutex); pool = *((apr_pool_t**) apr_shm_baseaddr_get(lua_ivm_shm)); apr_pool_userdata_get((void **)&object, raw_key, pool); @@ -2022,7 +2186,7 @@ static int lua_ivm_set(lua_State *L) request_rec *r = ap_lua_check_request_rec(L, 1); key = luaL_checkstring(L, 2); luaL_checkany(L, 3); - raw_key = apr_pstrcat(r->pool, "lua_ivm_", key, NULL); + raw_key = apr_pstrcat(lua_temp_pool(r), "lua_ivm_", key, NULL); apr_global_mutex_lock(lua_ivm_mutex); pool = *((apr_pool_t**) apr_shm_baseaddr_get(lua_ivm_shm)); @@ -2236,41 +2400,47 @@ static int lua_websocket_greet(lua_State *L) static apr_status_t lua_websocket_readbytes(conn_rec* c, apr_bucket_brigade *brigade, - char* buffer, apr_off_t len) + char* buffer, apr_size_t len) { apr_size_t delivered; apr_status_t rv; - rv = ap_get_brigade(c->input_filters, brigade, AP_MODE_READBYTES, - APR_BLOCK_READ, len); - if (rv == APR_SUCCESS) { - delivered = len; - rv = apr_brigade_flatten(brigade, buffer, &delivered); - if ((rv == APR_SUCCESS) && (delivered < len)) { - rv = APR_INCOMPLETE; + /* A READBYTES read may return fewer bytes than asked for. */ + while (len > 0) { + rv = ap_get_brigade(c->input_filters, brigade, AP_MODE_READBYTES, + APR_BLOCK_READ, len); + if (rv == APR_SUCCESS) { + delivered = len; + rv = apr_brigade_flatten(brigade, buffer, &delivered); + } + apr_brigade_cleanup(brigade); + if (rv != APR_SUCCESS) { + return rv; } + if (delivered == 0) { + return APR_EOF; + } + buffer += delivered; + len -= delivered; } - apr_brigade_cleanup(brigade); - return rv; + return APR_SUCCESS; } static int lua_websocket_peek(lua_State *L) { apr_status_t rv; + apr_off_t len = 0; apr_bucket_brigade *brigade; request_rec *r = ap_lua_check_request_rec(L, 1); - brigade = apr_brigade_create(r->connection->pool, - r->connection->bucket_alloc); + brigade = apr_brigade_create(lua_temp_pool(r), r->connection->bucket_alloc); rv = ap_get_brigade(r->connection->input_filters, brigade, - AP_MODE_READBYTES, APR_NONBLOCK_READ, 1); + AP_MODE_SPECULATIVE, APR_NONBLOCK_READ, 1); if (rv == APR_SUCCESS) { - lua_pushboolean(L, 1); - } - else { - lua_pushboolean(L, 0); + rv = apr_brigade_length(brigade, 1, &len); } + lua_pushboolean(L, rv == APR_SUCCESS && len > 0); apr_brigade_cleanup(brigade); return 1; } @@ -2278,131 +2448,131 @@ static int lua_websocket_peek(lua_State *L) static int lua_websocket_read(lua_State *L) { apr_status_t rv; - int do_read = 1; - int n = 0; - apr_size_t plen = 0; + int ret = 0; + apr_size_t n, plen; unsigned short payload_short = 0; apr_uint64_t payload_long = 0; - unsigned char *mask_bytes; - char byte; + unsigned char mask_bytes[4]; + unsigned char header[2]; + apr_pool_t *pool; apr_bucket_brigade *brigade; conn_rec* c; request_rec *r = ap_lua_check_request_rec(L, 1); c = r->connection; - mask_bytes = apr_pcalloc(r->pool, 4); + pool = lua_temp_pool(r); + brigade = apr_brigade_create(pool, c->bucket_alloc); - brigade = apr_brigade_create(r->pool, c->bucket_alloc); - - while (do_read) { - do_read = 0; - /* Get opcode and FIN bit */ - rv = lua_websocket_readbytes(c, brigade, &byte, 1); - if (rv == APR_SUCCESS) { - unsigned char ubyte, fin, opcode, mask, payload; - ubyte = (unsigned char)byte; - /* fin bit is the first bit */ - fin = ubyte >> (CHAR_BIT - 1); - /* opcode is the last four bits (there's 3 reserved bits we don't care about) */ - opcode = ubyte & 0xf; - - /* Get the payload length and mask bit */ - rv = lua_websocket_readbytes(c, brigade, &byte, 1); - if (rv == APR_SUCCESS) { - ubyte = (unsigned char)byte; - /* Mask is the first bit */ - mask = ubyte >> (CHAR_BIT - 1); - /* Payload is the last 7 bits */ - payload = ubyte & 0x7f; - plen = payload; - - /* Extended payload? */ - if (payload == 126) { - rv = lua_websocket_readbytes(c, brigade, - (char*) &payload_short, 2); - - if (rv != APR_SUCCESS) { - return 0; - } - - plen = ntohs(payload_short); - } - /* Super duper extended payload? */ - if (payload == 127) { - rv = lua_websocket_readbytes(c, brigade, - (char*) &payload_long, 8); + for (;;) { + unsigned char fin, opcode, mask; + char *buffer; - if (rv != APR_SUCCESS) { - return 0; - } + /* FIN bit, opcode, mask bit and payload length */ + rv = lua_websocket_readbytes(c, brigade, (char *) header, 2); + if (rv != APR_SUCCESS) { + break; + } + fin = header[0] >> (CHAR_BIT - 1); + /* opcode is the last four bits (there's 3 reserved bits we don't care about) */ + opcode = header[0] & 0xf; + mask = header[1] >> (CHAR_BIT - 1); + plen = header[1] & 0x7f; + + /* Extended payload? */ + if (plen == 126) { + rv = lua_websocket_readbytes(c, brigade, + (char*) &payload_short, 2); + if (rv != APR_SUCCESS) { + break; + } + plen = ntohs(payload_short); + } + /* Super duper extended payload? */ + else if (plen == 127) { + apr_uint64_t len64; - plen = ap_ntoh64(&payload_long); - } - ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(03210) - "Websocket: Reading %" APR_SIZE_T_FMT " (%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) { - rv = lua_websocket_readbytes(c, brigade, - (char*) mask_bytes, 4); - - if (rv != APR_SUCCESS) { - return 0; - } - } - if (plen < (HUGE_STRING_LEN*1024) && plen > 0) { - apr_size_t remaining = plen; - char *buffer = apr_palloc(r->pool, plen+1); - buffer[plen] = 0; - - rv = lua_websocket_readbytes(c, brigade, buffer, remaining); - - if (rv != APR_SUCCESS) { - return 0; - } - - ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, - "Websocket: Frame contained %" APR_SIZE_T_FMT \ - " bytes, pushed to Lua stack", remaining); - if (mask) { - for (n = 0; n < plen; n++) { - buffer[n] ^= mask_bytes[n%4]; - } - } - - lua_pushlstring(L, buffer, (size_t) plen); /* push to stack */ - lua_pushboolean(L, fin); /* push FIN bit to stack as boolean */ - return 2; - } + rv = lua_websocket_readbytes(c, brigade, + (char*) &payload_long, 8); + if (rv != APR_SUCCESS) { + break; + } + len64 = ap_ntoh64(&payload_long); + if (len64 >= HUGE_STRING_LEN*1024) { + break; + } + plen = (apr_size_t) len64; + } + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(03210) + "Websocket: Reading %" APR_SIZE_T_FMT " (%s) bytes, masking is %s. %s", + plen, + ((header[1] & 0x7f) >= 126) ? "extra payload" : "no extra payload", + mask ? "on" : "off", + fin ? "This is a final frame" : "more to follow"); + + /* Every client frame is masked (RFC 6455 5.1), a control frame + * carries at most 125 bytes (5.5), and anything larger than + * HUGE_STRING_LEN*1024 is refused. */ + if (!mask || plen >= HUGE_STRING_LEN*1024 + || ((opcode & 0x8) && plen > 125)) { + break; + } - /* Decide if we need to react to the opcode or not */ - if (opcode == 0x09) { /* ping */ - char frame[2]; - apr_bucket *b; + rv = lua_websocket_readbytes(c, brigade, (char*) mask_bytes, 4); + if (rv != APR_SUCCESS) { + break; + } - frame[0] = 0x8A; - frame[1] = 0; + buffer = apr_palloc(pool, plen + 1); + buffer[plen] = 0; + rv = lua_websocket_readbytes(c, brigade, buffer, plen); + if (rv != APR_SUCCESS) { + break; + } + for (n = 0; n < plen; n++) { + buffer[n] ^= mask_bytes[n%4]; + } - /* Pong! */ - b = apr_bucket_transient_create(frame, 2, c->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(brigade, b); + if (opcode == 0x09) { /* ping */ + apr_bucket *b; - rv = ap_pass_brigade(c->output_filters, brigade); - apr_brigade_cleanup(brigade); + header[0] = 0x8A; + header[1] = (unsigned char) plen; - if (rv != APR_SUCCESS) { - return 0; - } + /* Pong, with the ping's payload */ + b = apr_bucket_transient_create((char *) header, 2, + c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(brigade, b); + b = apr_bucket_transient_create(buffer, plen, c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(brigade, b); + b = apr_bucket_flush_create(c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(brigade, b); - do_read = 1; - } + rv = ap_pass_brigade(c->output_filters, brigade); + apr_brigade_cleanup(brigade); + if (rv != APR_SUCCESS) { + break; } + continue; } + if (opcode == 0x0A) { /* unsolicited pong */ + continue; + } + if (opcode == 0x08) { /* close */ + break; + } + + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "Websocket: Frame contained %" APR_SIZE_T_FMT \ + " bytes, pushed to Lua stack", plen); + + lua_pushlstring(L, buffer, plen); /* push to stack */ + lua_pushboolean(L, fin); /* push FIN bit to stack as boolean */ + ret = 2; + break; } - return 0; + + return ret; } @@ -2618,9 +2788,8 @@ static int req_newindex(lua_State *L) /* helper function for walking config trees */ -static void read_cfg_tree(lua_State *L, request_rec *r, ap_directive_t *rcfg) { +static void read_cfg_tree(lua_State *L, ap_directive_t *rcfg) { int x = 0; - const char* value; ap_directive_t *cfg; lua_newtable(L); @@ -2628,9 +2797,8 @@ static void read_cfg_tree(lua_State *L, request_rec *r, ap_directive_t *rcfg) { x++; lua_pushnumber(L, x); lua_newtable(L); - value = apr_psprintf(r->pool, "%s %s", cfg->directive, cfg->args); lua_pushstring(L, "directive"); - lua_pushstring(L, value); + lua_pushfstring(L, "%s %s", cfg->directive, cfg->args); lua_settable(L, -3); lua_pushstring(L, "file"); lua_pushstring(L, cfg->filename); @@ -2640,7 +2808,7 @@ static void read_cfg_tree(lua_State *L, request_rec *r, ap_directive_t *rcfg) { lua_settable(L, -3); if (cfg->first_child) { lua_pushstring(L, "children"); - read_cfg_tree(L, r, cfg->first_child); + read_cfg_tree(L, cfg->first_child); lua_settable(L, -3); } lua_settable(L, -3); @@ -2648,8 +2816,8 @@ static void read_cfg_tree(lua_State *L, request_rec *r, ap_directive_t *rcfg) { } static int lua_ap_get_config(lua_State *L) { - request_rec *r = ap_lua_check_request_rec(L, 1); - read_cfg_tree(L, r, ap_conftree); + ap_lua_check_request_rec(L, 1); + read_cfg_tree(L, ap_conftree); return 1; } @@ -2666,12 +2834,12 @@ static int lua_ap_get_active_config(lua_State *L) { for (subdir = dir->first_child; subdir; subdir = subdir->next) { if (ap_strcasecmp_match(subdir->directive, "servername") && !ap_strcasecmp_match(r->hostname, subdir->args)) { - read_cfg_tree(L, r, dir->first_child); + read_cfg_tree(L, dir->first_child); return 1; } if (ap_strcasecmp_match(subdir->directive, "serveralias") && !ap_strcasecmp_match(r->hostname, subdir->args)) { - read_cfg_tree(L, r, dir->first_child); + read_cfg_tree(L, dir->first_child); return 1; } } @@ -2860,11 +3028,11 @@ void ap_lua_load_request_lmodule(lua_State *L, apr_pool_t *p) apr_hash_set(dispatch, "allowoverrides", APR_HASH_KEY_STRING, makefun(&lua_ap_allowoverrides, APL_REQ_FUNTYPE_STRING, p)); apr_hash_set(dispatch, "started", APR_HASH_KEY_STRING, - makefun(&lua_ap_started, APL_REQ_FUNTYPE_INT, p)); + makefun(&lua_ap_started, APL_REQ_FUNTYPE_INT64, p)); apr_hash_set(dispatch, "basic_auth_pw", APR_HASH_KEY_STRING, makefun(&lua_ap_basic_auth_pw, APL_REQ_FUNTYPE_STRING, p)); apr_hash_set(dispatch, "limit_req_body", APR_HASH_KEY_STRING, - makefun(&lua_ap_limit_req_body, APL_REQ_FUNTYPE_INT, p)); + makefun(&lua_ap_limit_req_body, APL_REQ_FUNTYPE_INT64, p)); apr_hash_set(dispatch, "server_built", APR_HASH_KEY_STRING, makefun(&ap_get_server_built, APL_REQ_FUNTYPE_STRING, p)); apr_hash_set(dispatch, "is_initial_req", APR_HASH_KEY_STRING, diff --git a/modules/lua/lua_request.h b/modules/lua/lua_request.h index a39f4b97b1b..b9f57536360 100644 --- a/modules/lua/lua_request.h +++ b/modules/lua/lua_request.h @@ -31,6 +31,7 @@ void ap_lua_push_request(lua_State *L, request_rec *r); #define APL_REQ_FUNTYPE_TABLE 3 #define APL_REQ_FUNTYPE_LUACFUN 4 #define APL_REQ_FUNTYPE_BOOLEAN 5 +#define APL_REQ_FUNTYPE_INT64 6 typedef struct { diff --git a/test/modules/lua/__init__.py b/test/modules/lua/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/modules/lua/conftest.py b/test/modules/lua/conftest.py new file mode 100644 index 00000000000..e038529e5d7 --- /dev/null +++ b/test/modules/lua/conftest.py @@ -0,0 +1,35 @@ +import logging +import os + +import pytest +import sys + +from .env import LuaTestEnv + +sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + + +def pytest_report_header(config, start_path): + env = LuaTestEnv() + return f"lua [apache: {env.get_httpd_version()}, mpm: {env.mpm_module}, {env.prefix}]" + + +@pytest.fixture(scope="package") +def env(pytestconfig) -> LuaTestEnv: + level = logging.INFO + console = logging.StreamHandler() + console.setLevel(level) + console.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) + logging.getLogger('').addHandler(console) + logging.getLogger('').setLevel(level=level) + env = LuaTestEnv(pytestconfig=pytestconfig) + env.setup_httpd() + env.apache_access_log_clear() + env.httpd_error_log.clear_log() + return env + + +@pytest.fixture(autouse=True, scope="package") +def _stop_package_scope(env): + yield + assert env.apache_stop() == 0 diff --git a/test/modules/lua/env.py b/test/modules/lua/env.py new file mode 100644 index 00000000000..725083907e5 --- /dev/null +++ b/test/modules/lua/env.py @@ -0,0 +1,27 @@ +import inspect +import logging +import os + +from pyhttpd.env import HttpdTestEnv, HttpdTestSetup + +log = logging.getLogger(__name__) + + +class LuaTestSetup(HttpdTestSetup): + + def __init__(self, env: 'HttpdTestEnv'): + super().__init__(env=env) + self.add_source_dir(os.path.dirname(inspect.getfile(LuaTestSetup))) + # mod_lua needs liblua, so it may not have been built; the tests + # skip when it is absent. + self.add_optional_modules(["lua"]) + + +class LuaTestEnv(HttpdTestEnv): + + def __init__(self, pytestconfig=None): + super().__init__(pytestconfig=pytestconfig) + self.add_httpd_log_modules(["lua"]) + + def setup_httpd(self, setup: HttpdTestSetup = None): + super().setup_httpd(setup=LuaTestSetup(env=self)) diff --git a/test/modules/lua/htdocs/test1/lua/body.lua b/test/modules/lua/htdocs/test1/lua/body.lua new file mode 100644 index 00000000000..518ffb1a6b2 --- /dev/null +++ b/test/modules/lua/htdocs/test1/lua/body.lua @@ -0,0 +1,43 @@ +-- r:requestbody() in several modes: /lua/body.lua? +-- read the body, with its length, md5 and the child's RSS growth +-- readmax the body with a 100 byte limit +-- file the body stored to a file, with its length and md5 +-- filemax the body stored to a file with a 100 byte limit +-- fileover the body stored over the file left by an earlier request + +local function rss() + collectgarbage() + for line in io.lines("/proc/self/status") do + local kb = line:match("^VmRSS:%s+(%d+)") + if kb then return tonumber(kb) end + end + return 0 +end + +function handle(r) + local mode = r.args + local path = r.document_root .. "/lua/upload.out" + local res, extra + r.content_type = "text/plain" + if mode == "read" then + local before = rss() + local body, len = r:requestbody() + local growth = rss() - before + r:puts(tostring(len), " ", body and r:md5(body) or "-", " ", + growth, "\n") + elseif mode == "readmax" then + res = {r:requestbody(nil, 100)} + r:puts(tostring(res[2] and res[1] and #res[1] or res[1]), "|", + tostring(res[2]), "\n") + elseif mode == "file" or mode == "filemax" or mode == "fileover" then + if mode ~= "fileover" then os.remove(path) end + res = {r:requestbody(path, mode == "filemax" and 100 or nil)} + local f = io.open(path, "rb") + extra = f and r:md5(f:read("*a")) or "-" + if f then f:close() end + r:puts(tostring(res[1]), "|", tostring(res[2]), "|", extra, "\n") + else + return 404 + end + return apache2.OK +end diff --git a/test/modules/lua/htdocs/test1/lua/docroot.lua b/test/modules/lua/htdocs/test1/lua/docroot.lua new file mode 100644 index 00000000000..4032f5dc02d --- /dev/null +++ b/test/modules/lua/htdocs/test1/lua/docroot.lua @@ -0,0 +1,38 @@ +-- Sets a document root from a string nothing else refers to, lets Lua +-- collect it, then reads it back. /lua/docroot.lua? +local LEN = 1000 + +local function fresh(tag) + -- A string no other value refers to. + return table.concat({string.rep("x", LEN), tag, tostring(os.time())}) +end + +local function churn() + collectgarbage() + collectgarbage() + local t = {} + for i = 1, 4096 do t[i] = string.rep("y", LEN + 12) .. i end + return t +end + +local setters = { + set_document_root = function(r) + r:set_document_root(fresh("/root")) + local _ = churn() + return r.document_root + end, + set_context_info = function(r) + r:set_context_info(fresh("/prefix"), fresh("/root")) + local _ = churn() + return r.context_prefix .. "|" .. r.context_document_root + end, +} + +function handle(r) + local setter = setters[r.args] + if not setter then return 404 end + local got = setter(r):gsub("%d+", "N") + r.content_type = "text/plain" + r:puts(got, "\n") + return apache2.OK +end diff --git a/test/modules/lua/htdocs/test1/lua/empty b/test/modules/lua/htdocs/test1/lua/empty new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/modules/lua/htdocs/test1/lua/fields.lua b/test/modules/lua/htdocs/test1/lua/fields.lua new file mode 100644 index 00000000000..b1388ee84a7 --- /dev/null +++ b/test/modules/lua/htdocs/test1/lua/fields.lua @@ -0,0 +1,12 @@ +-- Reads and writes request tables through separate accesses of the +-- same field, and prints the options and overrides strings. +function handle(r) + r.notes["lua-a"] = "1" + local n = r.notes + n["lua-b"] = "2" + r.headers_out["X-Lua-Notes"] = r.notes["lua-a"] .. n["lua-b"] + r.headers_out["X-Lua-Host"] = r.headers_in["Host"] + r.content_type = "text/plain" + r:puts(r.options, "|", r.allowoverrides, "\n") + return apache2.OK +end diff --git a/test/modules/lua/htdocs/test1/lua/file.txt b/test/modules/lua/htdocs/test1/lua/file.txt new file mode 100644 index 00000000000..7f34f551b40 --- /dev/null +++ b/test/modules/lua/htdocs/test1/lua/file.txt @@ -0,0 +1,100 @@ +line 0000 +line 0001 +line 0002 +line 0003 +line 0004 +line 0005 +line 0006 +line 0007 +line 0008 +line 0009 +line 0010 +line 0011 +line 0012 +line 0013 +line 0014 +line 0015 +line 0016 +line 0017 +line 0018 +line 0019 +line 0020 +line 0021 +line 0022 +line 0023 +line 0024 +line 0025 +line 0026 +line 0027 +line 0028 +line 0029 +line 0030 +line 0031 +line 0032 +line 0033 +line 0034 +line 0035 +line 0036 +line 0037 +line 0038 +line 0039 +line 0040 +line 0041 +line 0042 +line 0043 +line 0044 +line 0045 +line 0046 +line 0047 +line 0048 +line 0049 +line 0050 +line 0051 +line 0052 +line 0053 +line 0054 +line 0055 +line 0056 +line 0057 +line 0058 +line 0059 +line 0060 +line 0061 +line 0062 +line 0063 +line 0064 +line 0065 +line 0066 +line 0067 +line 0068 +line 0069 +line 0070 +line 0071 +line 0072 +line 0073 +line 0074 +line 0075 +line 0076 +line 0077 +line 0078 +line 0079 +line 0080 +line 0081 +line 0082 +line 0083 +line 0084 +line 0085 +line 0086 +line 0087 +line 0088 +line 0089 +line 0090 +line 0091 +line 0092 +line 0093 +line 0094 +line 0095 +line 0096 +line 0097 +line 0098 +line 0099 diff --git a/test/modules/lua/htdocs/test1/lua/info.lua b/test/modules/lua/htdocs/test1/lua/info.lua new file mode 100644 index 00000000000..5ae6de6a0dc --- /dev/null +++ b/test/modules/lua/htdocs/test1/lua/info.lua @@ -0,0 +1,23 @@ +-- Prints one request field or call result: /lua/info.lua? +local fields = { + limit_req_body = function(r) return r.limit_req_body end, + started = function(r) return r.started end, + mpm_query = function(r) return r.mpm_query(9999) end, + scoreboard_worker = function(r) return r:scoreboard_worker(100000, 0) end, + md5 = function(r) return r:md5("abc") end, + sha1 = function(r) return r:sha1("abc") end, + base64_max = function(r) + local ok = pcall(r.base64_encode, r, string.rep("x", 8 * 1024 * 1024)) + local over = pcall(r.base64_encode, r, string.rep("x", 8 * 1024 * 1024 + 1)) + return tostring(ok) .. "|" .. tostring(over) + end, +} + +function handle(r) + local field = fields[r.args] + if not field then return 404 end + r.content_type = "text/plain" + local value = field(r) + r:puts(tostring(value), "\n") + return apache2.OK +end diff --git a/test/modules/lua/htdocs/test1/lua/mem.lua b/test/modules/lua/htdocs/test1/lua/mem.lua new file mode 100644 index 00000000000..ad24acb34b3 --- /dev/null +++ b/test/modules/lua/htdocs/test1/lua/mem.lua @@ -0,0 +1,70 @@ +-- Runs one r:* call in a loop and reports how much this child's +-- VmRSS grew, in kB. /lua/mem.lua?& +-- +-- Each call should hold on to nothing once it returns, so the growth +-- must not scale with the number of iterations. + +local s = string.rep("a/b &d%e\"f", 1260) -- ~16KB, needs escaping +local b64 = nil +local pct = string.rep("%41", 5461) -- ~16KB URL-escaped +local pattern = string.rep("a", 4000) .. "(b)" -- a large compiled pattern +local path = string.rep("d", 2000) -- under PATH_MAX + +local cases = { + noop = function(r) end, + + -- input-sized results + base64_encode = function(r) r:base64_encode(s) end, + base64_decode = function(r) r:base64_decode(b64) end, + escape = function(r) r:escape(s) end, + unescape = function(r) r:unescape(pct) end, + escape_html = function(r) r:escape_html(s) end, + os_escape_path = function(r) r:os_escape_path(s) end, + escape_logitem = function(r) r:escape_logitem(s) end, + construct_url = function(r) r:construct_url(s) end, + runtime_dir_relative = function(r) r:runtime_dir_relative(path) end, + htpassword = function(r) r:htpassword(s, 1) end, -- ALG_APSHA + expr = function(r) r:expr("'" .. s .. "' == 'x'") end, + regex = function(r) r:regex(s, "^(.*)$") end, + regex_compile = function(r) r:regex("x", pattern) end, + ivm_get = function(r) r:ivm_get(s) end, + ivm_set = function(r) r:ivm_set(s, "v") end, + + -- fixed-size results + md5 = function(r) r:md5("x") end, + sha1 = function(r) r:sha1("x") end, + scoreboard_worker = function(r) r:scoreboard_worker(0, 0) end, + config = function(r) r:config() end, + options = function(r) local _ = r.options end, + allowoverrides = function(r) local _ = r.allowoverrides end, + headers_in = function(r) local _ = r.headers_in end, + get_direntries = function(r) r:get_direntries(r.document_root) end, + sendfile = function(r) r:sendfile(r.document_root .. "/lua/empty") end, + wspeek = function(r) r:wspeek() end, +} + +local function rss() + collectgarbage() + collectgarbage() + for line in io.lines("/proc/self/status") do + local kb = line:match("^VmRSS:%s+(%d+)") + if kb then return tonumber(kb) end + end + return 0 +end + +function handle(r) + local name, count = r.args:match("^([%w_]+)&(%d+)$") + local case = cases[name] + if not case then return 404 end + count = tonumber(count) + b64 = r:base64_encode(string.rep("x", 12288)) + -- One call first, so one-time setup is not counted. + case(r) + local before = rss() + for _ = 1, count do case(r) end + local after = rss() + r.content_type = "text/plain" + r:puts(string.format("%d\n", after - before)) + return apache2.OK +end diff --git a/test/modules/lua/htdocs/test1/lua/regex.lua b/test/modules/lua/htdocs/test1/lua/regex.lua new file mode 100644 index 00000000000..e39091a0d24 --- /dev/null +++ b/test/modules/lua/htdocs/test1/lua/regex.lua @@ -0,0 +1,9 @@ +-- r:regex() with as many capture groups as it accepts: prints the +-- last capture. +function handle(r) + local n = tonumber(r.args) + local m = r:regex(string.rep("x", n), string.rep("(x)", n)) + r.content_type = "text/plain" + r:puts(tostring(m and m[n]), "\n") + return apache2.OK +end diff --git a/test/modules/lua/htdocs/test1/lua/sendfile.lua b/test/modules/lua/htdocs/test1/lua/sendfile.lua new file mode 100644 index 00000000000..aea914c0fab --- /dev/null +++ b/test/modules/lua/htdocs/test1/lua/sendfile.lua @@ -0,0 +1,11 @@ +-- r:sendfile() of a small file, more output after it, then the file +-- again: /lua/sendfile.lua? +function handle(r) + local path = r.document_root .. "/lua/file.txt" + r.content_type = "text/plain" + for _ = 1, tonumber(r.args) do + r:sendfile(path) + r:puts("--\n") + end + return apache2.OK +end diff --git a/test/modules/lua/htdocs/test1/lua/wsread.lua b/test/modules/lua/htdocs/test1/lua/wsread.lua new file mode 100644 index 00000000000..6983ed66cc5 --- /dev/null +++ b/test/modules/lua/htdocs/test1/lua/wsread.lua @@ -0,0 +1,31 @@ +-- The r:wsread() loop from the report. Some text messages get an +-- answer: "sync" is acknowledged; "rss" is answered with this child's +-- VmRSS in kB, after a full Lua GC so that only memory held outside +-- the Lua heap is left; "peek" with what r:wspeek() returns; and +-- "peekread" by waiting for r:wspeek() and echoing the next message. +function rss() + collectgarbage() + for line in io.lines("/proc/self/status") do + local kb = line:match("^VmRSS:%s+(%d+)") + if kb then return kb end + end + return "0" +end + +function handle(r) + if r:wsupgrade() then + while true do + local data = r:wsread() + if data == nil then break end + if data == "sync" then r:wswrite("ok") end + if data == "rss" then r:wswrite(rss()) end + if data == "peek" then r:wswrite(tostring(r:wspeek())) end + if data == "peekread" then + while not r:wspeek() do r.usleep(10000) end + local next = r:wsread() + r:wswrite(next) + end + end + end + return apache2.OK +end diff --git a/test/modules/lua/test_001_websocket.py b/test/modules/lua/test_001_websocket.py new file mode 100644 index 00000000000..27f4ba47f61 --- /dev/null +++ b/test/modules/lua/test_001_websocket.py @@ -0,0 +1,200 @@ +import base64 +import os +import socket +import struct +import textwrap +import time + +import pytest + +from pyhttpd.conf import HttpdConf + + +# Payload per frame, and how many to send: 4096 x 32KiB. Each frame +# is paced by a "sync" round trip and fits in one loopback segment: +# r:wsread() fails on a frame which has only partly arrived. +FRAME_SIZE = 32 * 1024 +FRAME_COUNT = 4096 + + +@pytest.mark.skipif(not os.path.exists("/proc/self/status"), + reason="needs /proc/self/status") +class TestWebsocket: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + env.require(env.has_shared_module("lua"), "mod_lua not built", + dep="mod_lua") + conf = HttpdConf(env, extras={ + 'base': textwrap.dedent(f""" + + SetHandler lua-script + + """), + }) + conf.add_vhost_test1() + conf.install() + assert env.apache_restart() == 0 + + @staticmethod + def frame(payload: bytes, opcode=0x2, masked=True) -> bytes: + # A client frame; an all-zero mask leaves the payload as is. + n = len(payload) + m = 0x80 if masked else 0 + if n < 126: + hdr = struct.pack("!BB", 0x80 | opcode, m | n) + elif n < 65536: + hdr = struct.pack("!BBH", 0x80 | opcode, m | 126, n) + else: + hdr = struct.pack("!BBQ", 0x80 | opcode, m | 127, n) + return hdr + (b"\0\0\0\0" if masked else b"") + payload + + @staticmethod + def recv_exact(sock, n: int) -> bytes: + buf = b"" + while len(buf) < n: + chunk = sock.recv(n - len(buf)) + assert chunk, f"connection closed after {len(buf)}/{n} bytes" + buf += chunk + return buf + + def recv_opcode_frame(self, sock): + b0, b1 = self.recv_exact(sock, 2) + n = b1 & 0x7f + if n == 126: + n, = struct.unpack("!H", self.recv_exact(sock, 2)) + elif n == 127: + n, = struct.unpack("!Q", self.recv_exact(sock, 8)) + return b0 & 0xf, self.recv_exact(sock, n) + + def recv_frame(self, sock) -> bytes: + return self.recv_opcode_frame(sock)[1] + + def sync(self, sock) -> bytes: + sock.sendall(self.frame(b"sync", opcode=0x1)) + return self.recv_frame(sock) + + def rss(self, sock) -> int: + sock.sendall(self.frame(b"rss", opcode=0x1)) + return int(self.recv_frame(sock)) + + def upgrade(self, env): + sock = socket.create_connection((env.http_addr, env.http_port), + timeout=30) + key = base64.b64encode(os.urandom(16)).decode() + sock.sendall((f"GET /lua/wsread.lua HTTP/1.1\r\n" + f"Host: test1.{env.http_tld}:{env.http_port}\r\n" + f"Upgrade: websocket\r\n" + f"Connection: Upgrade\r\n" + f"Sec-WebSocket-Key: {key}\r\n" + f"Sec-WebSocket-Version: 13\r\n\r\n").encode()) + head = b"" + while b"\r\n\r\n" not in head: + chunk = sock.recv(1) + assert chunk, f"connection closed in response head: {head!r}" + head += chunk + assert head.startswith(b"HTTP/1.1 101 "), head + return sock + + # r:wsread() caps its memory use: nothing it allocates for a frame + # outlives the call, so memory use does not scale with the number + # of frames read. + @pytest.mark.skipif("TEST_ASAN" in os.environ, + reason="RSS is not a measure of memory use under ASan") + def test_lua_001_01_wsread_memory(self, env): + sock = self.upgrade(env) + try: + before = self.rss(sock) + payload = b"x" * FRAME_SIZE + data = self.frame(payload) + self.frame(b"sync", opcode=0x1) + for _ in range(FRAME_COUNT): + sock.sendall(data) + assert self.recv_frame(sock) == b"ok" + after = self.rss(sock) + finally: + sock.close() + sent_kb = FRAME_SIZE * FRAME_COUNT // 1024 + growth_kb = after - before + assert growth_kb < sent_kb // 4, \ + f"child RSS grew {growth_kb}kB ({before}kB -> {after}kB) " \ + f"reading {sent_kb}kB of websocket frames" + + # A frame which arrives in two parts is still read whole. + def test_lua_001_02_partial_frame(self, env): + sock = self.upgrade(env) + try: + data = self.frame(b"sync", opcode=0x1) + sock.sendall(data[:-2]) + time.sleep(0.5) + sock.sendall(data[-2:]) + assert self.recv_frame(sock) == b"ok" + finally: + sock.close() + + # A frame larger than one socket read, sent without pacing. + def test_lua_001_03_large_frame(self, env): + sock = self.upgrade(env) + try: + sock.sendall(self.frame(b"x" * 7 * 1024 * 1024)) + assert self.sync(sock) == b"ok" + finally: + sock.close() + + # RFC 6455 5.5.3: a Pong echoes the Ping's application data. + def test_lua_001_04_ping_payload(self, env): + sock = self.upgrade(env) + try: + sock.sendall(self.frame(b"hello", opcode=0x9)) + assert self.recv_opcode_frame(sock) == (0xa, b"hello") + assert self.sync(sock) == b"ok" + finally: + sock.close() + + # An empty message is a message, not the end of the connection. + def test_lua_001_05_empty_frame(self, env): + sock = self.upgrade(env) + try: + sock.sendall(self.frame(b"", opcode=0x1)) + assert self.sync(sock) == b"ok" + finally: + sock.close() + + # RFC 6455 5.1: the server closes the connection on an unmasked + # client frame. + def test_lua_001_06_unmasked_frame(self, env): + sock = self.upgrade(env) + try: + sock.sendall(self.frame(b"sync", opcode=0x1, masked=False)) + sock.settimeout(5) + data = b"" + try: + while True: + chunk = sock.recv(4096) + if not chunk: + break + data += chunk + except socket.timeout: + pytest.fail(f"connection left open, received {data!r}") + assert b"ok" not in data, data + finally: + sock.close() + + # r:wspeek() is false with nothing to read. + def test_lua_001_07_wspeek_empty(self, env): + sock = self.upgrade(env) + try: + sock.sendall(self.frame(b"peek", opcode=0x1)) + assert self.recv_frame(sock) == b"false" + finally: + sock.close() + + # r:wspeek() does not consume what it finds. + def test_lua_001_08_wspeek_read(self, env): + sock = self.upgrade(env) + try: + sock.sendall(self.frame(b"peekread", opcode=0x1)) + time.sleep(0.5) + sock.sendall(self.frame(b"hello", opcode=0x1)) + assert self.recv_frame(sock) == b"hello" + finally: + sock.close() diff --git a/test/modules/lua/test_002_memory.py b/test/modules/lua/test_002_memory.py new file mode 100644 index 00000000000..b70c8d66482 --- /dev/null +++ b/test/modules/lua/test_002_memory.py @@ -0,0 +1,87 @@ +import os +import textwrap + +import pytest + +from pyhttpd.conf import HttpdConf + + +# Iterations per case, chosen so that a call which leaves its result +# (or anything else proportional to its input) in a long-lived pool +# grows the child by roughly 64MB or more. +CASES = { + # 16KB of input per call + "base64_encode": 4096, + "base64_decode": 4096, + "escape": 4096, + "unescape": 4096, + "escape_html": 4096, + "os_escape_path": 4096, + "escape_logitem": 4096, + "construct_url": 4096, + "htpassword": 4096, + "expr": 4096, + "regex": 4096, + "regex_compile": 16384, + # 2KB of input per call + "runtime_dir_relative": 32768, + "ivm_get": 4096, + "ivm_set": 4096, + # a fixed-size result per call + "md5": 2000000, + "sha1": 2000000, + "scoreboard_worker": 200000, + "config": 20000, + "options": 1000000, + "allowoverrides": 4000000, + "headers_in": 2000000, + "get_direntries": 100000, + "sendfile": 200000, + "wspeek": 1000000, +} + +# Growth below this is noise: the Lua heap, malloc arenas, and so on. +LIMIT_KB = 16 * 1024 + + +@pytest.mark.skipif(not os.path.exists("/proc/self/status"), + reason="needs /proc/self/status") +@pytest.mark.skipif("TEST_ASAN" in os.environ, + reason="RSS is not a measure of memory use under ASan") +class TestMemory: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + env.require(env.has_shared_module("lua"), "mod_lua not built", + dep="mod_lua") + # A fresh child for each request, so that memory one case + # leaves behind in the allocator cannot hide growth in the next. + conf = HttpdConf(env, extras={ + 'base': textwrap.dedent(f""" + MaxConnectionsPerChild 1 + LuaScope once + + SetHandler lua-script + # Neither All nor None, for r.allowoverrides + AllowOverride FileInfo + + """), + }) + conf.add_vhost_test1() + conf.install() + assert env.apache_restart() == 0 + + def growth(self, env, case, count): + url = env.mkurl("http", "test1", f"/lua/mem.lua?{case}&{count}") + r = env.curl_raw([url], timeout=120) + assert r.response["status"] == 200, r.response + return int(r.response["body"]) + + def test_lua_002_01_noop(self, env): + assert self.growth(env, "noop", 1000000) < LIMIT_KB + + @pytest.mark.parametrize("case", CASES.keys()) + def test_lua_002_02_growth(self, env, case): + growth = self.growth(env, case, CASES[case]) + assert growth < LIMIT_KB, \ + f"r:{case} x {CASES[case]} grew the child by {growth}kB" diff --git a/test/modules/lua/test_003_request.py b/test/modules/lua/test_003_request.py new file mode 100644 index 00000000000..85ab57dae61 --- /dev/null +++ b/test/modules/lua/test_003_request.py @@ -0,0 +1,95 @@ +import os +import textwrap +import time + +import pytest + +from pyhttpd.conf import HttpdConf + + +class TestRequest: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + env.require(env.has_shared_module("lua"), "mod_lua not built", + dep="mod_lua") + conf = HttpdConf(env, extras={ + 'base': textwrap.dedent(f""" + + SetHandler lua-script + Options +Indexes + AllowOverride FileInfo + + + LimitRequestBody 5000000000 + + """), + }) + conf.add_vhost_test1() + conf.install() + assert env.apache_restart() == 0 + + def get(self, env, path): + r = env.curl_get(env.mkurl("http", "test1", path)) + assert r.response["status"] == 200, r.response + return r.response["body"].decode().strip() + + # MODLUA_MAX_REG_MATCH (25) capture groups are allowed, and the + # last of them is returned. + def test_lua_003_01_regex_max_groups(self, env): + assert self.get(env, "/lua/regex.lua?24") == "x" + assert self.get(env, "/lua/regex.lua?25") == "x" + + # The document root and context info outlive the Lua strings they + # were set from. + def test_lua_003_02_set_document_root(self, env): + assert self.get(env, "/lua/docroot.lua?set_document_root") == \ + "x" * 1000 + "/rootN" + + def test_lua_003_03_set_context_info(self, env): + assert self.get(env, "/lua/docroot.lua?set_context_info") == \ + "x" * 1000 + "/prefixN|" + "x" * 1000 + "/rootN" + + # Request tables read and written through repeated field accesses. + def test_lua_003_04_fields(self, env): + r = env.curl_get(env.mkurl("http", "test1", "/lua/fields.lua")) + assert r.response["status"] == 200, r.response + assert r.response["header"]["x-lua-notes"] == "12" + assert r.response["header"]["x-lua-host"].startswith("test1.") + options, overrides = r.response["body"].decode().strip().split("|") + assert "Indexes" in options.split() + assert overrides.split() == ["FileInfo"] + + # Output from r:sendfile() is kept in order with output around it. + @pytest.mark.parametrize("count", [1, 3]) + def test_lua_003_05_sendfile(self, env, count): + with open(os.path.join(os.path.dirname(__file__), + "htdocs/test1/lua/file.txt")) as f: + content = f.read() + assert self.get(env, f"/lua/sendfile.lua?{count}") + "\n" == \ + (content + "--\n") * count + + # A limit over 2^32 is reported in full. + def test_lua_003_06_limit_req_body(self, env): + assert self.get(env, "/lua/info.lua?limit_req_body") == "5000000000" + + def test_lua_003_07_mpm_query_unknown(self, env): + assert self.get(env, "/lua/info.lua?mpm_query") == "nil" + + def test_lua_003_08_scoreboard_worker_range(self, env): + assert self.get(env, "/lua/info.lua?scoreboard_worker") == "nil" + + # The server start time, in seconds since the epoch. + def test_lua_003_09_started(self, env): + started = int(self.get(env, "/lua/info.lua?started")) + assert 0 <= time.time() - started < 3600 + + def test_lua_003_10_digests(self, env): + assert self.get(env, "/lua/info.lua?md5") == \ + "900150983cd24fb0d6963f7d28e17f72" + assert self.get(env, "/lua/info.lua?sha1") == \ + "a9993e364706816aba3e25717850c26c9cd0d89d" + + # r:base64_encode() takes at most 8MB. + def test_lua_003_11_base64_max(self, env): + assert self.get(env, "/lua/info.lua?base64_max") == "true|false" diff --git a/test/modules/lua/test_004_body.py b/test/modules/lua/test_004_body.py new file mode 100644 index 00000000000..c148564aa08 --- /dev/null +++ b/test/modules/lua/test_004_body.py @@ -0,0 +1,103 @@ +import hashlib +import os +import socket +import textwrap +import time + +import pytest + +from pyhttpd.conf import HttpdConf + + +BODY = bytes(range(256)) * 4 + + +class TestBody: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + env.require(env.has_shared_module("lua"), "mod_lua not built", + dep="mod_lua") + conf = HttpdConf(env, extras={ + 'base': textwrap.dedent(f""" + + SetHandler lua-script + + """), + }) + conf.add_vhost_test1() + conf.install() + assert env.apache_restart() == 0 + + def post(self, env, mode, body=BODY): + url = env.mkurl("http", "test1", f"/lua/body.lua?{mode}") + fpath = os.path.join(env.gen_dir, "lua-body.bin") + with open(fpath, "wb") as f: + f.write(body) + r = env.curl_raw([url], options=[ + "-H", "Content-Type: application/octet-stream", + "--data-binary", f"@{fpath}"]) + assert r.response["status"] == 200, r.response + return r.response["body"].decode().strip() + + def test_lua_004_01_read(self, env): + length, digest, _ = self.post(env, "read").split() + assert length == str(len(BODY)) + assert digest == hashlib.md5(BODY).hexdigest() + + @staticmethod + def max_child_rss(env) -> int: + """The largest VmRSS of the server's children, in kB.""" + ppid = env.read_pid_file() + assert ppid, "no httpd.pid" + largest = 0 + for pid in filter(str.isdigit, os.listdir("/proc")): + try: + with open(f"/proc/{pid}/status") as f: + status = dict(line.split(":", 1) for line in f) + except OSError: + continue + if int(status.get("PPid", "0")) == ppid and "VmRSS" in status: + largest = max(largest, int(status["VmRSS"].split()[0])) + return largest + + # Memory for the body is not allocated from the Content-Length + # before the body arrives. + @pytest.mark.skipif(not os.path.exists("/proc/self/status"), + reason="needs /proc/self/status") + def test_lua_004_02_declared_length(self, env): + before = self.max_child_rss(env) + sock = socket.create_connection((env.http_addr, env.http_port), + timeout=30) + try: + sock.sendall((f"POST /lua/body.lua?read HTTP/1.1\r\n" + f"Host: test1.{env.http_tld}:{env.http_port}\r\n" + f"Content-Length: {256 * 1024 * 1024}\r\n" + f"\r\n").encode() + b"x" * 10) + # The handler is now waiting in r:requestbody() for the rest. + time.sleep(1) + growth = self.max_child_rss(env) - before + finally: + sock.close() + assert growth < 64 * 1024, \ + f"child RSS grew {growth}kB on 10 bytes of a declared 256MB body" + + # A body over the limit is refused with nil and a message. + def test_lua_004_03_read_max(self, env): + assert self.post(env, "readmax") == \ + "nil|Request body was larger than the permitted size." + assert self.post(env, "readmax", b"x" * 50) == "50|50" + + def test_lua_004_04_file(self, env): + assert self.post(env, "file") == \ + f"{len(BODY)}|nil|{hashlib.md5(BODY).hexdigest()}" + + def test_lua_004_05_file_max(self, env): + assert self.post(env, "filemax") == \ + "nil|Request body was larger than the permitted size.|-" + + # A shorter body replaces a longer file entirely. + def test_lua_004_06_file_overwrite(self, env): + self.post(env, "file") + assert self.post(env, "fileover", b"abc") == \ + f"3|nil|{hashlib.md5(b'abc').hexdigest()}"