diff --git a/lib/resty/wasmx.lua b/lib/resty/wasmx.lua index 9f71f75f2..e61da77c0 100644 --- a/lib/resty/wasmx.lua +++ b/lib/resty/wasmx.lua @@ -15,10 +15,14 @@ ffi.cdef [[ ngx_str_t *config; } ngx_wasm_filter_t; + typedef intptr_t ngx_int_t; typedef unsigned char u_char; typedef struct ngx_wavm_t ngx_wasm_vm_t; typedef struct ngx_wasm_ops_plan_t ngx_wasm_plan_t; + typedef ngx_int_t (*ngx_wasm_host_prop_fn_t)(void *data, ngx_str_t *key, + ngx_str_t *value); + ngx_wasm_vm_t *ngx_wasm_ffi_main_vm(); ]] diff --git a/lib/resty/wasmx/proxy_wasm.lua b/lib/resty/wasmx/proxy_wasm.lua index e44e727bf..b3310cb04 100644 --- a/lib/resty/wasmx/proxy_wasm.lua +++ b/lib/resty/wasmx/proxy_wasm.lua @@ -38,23 +38,25 @@ local _M = { if subsystem == "http" then ffi.cdef [[ - int ngx_http_wasm_ffi_plan_new(ngx_wasm_vm_t *vm, - ngx_wasm_filter_t *filters, - size_t n_filters, - ngx_wasm_plan_t **out, - u_char *err, size_t *errlen); - int ngx_http_wasm_ffi_plan_free(ngx_wasm_plan_t *plan); - int ngx_http_wasm_ffi_plan_load(ngx_wasm_plan_t *plan); - int ngx_http_wasm_ffi_plan_attach(ngx_http_request_t *r, - ngx_wasm_plan_t *plan, - unsigned int isolation); - int ngx_http_wasm_ffi_start(ngx_http_request_t *r); - int ngx_http_wasm_ffi_set_property(ngx_http_request_t *r, - ngx_str_t *key, - ngx_str_t *value); - int ngx_http_wasm_ffi_get_property(ngx_http_request_t *r, - ngx_str_t *key, - ngx_str_t *out); + ngx_int_t ngx_http_wasm_ffi_plan_new(ngx_wasm_vm_t *vm, + ngx_wasm_filter_t *filters, + size_t n_filters, + ngx_wasm_plan_t **out, + u_char *err, size_t *errlen); + ngx_int_t ngx_http_wasm_ffi_plan_free(ngx_wasm_plan_t *plan); + ngx_int_t ngx_http_wasm_ffi_plan_load(ngx_wasm_plan_t *plan); + ngx_int_t ngx_http_wasm_ffi_plan_attach(ngx_http_request_t *r, + ngx_wasm_plan_t *plan, + unsigned int isolation); + ngx_int_t ngx_http_wasm_ffi_start(ngx_http_request_t *r); + ngx_int_t ngx_http_wasm_ffi_set_property(ngx_http_request_t *r, + ngx_str_t *key, + ngx_str_t *value); + ngx_int_t ngx_http_wasm_ffi_get_property(ngx_http_request_t *r, + ngx_str_t *key, + ngx_str_t *out); + ngx_int_t ngx_http_wasm_ffi_enable_property_setter(ngx_http_request_t *r); + ngx_int_t ngx_http_wasm_ffi_enable_property_getter(ngx_http_request_t *r); ]] else @@ -229,7 +231,7 @@ function _M.set_property(key, value) error("key must be a string", 2) end - if type(value) ~= "string" then + if value ~= nil and type(value) ~= "string" then error("value must be a string", 2) end @@ -238,8 +240,14 @@ function _M.set_property(key, value) error("no request found", 2) end - local ckey = ffi_new("ngx_str_t", { data = key, len = #key }) - local cvalue = ffi_new("ngx_str_t", { data = value, len = #value }) + local ckey = ffi_new("ngx_str_t", { + data = key, + len = #key + }) + local cvalue = ffi_new("ngx_str_t", { + data = value, + len = value and #value or 0 + }) local rc = C.ngx_http_wasm_ffi_set_property(r, ckey, cvalue) if rc == FFI_ERROR then @@ -276,8 +284,81 @@ function _M.get_property(key) return nil, "property \"" .. key .. "\" not found", NOT_FOUND end + if cvalue.data == nil then + return nil + end + return ffi_str(cvalue.data, cvalue.len) end +--- +-- Define a setter function for host-managed properties. +-- +-- @param setter The setter function is the handler for updating properties. +-- Its type signature is `function(string, string): boolean, string`, +-- where the inputs are the property key and value and the results are: +-- * `truthy` - success setting the property +-- * `truthy, value` - success, cache property value (useful if hosts +-- sets only a setter but not a getter) +-- * `truthy, value, truthy` - success, constant property value: further +-- requests to the same property during a request are cached by +-- ngx_wasm_module and do not invoke the Lua getter. +-- * `falsy, err` - failure, and an optional error message +-- @return true on success setting the getter, or nil and an error message. +function _M.set_property_setter(setter) + if type(setter) ~= "function" then + error("setter must be a function", 2) + end + + local r = get_request() + if not r then + error("no request found", 2) + end + + _M.property_setter = setter + + local rc = C.ngx_http_wasm_ffi_enable_property_setter(r) + if rc ~= FFI_OK then + return nil, "could not set property setter" + end + + return true +end + + +--- +-- Define a getter function for host-managed properties. +-- +-- @param getter The getter function is the handler for resolving properties. +-- Its type signature is `function(string): boolean, string, boolean`, +-- where the input is the property key and the results are: +-- * `truthy, value` - success, property value +-- * `truthy, value, truthy` - success, constant property value: further +-- requests to the same property during a request are cached by +-- ngx_wasm_module and do not invoke the Lua getter. +-- * `falsy` - property not found +-- * `falsy, err` - failure, error message +-- @return true on success setting the getter, or nil and an error message. +function _M.set_property_getter(getter) + if type(getter) ~= "function" then + error("getter must be a function", 2) + end + + local r = get_request() + if not r then + error("no request found", 2) + end + + _M.property_getter = getter + + local rc = C.ngx_http_wasm_ffi_enable_property_getter(r) + if rc ~= FFI_OK then + return nil, "could not set property getter" + end + + return true +end + + return _M diff --git a/src/common/lua/ngx_wasm_lua.c b/src/common/lua/ngx_wasm_lua.c index fc83c1ba7..11f8274c2 100644 --- a/src/common/lua/ngx_wasm_lua.c +++ b/src/common/lua/ngx_wasm_lua.c @@ -1,5 +1,5 @@ #ifndef DDEBUG -#define DDEBUG 0 +#define DDEBUG 1 #endif #include "ddebug.h" @@ -113,6 +113,56 @@ ngx_wasm_lua_thread_destroy(ngx_wasm_lua_ctx_t *lctx) } +static ngx_int_t +ngx_wasm_lua_load_code(ngx_wasm_lua_ctx_t *lctx, + const char *tag, const char *src, + ngx_wasm_subsys_env_t *env) +{ + ngx_int_t rc; + + lctx->code = src; + lctx->code_len = ngx_strlen(src); + lctx->cache_key = ngx_wasm_lua_thread_cache_key(lctx->pool, + tag, + (u_char *) lctx->code, + lctx->code_len); + if (lctx->cache_key == NULL) { + return NGX_ERROR; + } + + /* load code */ + + switch (env->subsys->kind) { +#if (NGX_WASM_HTTP) + case NGX_WASM_SUBSYS_HTTP: + rc = ngx_http_lua_cache_loadbuffer(lctx->log, + lctx->L, + (u_char *) lctx->code, + lctx->code_len, + &lctx->code_ref, + lctx->cache_key, + tag); + break; +#endif +#if (NGX_WASM_STREAM) + case NGX_WASM_SUBSYS_STREAM: + rc = ngx_stream_lua_cache_loadbuffer(lctx->log, + lctx->L, + (u_char *) lctx->code, + lctx->code_len, + lctx->cache_key, + tag); + break; +#endif + default: + ngx_wasm_assert(0); + return NGX_ERROR; + } + + return rc; +} + + ngx_wasm_lua_ctx_t * ngx_wasm_lua_thread_new(const char *tag, const char *src, ngx_wasm_subsys_env_t *env, ngx_log_t *log, void *data, @@ -155,6 +205,9 @@ ngx_wasm_lua_thread_new(const char *tag, const char *src, lctx->L = ngx_http_lua_get_lua_vm(r, NULL); lctx->co = ngx_http_lua_new_thread(r, lctx->L, &lctx->co_ref); + + ngx_http_lua_set_req(lctx->co, r); + break; } #endif @@ -166,6 +219,9 @@ ngx_wasm_lua_thread_new(const char *tag, const char *src, lctx->L = ngx_stream_lua_get_lua_vm(sr, NULL); lctx->co = ngx_stream_lua_new_thread(sr, lctx->L, lctx->co_ref); + + ngx_stream_lua_set_req(lctx->co, sr); + break; } #endif @@ -183,47 +239,11 @@ ngx_wasm_lua_thread_new(const char *tag, const char *src, /* code */ - lctx->code = src; - lctx->code_len = ngx_strlen(src); - lctx->cache_key = ngx_wasm_lua_thread_cache_key(lctx->pool, - tag, - (u_char *) lctx->code, - lctx->code_len); - if (lctx->cache_key == NULL) { - goto error; - } - - /* load code */ - - switch (env->subsys->kind) { -#if (NGX_WASM_HTTP) - case NGX_WASM_SUBSYS_HTTP: - rc = ngx_http_lua_cache_loadbuffer(lctx->log, - lctx->L, - (u_char *) lctx->code, - lctx->code_len, - &lctx->code_ref, - lctx->cache_key, - tag); - break; -#endif -#if (NGX_WASM_STREAM) - case NGX_WASM_SUBSYS_STREAM: - rc = ngx_stream_lua_cache_loadbuffer(lctx->log, - lctx->L, - (u_char *) lctx->code, - lctx->code_len, - lctx->cache_key, - tag); - break; -#endif - default: - ngx_wasm_assert(0); - goto error; - } - - if (rc != NGX_OK) { - goto error; + if (src) { + rc = ngx_wasm_lua_load_code(lctx, tag, src, env); + if (rc != NGX_OK) { + goto error; + } } /* move code closure to new coroutine */ diff --git a/src/common/lua/ngx_wasm_lua_ffi.c b/src/common/lua/ngx_wasm_lua_ffi.c index 0516a39b1..538edd2fc 100644 --- a/src/common/lua/ngx_wasm_lua_ffi.c +++ b/src/common/lua/ngx_wasm_lua_ffi.c @@ -3,6 +3,7 @@ #endif #include "ddebug.h" +#include #include @@ -180,6 +181,21 @@ ngx_http_wasm_ffi_start(ngx_http_request_t *r) } +static ngx_inline ngx_proxy_wasm_ctx_t * +get_pwctx(ngx_http_request_t *r) +{ + ngx_http_wasm_req_ctx_t *rctx; + + if (ngx_http_wasm_rctx(r, &rctx) != NGX_OK) { + return NULL; + } + + return ngx_proxy_wasm_ctx(NULL, 0, + NGX_PROXY_WASM_ISOLATION_STREAM, + &ngx_http_proxy_wasm, rctx); +} + + ngx_int_t ngx_http_wasm_ffi_set_property(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value) @@ -226,4 +242,206 @@ ngx_http_wasm_ffi_get_property(ngx_http_request_t *r, return ngx_proxy_wasm_properties_get(pwctx, key, value); } + + +void +ngx_wasm_lua_ffi_host_prop_handler( + ngx_wasm_ffi_host_property_ctx_t *hpctx, + unsigned char ok, const char* new_value, size_t len, + unsigned char is_const) +{ + ngx_proxy_wasm_ctx_t *pwctx = get_pwctx(hpctx->r); + ngx_str_t *key = hpctx->key; + ngx_str_t *value = hpctx->value; + + if (!ok) { + if (new_value) { + ngx_wavm_log_error(NGX_LOG_ERR, pwctx->log, NULL, + "error in property handler: %s", + new_value); + hpctx->rc = NGX_ERROR; + return; + + } else if (!hpctx->is_getter) { + ngx_wavm_log_error(NGX_LOG_ERR, pwctx->log, NULL, + "error in property handler: unknown error"); + hpctx->rc = NGX_ERROR; + return; + } + + hpctx->rc = NGX_DECLINED; + return; + } + + if (new_value == NULL) { + value->data = NULL; + value->len = 0; + + } else { + value->data = (u_char *) new_value; + value->len = len; + } + + hpctx->rc = ngx_proxy_wasm_properties_set_host(pwctx, key, value, is_const, + hpctx->is_getter); + + if (hpctx->is_getter && hpctx->rc == NGX_OK && value->data == NULL) { + hpctx->rc = NGX_DECLINED; + } + + hpctx->rc = NGX_OK; +} + +static const char *HOST_PROPERTY_SCRIPT_NAME = "wasm_lua_host_property_chunk"; +static const char *HOST_PROPERTY_SCRIPT = "" + "local hpctx, is_getter, key, value = ... \n" + "local ffi = require('ffi') \n" + "local fmt = string.format \n" + " \n" + "ffi.cdef[[ \n" + " typedef unsigned char u_char; \n" + " typedef void ngx_wasm_ffi_host_property_ctx_t; \n" + " \n" + " void ngx_wasm_lua_ffi_host_prop_handler( \n" + " ngx_wasm_ffi_host_property_ctx_t *hpctx, \n" + " u_char ok, const char* value, size_t len, \n" + " u_char is_const); \n" + "]] \n" + " \n" + "local proxy_wasm = require('resty.wasmx.proxy_wasm') \n" + " \n" + "local fn_name = is_getter and 'property_getter' or 'property_setter' \n" + "local fn = proxy_wasm[fn_name] \n" + "if not fn then \n" + " error('property handler function not found') \n" + "end \n" + " \n" + "local pok, ok, new_value, is_const = pcall(fn, key, value) \n" + "if not pok then \n" + " ngx.log(ngx.ERR, 'Lua error in property handler: ' .. ok) \n" + " error('fail') \n" + "end \n" + " \n" + "local new_value_len = new_value and #new_value or 0 \n" + " \n" + "ffi.C.ngx_wasm_lua_ffi_host_prop_handler(hpctx, ok and 1 or 0, \n" + " new_value, new_value_len, \n" + " is_const and 1 or 0) \n"; + +static ngx_int_t +property_handler(ngx_proxy_wasm_ctx_t* pwctx, ngx_str_t *key, ngx_str_t *value, + unsigned is_getter) +{ + ngx_http_wasm_req_ctx_t *rctx = pwctx->data; + ngx_wasm_ffi_host_property_ctx_t hpctx; + lua_State *co; + ngx_wasm_lua_ctx_t *lctx; + int rv; + + // FIXME if this function is changed to return NGX_AGAIN, + // hpctx needs to be dynamically allocated: + + hpctx.r = rctx->r; + hpctx.is_getter = is_getter; + hpctx.key = key; + hpctx.value = value; + + /* create coroutine */ + + lctx = ngx_wasm_lua_thread_new(HOST_PROPERTY_SCRIPT_NAME, + HOST_PROPERTY_SCRIPT, + &rctx->env, rctx->r->connection->log, &hpctx, + NULL, NULL); + if (lctx == NULL) { + return NGX_ERROR; + } + + /* push key and value arguments */ + + co = lctx->co; + + ngx_wasm_assert(key && key->data); + ngx_wasm_assert(value); + + lctx->nargs = 4; + + lua_pushlightuserdata(co, &hpctx); + lua_pushboolean(co, is_getter); + lua_pushlstring(co, (char *) key->data, key->len); + + if (is_getter || value->data == NULL) { + lua_pushnil(co); + + } else { + if ((int) value->len == 0) { + lua_pushliteral(co, ""); + + } else { + lua_pushlstring(co, (char *) value->data, value->len); + } + } + + /* run handler */ + + rv = lua_resume(lctx->co, lctx->nargs); + if (rv == LUA_YIELD) { + ngx_wavm_log_error(NGX_LOG_ERR, pwctx->log, NULL, + "cannot yield from host property handler"); + + return NGX_ERROR; + } + + if (rv != 0) { + ngx_wavm_log_error(NGX_LOG_ERR, pwctx->log, NULL, + "Lua resume failed with error: code %d", rv); + + return NGX_ERROR; + } + + return hpctx.rc; +} + + +static ngx_int_t +property_setter(void* pwctx, ngx_str_t *key, ngx_str_t *value) +{ + return property_handler((ngx_proxy_wasm_ctx_t *) pwctx, key, value, 0); +} + + +static ngx_int_t +property_getter(void* pwctx, ngx_str_t *key, ngx_str_t *value) +{ + return property_handler((ngx_proxy_wasm_ctx_t *) pwctx, key, value, 1); +} + + +ngx_int_t +ngx_http_wasm_ffi_enable_property_setter(ngx_http_request_t *r) +{ + ngx_proxy_wasm_ctx_t *pwctx = get_pwctx(r); + + if (pwctx == NULL) { + return NGX_ERROR; + } + + return ngx_proxy_wasm_properties_set_host_prop_setter(pwctx, + property_setter, + pwctx); +} + + +ngx_int_t +ngx_http_wasm_ffi_enable_property_getter(ngx_http_request_t *r) +{ + ngx_proxy_wasm_ctx_t *pwctx = get_pwctx(r); + + if (pwctx == NULL) { + return NGX_ERROR; + } + + return ngx_proxy_wasm_properties_set_host_prop_getter(pwctx, + property_getter, + pwctx); +} #endif diff --git a/src/common/lua/ngx_wasm_lua_ffi.h b/src/common/lua/ngx_wasm_lua_ffi.h index 0178058e1..9d9808da7 100644 --- a/src/common/lua/ngx_wasm_lua_ffi.h +++ b/src/common/lua/ngx_wasm_lua_ffi.h @@ -21,6 +21,15 @@ typedef struct { } ngx_wasm_ffi_filter_t; +typedef struct { + ngx_http_request_t *r; + unsigned is_getter; + ngx_str_t *key; + ngx_str_t *value; + ngx_int_t rc; +} ngx_wasm_ffi_host_property_ctx_t; + + ngx_int_t ngx_http_wasm_ffi_plan_new(ngx_wavm_t *vm, ngx_wasm_ffi_filter_t *filters, size_t n_filters, ngx_wasm_ops_plan_t **out, u_char *err, size_t *errlen); @@ -33,6 +42,12 @@ ngx_int_t ngx_http_wasm_ffi_set_property(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value); ngx_int_t ngx_http_wasm_ffi_get_property(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value); +ngx_int_t ngx_http_wasm_ffi_set_host_property(ngx_http_request_t *r, + ngx_str_t *key, ngx_str_t *value, unsigned is_const, unsigned retrieve); +ngx_int_t ngx_http_wasm_ffi_set_property_setter(ngx_http_request_t *r, + ngx_wasm_host_prop_fn_t fn); +ngx_int_t ngx_http_wasm_ffi_set_property_getter(ngx_http_request_t *r, + ngx_wasm_host_prop_fn_t fn); #endif diff --git a/src/common/proxy_wasm/ngx_proxy_wasm.h b/src/common/proxy_wasm/ngx_proxy_wasm.h index 772508676..4c2877244 100644 --- a/src/common/proxy_wasm/ngx_proxy_wasm.h +++ b/src/common/proxy_wasm/ngx_proxy_wasm.h @@ -166,6 +166,9 @@ typedef struct ngx_http_proxy_wasm_dispatch_s ngx_http_proxy_wasm_dispatch_t; typedef ngx_str_t ngx_proxy_wasm_marshalled_map_t; +typedef ngx_int_t (*ngx_wasm_host_prop_fn_t)(void *data, ngx_str_t *key, + ngx_str_t *value); + typedef struct { ngx_queue_t busy; @@ -245,10 +248,14 @@ struct ngx_proxy_wasm_ctx_s { ngx_uint_t call_code; ngx_uint_t response_code; - /* host properties rbtree */ + /* host properties */ ngx_rbtree_t host_props_tree; ngx_rbtree_node_t host_props_sentinel; + ngx_wasm_host_prop_fn_t host_prop_getter; + ngx_wasm_host_prop_fn_t host_prop_setter; + void *host_prop_getter_data; + void *host_prop_setter_data; /* flags */ diff --git a/src/common/proxy_wasm/ngx_proxy_wasm_properties.c b/src/common/proxy_wasm/ngx_proxy_wasm_properties.c index 6af10b3e3..333688722 100644 --- a/src/common/proxy_wasm/ngx_proxy_wasm_properties.c +++ b/src/common/proxy_wasm/ngx_proxy_wasm_properties.c @@ -53,6 +53,8 @@ static size_t host_prefix_len; typedef struct { ngx_str_node_t sn; ngx_str_t value; + unsigned is_const:1; + unsigned const_null:1; } host_props_node_t; @@ -877,25 +879,58 @@ ngx_proxy_wasm_properties_get_host(ngx_proxy_wasm_ctx_t *pwctx, hpn = (host_props_node_t *) ngx_str_rbtree_lookup(&pwctx->host_props_tree, path, hash); + if (hpn == NULL) { + return NGX_DECLINED; + } - if (hpn) { - value->data = hpn->value.data; - value->len = hpn->value.len; + /* stored value is not const: need to call getter again */ + if (!hpn->is_const && pwctx->host_prop_getter) { + return NGX_DECLINED; + } - return NGX_OK; + if (hpn->const_null) { + return NGX_BUSY; } - return NGX_DECLINED; + value->data = hpn->value.data; + value->len = hpn->value.len; + + return NGX_OK; } -static ngx_int_t +static ngx_inline ngx_int_t +set_hpn_value(host_props_node_t *hpn, ngx_str_t *value, unsigned is_const, + ngx_pool_t *pool) +{ + unsigned char *new_data; + + if (value->data == NULL) { + new_data = NULL; + hpn->const_null = 1; + + } else { + new_data = ngx_pstrdup(pool, value); + if (new_data == NULL) { + return NGX_ERROR; + } + } + + hpn->is_const = is_const; + hpn->value.len = value->len; + hpn->value.data = new_data; + + return NGX_OK; +} + + +ngx_int_t ngx_proxy_wasm_properties_set_host(ngx_proxy_wasm_ctx_t *pwctx, - ngx_str_t *path, ngx_str_t *value) + ngx_str_t *path, ngx_str_t *value, unsigned is_const, unsigned retrieve) { host_props_node_t *hpn; uint32_t hash; - unsigned new_entry = 1; + ngx_int_t rc; #ifdef NGX_WASM_HTTP ngx_http_wasm_req_ctx_t *rctx = pwctx->data; @@ -911,8 +946,17 @@ ngx_proxy_wasm_properties_set_host(ngx_proxy_wasm_ctx_t *pwctx, hpn = (host_props_node_t *) ngx_str_rbtree_lookup(&pwctx->host_props_tree, path, hash); - if (value->data == NULL) { + if (hpn && hpn->is_const) { + /* if a const node exists, we can't overwrite it */ + + return NGX_DECLINED; + } + + if (value->data == NULL && !is_const) { + /* if setting non-const NULL, remove node if it exists */ + if (hpn) { + ngx_pfree(pwctx->pool, hpn->value.data); ngx_rbtree_delete(&pwctx->host_props_tree, &hpn->sn.node); } @@ -920,10 +964,18 @@ ngx_proxy_wasm_properties_set_host(ngx_proxy_wasm_ctx_t *pwctx, } if (hpn) { + /* if the node exists, replace the value */ + ngx_pfree(pwctx->pool, hpn->value.data); - new_entry = 0; + rc = set_hpn_value(hpn, value, is_const, pwctx->pool); + if (rc != NGX_OK) { + return rc; + } } else { + /* if the node doesn't exist, + create it, set key, value, and store node in rbtree */ + hpn = ngx_pcalloc(pwctx->pool, sizeof(host_props_node_t) + path->len); if (hpn == NULL) { return NGX_ERROR; @@ -933,18 +985,19 @@ ngx_proxy_wasm_properties_set_host(ngx_proxy_wasm_ctx_t *pwctx, hpn->sn.str.len = path->len; hpn->sn.str.data = (u_char *) hpn + sizeof(host_props_node_t); ngx_memcpy(hpn->sn.str.data, path->data, path->len); - } - hpn->value.len = value->len; - hpn->value.data = ngx_pstrdup(pwctx->pool, value); - if (hpn->value.data == NULL) { - return NGX_ERROR; - } + rc = set_hpn_value(hpn, value, is_const, pwctx->pool); + if (rc != NGX_OK) { + return rc; + } - if (new_entry) { ngx_rbtree_insert(&pwctx->host_props_tree, &hpn->sn.node); } + if (retrieve) { + value->data = hpn->value.data; + } + return NGX_OK; } @@ -970,6 +1023,7 @@ ngx_proxy_wasm_properties_get(ngx_proxy_wasm_ctx_t *pwctx, ngx_str_t p = { path->len, NULL }; ngx_uint_t key; pwm2ngx_mapping_t *m; + ngx_int_t rc; p.data = replace_nulls_by_dots(path, dotted_path_buf); key = ngx_hash_key(p.data, p.len); @@ -997,7 +1051,22 @@ ngx_proxy_wasm_properties_get(ngx_proxy_wasm_ctx_t *pwctx, && ngx_memcmp(p.data, host_prefix, host_prefix_len) == 0) { /* host variable */ - return ngx_proxy_wasm_properties_get_host(pwctx, &p, value); + + /* even if there is a getter, try reading const value first */ + rc = ngx_proxy_wasm_properties_get_host(pwctx, &p, value); + if (rc == NGX_OK) { + return NGX_OK; + + } else if (rc == NGX_BUSY) { + return NGX_DECLINED; + } + + if (pwctx->host_prop_getter) { + return pwctx->host_prop_getter(pwctx->host_prop_getter_data, + &p, value); + } + + return rc; } return NGX_DECLINED; @@ -1033,8 +1102,57 @@ ngx_proxy_wasm_properties_set(ngx_proxy_wasm_ctx_t *pwctx, && ngx_memcmp(p.data, host_prefix, host_prefix_len) == 0) { /* host variable */ - return ngx_proxy_wasm_properties_set_host(pwctx, &p, value); + if (pwctx->host_prop_setter) { + return pwctx->host_prop_setter(pwctx->host_prop_setter_data, + &p, value); + } + + return ngx_proxy_wasm_properties_set_host(pwctx, &p, value, 0, 0); } return NGX_DECLINED; } + + +ngx_int_t +ngx_proxy_wasm_properties_set_host_prop_setter(ngx_proxy_wasm_ctx_t *pwctx, + ngx_wasm_host_prop_fn_t fn, void *data) +{ +#ifdef NGX_WASM_HTTP + ngx_http_wasm_req_ctx_t *rctx = pwctx->data; + + if (rctx == NULL || rctx->fake_request) { + ngx_wavm_log_error(NGX_LOG_ERR, pwctx->log, NULL, + "cannot set host properties setter " + "outside of a request"); + return NGX_ERROR; + } +#endif + + pwctx->host_prop_setter = fn; + pwctx->host_prop_setter_data = data; + + return NGX_OK; +} + + +ngx_int_t +ngx_proxy_wasm_properties_set_host_prop_getter(ngx_proxy_wasm_ctx_t *pwctx, + ngx_wasm_host_prop_fn_t fn, void *data) +{ +#ifdef NGX_WASM_HTTP + ngx_http_wasm_req_ctx_t *rctx = pwctx->data; + + if (rctx == NULL || rctx->fake_request) { + ngx_wavm_log_error(NGX_LOG_ERR, pwctx->log, NULL, + "cannot set host properties getter " + "outside of a request"); + return NGX_ERROR; + } +#endif + + pwctx->host_prop_getter = fn; + pwctx->host_prop_getter_data = data; + + return NGX_OK; +} diff --git a/src/common/proxy_wasm/ngx_proxy_wasm_properties.h b/src/common/proxy_wasm/ngx_proxy_wasm_properties.h index e39438cfa..9843fd229 100644 --- a/src/common/proxy_wasm/ngx_proxy_wasm_properties.h +++ b/src/common/proxy_wasm/ngx_proxy_wasm_properties.h @@ -9,6 +9,12 @@ ngx_int_t ngx_proxy_wasm_properties_get(ngx_proxy_wasm_ctx_t *pwctx, ngx_str_t *path, ngx_str_t *value); ngx_int_t ngx_proxy_wasm_properties_set(ngx_proxy_wasm_ctx_t *pwctx, ngx_str_t *path, ngx_str_t *value); +ngx_int_t ngx_proxy_wasm_properties_set_host(ngx_proxy_wasm_ctx_t *pwctx, + ngx_str_t *path, ngx_str_t *value, unsigned is_const, unsigned retrieve); +ngx_int_t ngx_proxy_wasm_properties_set_host_prop_setter( + ngx_proxy_wasm_ctx_t *pwctx, ngx_wasm_host_prop_fn_t fn, void *data); +ngx_int_t ngx_proxy_wasm_properties_set_host_prop_getter( + ngx_proxy_wasm_ctx_t *pwctx, ngx_wasm_host_prop_fn_t fn, void *data); #endif /* _NGX_PROXY_WASM_PROPERTIES_H_INCLUDED_ */ diff --git a/t/04-openresty/ffi/304-proxy_wasm_set_property_getter.t b/t/04-openresty/ffi/304-proxy_wasm_set_property_getter.t new file mode 100644 index 000000000..ac8f8786b --- /dev/null +++ b/t/04-openresty/ffi/304-proxy_wasm_set_property_getter.t @@ -0,0 +1,597 @@ +# vim:set ft= ts=4 sw=4 et fdm=marker: + +use strict; +use lib '.'; +use t::TestWasm::Lua; + +skip_no_openresty(); + +plan tests => repeat_each() * (blocks() * 5); + +run_tests(); + +__DATA__ + +=== TEST 1: set_property_getter() - setting host properties getter fails on init_worker_by_lua +--- wasm_modules: on_phases +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "on_phases" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + + local function getter(key) + if key == "my_key" then + return true, "my_value" + end + return false + end + + assert(proxy_wasm.set_property_getter(getter)) + } +--- config + location /t { + rewrite_by_lua_block { + ngx.say("ok") + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[error\] .*? cannot set host properties getter outside of a request/, + qr/\[error\] .*? init_worker_by_lua(\(nginx\.conf:\d+\))?:\d+: could not set property getter/, +] +--- no_error_log +[crit] + + + +=== TEST 2: set_property_getter() - setting host properties getter works on rewrite_by_lua +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm hostcalls 'on=response_body \ + test=/t/log/property \ + name=wasmx.my_property'; + + rewrite_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + local function getter(key) + if key == "wasmx.my_property" then + return true, "my_value" + end + return false + end + + assert(proxy_wasm.set_property_getter(getter)) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_response_body/, + qr/\[info\] .*? wasmx.my_property: my_value/, +] +--- no_error_log +[error] + + + +=== TEST 3: set_property_getter() - setting host properties getter works on access_by_lua +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm hostcalls 'on=response_body \ + test=/t/log/property \ + name=wasmx.my_property'; + + access_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + local function getter(key) + if key == "wasmx.my_property" then + return true, "my_value" + end + return false + end + + assert(proxy_wasm.set_property_getter(getter)) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_response_body/, + qr/\[info\] .*? wasmx.my_property: my_value/, +] +--- no_error_log +[error] + + + +=== TEST 4: set_property_getter() - setting host properties getter works on header_filter_by_lua +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm hostcalls 'on=response_body \ + test=/t/log/property \ + name=wasmx.my_property'; + + rewrite_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + + header_filter_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + + local function getter(key) + if key == "wasmx.my_property" then + return true, "my_value" + end + return false + end + + assert(proxy_wasm.set_property_getter(getter)) + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_response_body/, + qr/\[info\] .*? wasmx.my_property: my_value/, +] +--- no_error_log +[error] + + + +=== TEST 5: set_property_getter() - setting host properties getter works on body_filter_by_lua +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm hostcalls 'on=response_body \ + test=/t/log/property \ + name=wasmx.my_property'; + + rewrite_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + + body_filter_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + + local function getter(key) + if key == "wasmx.my_property" then + return true, "my_value" + end + return false + end + + assert(proxy_wasm.set_property_getter(getter)) + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_response_body/, + qr/\[info\] .*? wasmx.my_property: my_value/, +] +--- no_error_log +[error] + + + +=== TEST 6: set_property_getter() - setting host properties getter works on log_by_lua +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm_request_headers_in_access on; + + proxy_wasm hostcalls 'on=log \ + test=/t/log/property \ + name=wasmx.my_property'; + + access_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + + log_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + + local function getter(key) + if key == "wasmx.my_property" then + return true, "my_value" + end + return false + end + + assert(proxy_wasm.set_property_getter(getter)) + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_log/, + qr/\[info\] .*? wasmx.my_property: my_value/, +] +--- no_error_log +[error] + + + +=== TEST 7: set_property_getter() - setting property getter at startup doesn't reset filter list +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls", config = "on=log " .. + "test=/t/log/property " .. + "name=wasmx.startup_property" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm_request_headers_in_access on; + + access_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + + assert(proxy_wasm.attach(_G.c_plan)) + + local function getter(key) + if key == "wasmx.startup_property" then + return true, "foo" + end + return false + end + + assert(proxy_wasm.set_property_getter(getter)) + + assert(proxy_wasm.start()) + + ngx.say("ok") + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_log/, + qr/\[info\] .*?hostcalls.*? wasmx.startup_property: foo/, +] +--- no_error_log +[error] + + + +=== TEST 8: set_property_getter() - getter returning false produces property not found +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm hostcalls 'on=response_body \ + test=/t/log/property \ + name=wasmx.missing_property'; + + access_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + local function getter(key) + if key == "wasmx.my_property" then + return true, "my_value" + end + return false + end + + assert(proxy_wasm.set_property_getter(getter)) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_response_body/, + qr/\[info\] .*? property not found: wasmx.missing_property/, +] +--- no_error_log +[error] + + + +=== TEST 9: set_property_getter() - getter returning nil produces property not found +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm hostcalls 'on=response_body \ + test=/t/log/property \ + name=wasmx.missing_property'; + + access_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + local function getter(key) + if key == "wasmx.my_property" then + return true, "my_value" + end + end + + assert(proxy_wasm.set_property_getter(getter)) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_response_body/, + qr/\[info\] .*? property not found: wasmx.missing_property/, +] +--- no_error_log +[error] + + + +=== TEST 10: set_property_getter() - getter returning true on 3rd value caches result +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + access_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + local counter = 0 + + local function getter(key) + counter = counter + 1 + + ngx.log(ngx.INFO, "getting ", key, ", counter: ", + tostring(counter)) + + if key == "wasmx.dyn_property" then + return true, tostring(counter) + + elseif key == "wasmx.const_property" then + return true, tostring(counter), true + + elseif key == "wasmx.nil_dyn_property" then + return true, nil + + elseif key == "wasmx.nil_const_property" then + return true, nil, true + + elseif key == "wasmx.empty_dyn_property" then + return true, "" + + elseif key == "wasmx.empty_const_property" then + return true, "", true + end + end + + assert(proxy_wasm.set_property_getter(getter)) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + + log_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + + local function log_property(k) + local v = proxy_wasm.get_property(k) + ngx.log(ngx.INFO, k, ": ", type(v), " \"", v, "\"") + end + + -- four get_property operations, + -- but it will hit the getter three times + -- because the second call to get wasmx.const_property + -- hits the cache + log_property("wasmx.const_property") -- returns 1 + log_property("wasmx.dyn_property") -- returns 2 + log_property("wasmx.const_property") -- returns 1 + log_property("wasmx.dyn_property") -- returns 3 + + log_property("wasmx.nil_const_property") -- returns nil + log_property("wasmx.nil_dyn_property") -- returns nil + log_property("wasmx.nil_const_property") -- returns nil + log_property("wasmx.nil_dyn_property") -- returns nil + + log_property("wasmx.empty_const_property") -- returns "" + log_property("wasmx.empty_dyn_property") -- returns "" + log_property("wasmx.empty_const_property") -- returns "" + log_property("wasmx.empty_dyn_property") -- returns "" + } + } +--- response_body +ok +--- grep_error_log eval: qr/.*wasmx.*property.*/ +--- grep_error_log_out eval +qr/^[^\n]*?\[info\] [^\n]*? getting wasmx.const_property, counter: 1[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.const_property: string "1"[^\n]*? +[^\n]*?\[info\] [^\n]*? getting wasmx.dyn_property, counter: 2[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.dyn_property: string "2"[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.const_property: string "1"[^\n]*? +[^\n]*?\[info\] [^\n]*? getting wasmx.dyn_property, counter: 3[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.dyn_property: string "3"[^\n]*? +[^\n]*?\[info\] [^\n]*? getting wasmx.nil_const_property, counter: 4[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.nil_const_property: nil "nil"[^\n]*? +[^\n]*?\[info\] [^\n]*? getting wasmx.nil_dyn_property, counter: 5[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.nil_dyn_property: nil "nil"[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.nil_const_property: nil "nil"[^\n]*? +[^\n]*?\[info\] [^\n]*? getting wasmx.nil_dyn_property, counter: 6[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.nil_dyn_property: nil "nil"[^\n]*? +[^\n]*?\[info\] [^\n]*? getting wasmx.empty_const_property, counter: 7[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.empty_const_property: string ""[^\n]*? +[^\n]*?\[info\] [^\n]*? getting wasmx.empty_dyn_property, counter: 8[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.empty_dyn_property: string ""[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.empty_const_property: string ""[^\n]*? +[^\n]*?\[info\] [^\n]*? getting wasmx.empty_dyn_property, counter: 9[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.empty_dyn_property: string ""[^\n]*?$/ +--- no_error_log +[error] +[crit] + + + +=== TEST 11: set_property_getter() - errors in getter are caught by pcall in Lua library and don't crash the worker +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm hostcalls 'on=response_body \ + test=/t/log/property \ + name=wasmx.my_property'; + + rewrite_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + local function getter(key) + error("crash!") + end + + assert(proxy_wasm.set_property_getter(getter)) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_response_body/, + qr/\[error\] .*? Lua error in property handler: rewrite_by_lua\(nginx.conf:[0-9]+\):[0-9]+: crash\!/, +] +--- no_error_log +[emerg] diff --git a/t/04-openresty/ffi/305-proxy_wasm_set_property_setter.t b/t/04-openresty/ffi/305-proxy_wasm_set_property_setter.t new file mode 100644 index 000000000..04f05af05 --- /dev/null +++ b/t/04-openresty/ffi/305-proxy_wasm_set_property_setter.t @@ -0,0 +1,616 @@ +# vim:set ft= ts=4 sw=4 et fdm=marker: + +use strict; +use lib '.'; +use t::TestWasm::Lua; + +skip_no_openresty(); + +plan tests => repeat_each() * (blocks() * 5); + +run_tests(); + +__DATA__ + +=== TEST 1: set_property_setter() - setting host properties setter fails on init_worker_by_lua +--- wasm_modules: on_phases +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "on_phases" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + + local tbl = {} + + local function setter(key, value) + tbl[key] = value + return true + end + + assert(proxy_wasm.set_property_setter(setter)) + } +--- config + location /t { + rewrite_by_lua_block { + ngx.say("ok") + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[error\] .*? cannot set host properties setter outside of a request/, + qr/\[error\] .*? init_worker_by_lua(\(nginx\.conf:\d+\))?:\d+: could not set property setter/, +] +--- no_error_log +[crit] + + + +=== TEST 2: set_property_setter() - setting host properties setter works on rewrite_by_lua +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm hostcalls 'on=response_body \ + test=/t/log/property \ + name=wasmx.my_property'; + + rewrite_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + local tbl = {} + + local function setter(key, value) + local new_value = value:upper() + tbl[key] = new_value + return true, new_value + end + + assert(proxy_wasm.set_property_setter(setter)) + + assert(proxy_wasm.set_property("wasmx.my_property", "my_value")) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_response_body/, + qr/\[info\] .*? wasmx.my_property: MY_VALUE/, +] +--- no_error_log +[error] + + + +=== TEST 3: set_property_setter() - setting host properties setter works on access_by_lua +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm hostcalls 'on=response_body \ + test=/t/log/property \ + name=wasmx.my_property'; + + access_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + local tbl = {} + + local function setter(key, value) + local new_value = value:upper() + tbl[key] = new_value + return true, new_value + end + + assert(proxy_wasm.set_property_setter(setter)) + + assert(proxy_wasm.set_property("wasmx.my_property", "my_value")) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_response_body/, + qr/\[info\] .*? wasmx.my_property: MY_VALUE/, +] +--- no_error_log +[error] + + + +=== TEST 4: set_property_setter() - setting host properties setter works on header_filter_by_lua +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm hostcalls 'on=response_body \ + test=/t/log/property \ + name=wasmx.my_property'; + + rewrite_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + + header_filter_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + + local tbl = {} + + local function setter(key, value) + local new_value = value:upper() + tbl[key] = new_value + return true, new_value + end + + assert(proxy_wasm.set_property_setter(setter)) + + assert(proxy_wasm.set_property("wasmx.my_property", "my_value")) + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_response_body/, + qr/\[info\] .*? wasmx.my_property: MY_VALUE/, +] +--- no_error_log +[error] + + + +=== TEST 5: set_property_setter() - setting host properties setter works on body_filter_by_lua +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm hostcalls 'on=response_body \ + test=/t/log/property \ + name=wasmx.my_property'; + + rewrite_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + + body_filter_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + + local tbl = {} + + local function setter(key, value) + local new_value = value:upper() + tbl[key] = new_value + return true, new_value + end + + assert(proxy_wasm.set_property_setter(setter)) + + assert(proxy_wasm.set_property("wasmx.my_property", "my_value")) + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_response_body/, + qr/\[info\] .*? wasmx.my_property: MY_VALUE/, +] +--- no_error_log +[error] + + + +=== TEST 6: set_property_setter() - setting host properties setter works on log_by_lua +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm_request_headers_in_access on; + + proxy_wasm hostcalls 'on=log \ + test=/t/log/property \ + name=wasmx.my_property'; + + access_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + + log_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + + local tbl = {} + + local function setter(key, value) + local new_value = value:upper() + tbl[key] = new_value + return true, new_value + end + + assert(proxy_wasm.set_property_setter(setter)) + + assert(proxy_wasm.set_property("wasmx.my_property", "my_value")) + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_log/, + qr/\[info\] .*? wasmx.my_property: MY_VALUE/, +] +--- no_error_log +[error] + + + +=== TEST 7: set_property_setter() - setting property setter at startup doesn't reset filter list +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls", config = "on=log " .. + "test=/t/log/property " .. + "name=wasmx.my_property" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm_request_headers_in_access on; + + access_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + + assert(proxy_wasm.attach(_G.c_plan)) + + local function setter(key, value) + if key == "wasmx.my_property" then + return true, "foo" + end + return false + end + + assert(proxy_wasm.set_property_setter(setter)) + + assert(proxy_wasm.start()) + + ngx.say("ok") + } + + body_filter_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + + assert(proxy_wasm.set_property("wasmx.my_property", "bar")) + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_log/, + qr/\[info\] .*?hostcalls.*? wasmx.my_property: foo/, +] +--- no_error_log +[error] + + + +=== TEST 8: set_property_setter() - setter returning falsy logs custom message or "unknown error" +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + proxy_wasm hostcalls 'on=log \ + test=/t/log/property \ + name=wasmx.fail1_property'; + + location /t { + access_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + local function setter(key) + if key == "wasmx.fail1_property" then + return nil, "first wasmx property custom error" + + elseif key == "wasmx.fail2_property" then + return false, "second wasmx property custom error" + + elseif key == "wasmx.fail3_property" then + return nil + + elseif key == "wasmx.fail4_property" then + return false + end + end + + assert(proxy_wasm.set_property_setter(setter)) + + local msg = "result:" + for i = 1, 4 do + local key = "wasmx.fail" .. i .. "_property" + local ok, err = proxy_wasm.set_property(key, "wat") + if not ok then + msg = msg .. "\n" .. err + end + end + + assert(proxy_wasm.start()) + ngx.say(msg) + } + } +--- response_body +result: +unknown error +unknown error +unknown error +unknown error +--- grep_error_log eval: qr/.*property.*/ +--- grep_error_log_out eval +qr/^[^\n]*?\[error\] [^\n]*? error in property handler: first wasmx property custom error[^\n]*? +[^\n]*?\[error\] [^\n]*? error in property handler: second wasmx property custom error[^\n]*? +[^\n]*?\[error\] [^\n]*? error in property handler: unknown error[^\n]*? +[^\n]*?\[error\] [^\n]*? error in property handler: unknown error[^\n]*? +[^\n]*?\[info\] [^\n]*? property not found: wasmx.fail1_property[^\n]*?$/ +--- no_error_log +[crit] +[alert] + + + +=== TEST 9: set_property_setter() - setter returning true on 3rd value caches result +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + access_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + local counter = 0 + + local tbl = {} + + local function setter(key, value) + counter = counter + 1 + + if value and #value > 0 then + value = value:upper() .. " " .. tostring(counter) + end + + tbl[key] = value + + ngx.log(ngx.INFO, "setting ", key, " to ", value, ", counter: " .. counter) + + return true, value, (key:match("const") and true or false) + end + + assert(proxy_wasm.set_property_setter(setter)) + + local function getter(key) + counter = counter + 1 + + ngx.log(ngx.INFO, "getting ", key, " via getter, counter: ", counter) + + local value = tbl[key] + + if value and #value > 0 then + value = value .. " " .. tostring(counter) + end + + return true, value + end + + assert(proxy_wasm.set_property_getter(getter)) + + assert(proxy_wasm.start()) + ngx.say("ok") + } + + log_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + + local function log_property(k) + local v = proxy_wasm.get_property(k) + ngx.log(ngx.INFO, k, ": ", type(v), " \"", v, "\"") + end + + -- two set_property operations, each will hit the setter. + -- four get_property operations, + -- but only wasmx.dyn_property will hit the getter + -- because the setter to wasmx.const_property cached the result. + proxy_wasm.set_property("wasmx.const_property", "hello") -- sets "HELLO 1" + log_property("wasmx.const_property") -- returns "HELLO 1" + proxy_wasm.set_property("wasmx.dyn_property", "hi") -- sets "HI 2" + log_property("wasmx.dyn_property") -- returns "HI 2 3" + log_property("wasmx.const_property") -- returns "HELLO 1" + log_property("wasmx.dyn_property") -- returns "HI 2 4" + + proxy_wasm.set_property("wasmx.nil_const_property", nil) -- sets nil + log_property("wasmx.nil_const_property") -- returns nil + proxy_wasm.set_property("wasmx.nil_dyn_property", nil) -- sets nil + log_property("wasmx.nil_dyn_property") -- returns nil + log_property("wasmx.nil_const_property") -- returns nil + log_property("wasmx.nil_dyn_property") -- returns nil + + proxy_wasm.set_property("wasmx.empty_const_property", "") -- sets "" + log_property("wasmx.empty_const_property") -- returns "" + proxy_wasm.set_property("wasmx.empty_dyn_property", "") -- sets "" + log_property("wasmx.empty_dyn_property") -- returns "" + log_property("wasmx.empty_const_property") -- returns "" + log_property("wasmx.empty_dyn_property") -- returns "" + } + } +--- response_body +ok +--- grep_error_log eval: qr/.*wasmx.*property.*/ +--- grep_error_log_out eval +qr/^[^\n]*?\[info\] [^\n]*? setting wasmx.const_property to HELLO 1, counter: 1[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.const_property: string "HELLO 1"[^\n]*? +[^\n]*?\[info\] [^\n]*? setting wasmx.dyn_property to HI 2, counter: 2[^\n]*? +[^\n]*?\[info\] [^\n]*? getting wasmx.dyn_property via getter, counter: 3[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.dyn_property: string "HI 2 3"[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.const_property: string "HELLO 1"[^\n]*? +[^\n]*?\[info\] [^\n]*? getting wasmx.dyn_property via getter, counter: 4[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.dyn_property: string "HI 2 4"[^\n]*? +[^\n]*?\[info\] [^\n]*? setting wasmx.nil_const_property to nil, counter: 5[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.nil_const_property: nil "nil"[^\n]*? +[^\n]*?\[info\] [^\n]*? setting wasmx.nil_dyn_property to nil, counter: 6[^\n]*? +[^\n]*?\[info\] [^\n]*? getting wasmx.nil_dyn_property via getter, counter: 7[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.nil_dyn_property: nil "nil"[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.nil_const_property: nil "nil"[^\n]*? +[^\n]*?\[info\] [^\n]*? getting wasmx.nil_dyn_property via getter, counter: 8[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.nil_dyn_property: nil "nil"[^\n]*? +[^\n]*?\[info\] [^\n]*? setting wasmx.empty_const_property to , counter: 9[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.empty_const_property: string ""[^\n]*? +[^\n]*?\[info\] [^\n]*? setting wasmx.empty_dyn_property to , counter: 10[^\n]*? +[^\n]*?\[info\] [^\n]*? getting wasmx.empty_dyn_property via getter, counter: 11[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.empty_dyn_property: string ""[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.empty_const_property: string ""[^\n]*? +[^\n]*?\[info\] [^\n]*? getting wasmx.empty_dyn_property via getter, counter: 12[^\n]*? +[^\n]*?\[info\] [^\n]*? wasmx.empty_dyn_property: string ""[^\n]*?$/ +--- no_error_log +[error] +[crit] + + + +=== TEST 10: set_property_setter() - errors in setter are caught by pcall in Lua library and don't crash the worker +--- wasm_modules: hostcalls +--- http_config + init_worker_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + local filters = { + { name = "hostcalls" }, + } + + _G.c_plan = assert(proxy_wasm.new(filters)) + assert(proxy_wasm.load(_G.c_plan)) + } +--- config + location /t { + proxy_wasm hostcalls 'on=response_body \ + test=/t/log/property \ + name=wasmx.my_property'; + + rewrite_by_lua_block { + local proxy_wasm = require "resty.wasmx.proxy_wasm" + assert(proxy_wasm.attach(_G.c_plan)) + + local tbl = {} + + local function setter(key, value) + error("crash!") + end + + assert(proxy_wasm.set_property_setter(setter)) + + local ok, err = proxy_wasm.set_property("wasmx.my_property", "my_value") + assert((not ok) and err == "unknown error") + + assert(proxy_wasm.start()) + ngx.say("ok") + } + } +--- response_body +ok +--- error_log eval +[ + qr/\[info\] .*? \[hostcalls\] on_response_body/, + qr/\[error\] .*? Lua error in property handler: rewrite_by_lua\(nginx.conf:[0-9]+\):[0-9]+: crash\!/, +] +--- no_error_log +[emerg]