From 04016dd8b29006eb4a8a223152b57312c49f0c74 Mon Sep 17 00:00:00 2001 From: Hisham Muhammad Date: Sun, 15 Oct 2023 12:42:33 -0300 Subject: [PATCH] feat(proxy-wasm) host-managed property getters and setters This feature adds support for host-managed property getter and setter functions. These feature is implemented in three stages: * `ngx_proxy_wasm_properties.c`: in our implementation for proxy-wasm properties, we now allow the host to specify a function which gets called whenever proxy-wasm receives a `set_property` or `get_property` call matching the host-defined namespace. Those getter and setter functions are then responsible to return and store the values, and are able to produce any desired side-effects in the host. * `ngx_wasm_lua_ffi.c`: We then expose this functionality in our Lua FFI interface. This is a lightweight wrapper, which accepts a C function pointer directly, and associates this function to the request context object. * `lib/resty/wasmx/proxy_wasm.lua`: Finally, we expose a Lua API for setting a Lua functions as setters and getters. The interface is such that the host Lua program should set one setter function and/or getter function, and then within that function properly dispatch the behavior based on the received key. Internally, these functions use the fact that LuaJIT is able to convert a Lua function into a C function pointer. Due to lifetime management, those generated C callbacks are a limited resource in the LuaJIT VM, so we produce a single pair of these and then have them call the user-defined Lua function, which remains as a normal Lua closure. (See http://luajit.org/ext_ffi_semantics.html for more details on LuaJIT FFI C callback managment.) To ensure that the lifetime of strings produced by the host continue valid by the time the getter callback finished running and the Wasm engine reads the properties, getter/setter-produced values continue to be stored in the rbtree for host properties that we already had for plain `get_property`/`set_property`, introduced in da820875ed7. Since we need to store this data on the C side anyway, we also add an extra feature as an optimization: the concept of "const" host-properties. If the Lua callback returns a truthy third-argument, that signals that that value should be flagged as constant. Further calls to `get_property` during the same request will get the property value directly from the ngx_wasm_module rbtree "cache" without hitting the getter callback again. Test cases in the test suite demonstrate this feature. In this PR, the host namespace continues to be statically defined at compile time via the NGX_WASM_HOST_PROPERTY_NAMESPACE define. The regular FFI functions for `set_property` and `get_property` introduced in 447ef130a9 also continue to work normally. --- lib/resty/wasmx.lua | 4 + lib/resty/wasmx/proxy_wasm.lua | 198 +++++- src/common/lua/ngx_wasm_lua_ffi.c | 58 ++ src/common/lua/ngx_wasm_lua_ffi.h | 6 + src/common/proxy_wasm/ngx_proxy_wasm.h | 9 +- .../proxy_wasm/ngx_proxy_wasm_properties.c | 132 +++- .../proxy_wasm/ngx_proxy_wasm_properties.h | 6 + .../ffi/304-proxy_wasm_set_property_getter.t | 597 +++++++++++++++++ .../ffi/305-proxy_wasm_set_property_setter.t | 616 ++++++++++++++++++ 9 files changed, 1586 insertions(+), 40 deletions(-) create mode 100644 t/04-openresty/ffi/304-proxy_wasm_set_property_getter.t create mode 100644 t/04-openresty/ffi/305-proxy_wasm_set_property_setter.t 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..7934c4fd6 100644 --- a/lib/resty/wasmx/proxy_wasm.lua +++ b/lib/resty/wasmx/proxy_wasm.lua @@ -8,9 +8,11 @@ local C = ffi.C local ngx = ngx local error = error local type = type +local pcall = pcall local ffi_gc = ffi.gc local ffi_new = ffi.new local ffi_str = ffi.string +local tostring = tostring local subsystem = ngx.config.subsystem local get_request = wasmx.get_request local get_err_ptr = wasmx.get_err_ptr @@ -38,23 +40,32 @@ 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_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); ]] else @@ -67,6 +78,86 @@ local ffi_ops_type = ffi.typeof("ngx_wasm_plan_t *") local ffi_pops_type = ffi.typeof("ngx_wasm_plan_t *[1]") +local lua_prop_setter +local lua_prop_getter +local c_prop_setter +local c_prop_getter + + +do + -- LuaJIT C callbacks are a limited resource: + -- We can't create one of these on each request, + -- so we prepare these wrappers, which in turn call a Lua function. + + local function store_c_value(r, ckey, cvalue, lvalue, is_const) + if lvalue ~= nil then + local value = tostring(lvalue) + cvalue.data = value + cvalue.len = #value + + else + cvalue.data = nil + cvalue.len = 0 + end + + return C.ngx_http_wasm_ffi_set_host_property(r, ckey, cvalue, + is_const and 1 or 0, + 1) + end + + c_prop_setter = ffi.cast("ngx_wasm_host_prop_fn_t", function(r, ckey, + cvalue) + local lkey = ffi_str(ckey.data, ckey.len) + local lval + if cvalue.data ~= nil then + lval = ffi_str(cvalue.data, cvalue.len) + end + + local pok, ok, lvalue, is_const = pcall(lua_prop_setter, lkey, lval) + if not pok then + ngx.log(ngx.ERR, "error setting property from Lua: ", ok) + return FFI_ERROR + end + + if not ok then + local err = lvalue or "unknown error" + ngx.log(ngx.ERR, "error setting property: ", err) + return FFI_ERROR + end + + return store_c_value(r, ckey, cvalue, lvalue, is_const) + end) + + + c_prop_getter = ffi.cast("ngx_wasm_host_prop_fn_t", function(r, ckey, + cvalue) + local lkey = ffi_str(ckey.data, ckey.len) + + local pok, ok, lvalue, is_const = pcall(lua_prop_getter, lkey) + if not pok then + ngx.log(ngx.ERR, "error getting property from Lua: ", ok) + return FFI_ERROR + end + + if not ok then + if lvalue then + ngx.log(ngx.ERR, "error setting property: ", lvalue) + return FFI_ERROR + end + + return FFI_DECLINED + end + + local rc = store_c_value(r, ckey, cvalue, lvalue, is_const) + if rc == FFI_OK and lvalue == nil then + return FFI_DECLINED + end + + return rc + end) +end + + function _M.new(filters) if type(filters) ~= "table" then error("filters must be a table", 2) @@ -229,7 +320,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 @@ -239,7 +330,7 @@ function _M.set_property(key, value) end local ckey = ffi_new("ngx_str_t", { data = key, len = #key }) - local cvalue = ffi_new("ngx_str_t", { data = value, len = #value }) + 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 @@ -280,4 +371,73 @@ function _M.get_property(key) 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 + + lua_prop_setter = setter + + local rc = C.ngx_http_wasm_ffi_set_property_setter(r, c_prop_setter) + if rc == FFI_OK then + return true + end + + return nil, "could not set property setter" +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 + + lua_prop_getter = getter + + local rc = C.ngx_http_wasm_ffi_set_property_getter(r, c_prop_getter) + if rc == FFI_OK then + return true + end + + return nil, "could not set property getter" +end + + return _M diff --git a/src/common/lua/ngx_wasm_lua_ffi.c b/src/common/lua/ngx_wasm_lua_ffi.c index 0516a39b1..c875c748f 100644 --- a/src/common/lua/ngx_wasm_lua_ffi.c +++ b/src/common/lua/ngx_wasm_lua_ffi.c @@ -180,6 +180,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 +241,47 @@ ngx_http_wasm_ffi_get_property(ngx_http_request_t *r, return ngx_proxy_wasm_properties_get(pwctx, key, 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_proxy_wasm_ctx_t *pwctx = get_pwctx(r); + + if (pwctx == NULL) { + return NGX_ERROR; + } + + return ngx_proxy_wasm_properties_set_host(pwctx, key, value, is_const, + retrieve); +} + + +ngx_int_t +ngx_http_wasm_ffi_set_property_setter(ngx_http_request_t *r, + ngx_wasm_host_prop_fn_t fn) +{ + 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, fn, r); +} + + +ngx_int_t +ngx_http_wasm_ffi_set_property_getter(ngx_http_request_t *r, + ngx_wasm_host_prop_fn_t fn) +{ + 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, fn, r); +} #endif diff --git a/src/common/lua/ngx_wasm_lua_ffi.h b/src/common/lua/ngx_wasm_lua_ffi.h index 0178058e1..1d87e22d1 100644 --- a/src/common/lua/ngx_wasm_lua_ffi.h +++ b/src/common/lua/ngx_wasm_lua_ffi.h @@ -33,6 +33,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..6f865ea59 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 negative_cache:1; } host_props_node_t; @@ -877,24 +879,33 @@ 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) { + 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; + value->data = hpn->value.data; + value->len = hpn->value.len; + + if (hpn->negative_cache) { + return NGX_BUSY; } - return NGX_DECLINED; + return NGX_OK; } -static ngx_int_t +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 char *new_data; unsigned new_entry = 1; #ifdef NGX_WASM_HTTP ngx_http_wasm_req_ctx_t *rctx = pwctx->data; @@ -911,16 +922,19 @@ 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) { - ngx_rbtree_delete(&pwctx->host_props_tree, &hpn->sn.node); + if (hpn) { + if (hpn->is_const) { + return NGX_DECLINED; } - return NGX_OK; - } - - if (hpn) { ngx_pfree(pwctx->pool, hpn->value.data); + + if (value->data == NULL && !is_const) { + ngx_rbtree_delete(&pwctx->host_props_tree, &hpn->sn.node); + + return NGX_OK; + } + new_entry = 0; } else { @@ -935,16 +949,29 @@ ngx_proxy_wasm_properties_set_host(ngx_proxy_wasm_ctx_t *pwctx, 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; + if (value->data == NULL) { + new_data = NULL; + hpn->negative_cache = 1; + + } else { + new_data = ngx_pstrdup(pwctx->pool, value); + if (new_data == NULL) { + return NGX_ERROR; + } } + hpn->is_const = is_const; + hpn->value.len = value->len; + hpn->value.data = new_data; + 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 +997,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 +1025,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 +1076,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..e6c8f764f --- /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\] .*? error getting property from Lua: 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..471385d3d --- /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 setting property: first wasmx property custom error[^\n]*? +[^\n]*?\[error\] [^\n]*? error setting property: second wasmx property custom error[^\n]*? +[^\n]*?\[error\] [^\n]*? error setting property: unknown error[^\n]*? +[^\n]*?\[error\] [^\n]*? error setting property: 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\] .*? error setting property from Lua: rewrite_by_lua\(nginx.conf:[0-9]+\):[0-9]+: crash\!/, +] +--- no_error_log +[emerg]