From 7f8e86fbd84eece36f3833c99cf7e2f53d8d3ed6 Mon Sep 17 00:00:00 2001 From: Luna Date: Thu, 26 Oct 2023 22:19:08 -0300 Subject: [PATCH 1/9] lint pass --- config.lua | 27 ++++++--------------------- ctx.lua | 12 ++++++++---- main.lua | 8 -------- scripts/webfinger_allowlist.lua | 4 ++-- test.lua | 6 +++--- util.lua | 33 ++++++++++++++++++++++++++++----- 6 files changed, 47 insertions(+), 43 deletions(-) diff --git a/config.lua b/config.lua index 278ee9b..451a3e7 100644 --- a/config.lua +++ b/config.lua @@ -2,19 +2,8 @@ local env_config_path = os.getenv('APROXY_CONFIG_PATH') local DEFAULT_CONFIG_PATH = ".;/etc/aproxy" local config_path = env_config_path or DEFAULT_CONFIG_PATH -function mysplit (inputstr, sep) - if sep == nil then - sep = "%s" - end - local t={} - for str in string.gmatch(inputstr, "([^"..sep.."]+)") do - table.insert(t, str) - end - return t -end - local function findConfigFile() - for _, config_directory in ipairs(mysplit(config_path, ";")) do + for _, config_directory in ipairs(string.split(config_path, ";")) do local possible_config_path = config_directory .. "/" .. "conf.lua" local fd, res = io.open(possible_config_path, "rb") if fd then @@ -37,7 +26,7 @@ end local function fakeTableSchema(value_schema) return setmetatable({ __list = true }, { - __index = function (self, key) + __index = function () return value_schema end }) @@ -45,8 +34,8 @@ end local SPECIAL_KEYS = {__list = true} -local function validateSchema(schema, input, errors) - local errors = errors or {} +local function validateSchema(schema, input, errors_in) + local errors = errors_in or {} if schema.__list then -- generate full schema for lists that are same size as input for k, _ in pairs(input) do schema[k] = schema.__schema_value end @@ -105,12 +94,8 @@ local function validateConfigFile(config_object) return all_schema_errors end -local function writeSchemaErrors(errors, out) - out('sex') -end - -local function loadConfigFile(options) - local options = options or {} +local function loadConfigFile(options_in) + local options = options_in or {} local config_file_data = assert(findConfigFile(), 'no config file found, config path: ' .. config_path) local config_file_function = assert(loadstring(config_file_data)) diff --git a/ctx.lua b/ctx.lua index 78e3239..fb43089 100644 --- a/ctx.lua +++ b/ctx.lua @@ -1,3 +1,5 @@ +local util = require("util") + function log(msg) ngx.log(ngx.STDERR, tostring(msg)) end @@ -13,13 +15,14 @@ function ctx:loadFromConfig(conf) ctx:loadChain() end + function ctx:loadChain() self.compiled_chain = {} for module_name, module_config in pairs(self._wanted_scripts) do local module = require('scripts.' .. module_name) - local module_state = module.init(module_config) - -- TODO is it possible to make module_config readonly? - table.insert(self.compiled_chain, {module, module_config, module_state}) + local module_config_readonly = table.readonly(module_config) + local module_state = module.init(module_config_readonly) + table.insert(self.compiled_chain, {module, module_config_readonly, module_state}) end end @@ -32,7 +35,7 @@ function ctx:onRequest() local module, module_config, state = unpack(filter) for callback_regex, callback_function in pairs(module.callbacks) do - local match, error = ngx.re.match(request_uri, callback_regex) + local match = ngx.re.match(request_uri, callback_regex) if match then table.insert(callbacks_to_call, {module, callback_function, module_config, state}) end @@ -47,6 +50,7 @@ function ctx:onRequest() ngx.status = status_code ngx.say(body or "request denied") ngx.exit(status_code) + return end end end diff --git a/main.lua b/main.lua index b0cf8ff..0470522 100644 --- a/main.lua +++ b/main.lua @@ -1,11 +1,3 @@ - --- function loadConfig() --- -- TODO load config_path --- return require("./config.lua") --- end --- --- local config = loadConfig() - local ctx = require('ctx') local config = require('config') require('util') diff --git a/scripts/webfinger_allowlist.lua b/scripts/webfinger_allowlist.lua index 41d5c80..7e7355f 100644 --- a/scripts/webfinger_allowlist.lua +++ b/scripts/webfinger_allowlist.lua @@ -1,4 +1,4 @@ -function webfingerInit(cfg) +local function webfingerInit(cfg) local accounts_set = {} for _, account in ipairs(cfg.accounts) do accounts_set["acct:" .. account] = true @@ -6,7 +6,7 @@ function webfingerInit(cfg) return accounts_set end -function webfingerCallback(cfg, accounts_set) +local function webfingerCallback(cfg, accounts_set) local args, err = ngx.req.get_uri_args() if err == "truncated" then return 400, 'uri args too long' diff --git a/test.lua b/test.lua index 968117b..ded2270 100644 --- a/test.lua +++ b/test.lua @@ -64,7 +64,7 @@ function setupTest(module_require_path, input_config) local count = table.pprint(schema_errors) lu.assertIs(count, 0) - state = module.init(input_config) + local state = module.init(input_config) ctx.compiled_chain = { {module, input_config, state} } @@ -74,9 +74,9 @@ end function onRequest() ctx:setWantedScripts() - local ctx = require('ctx') + local context = require('ctx') do - ctx:onRequest() + context:onRequest() end end diff --git a/util.lua b/util.lua index 4db2229..5c0b74f 100644 --- a/util.lua +++ b/util.lua @@ -4,11 +4,11 @@ function table.len(t) return count end -function table.pprint(t, options, ident, total_count) - local ident = ident or 0 - local total_count = total_count or 0 +function table.pprint(t, options_in, ident_in, total_count_in) + local ident = ident_in or 0 + local total_count = total_count_in or 0 - local options = options or {} + local options = options_in or {} local print_function = options.call or print if type(t) == 'table' then local count = 0 @@ -18,7 +18,7 @@ function table.pprint(t, options, ident, total_count) total_count = table.pprint(v, options, ident + 1, total_count) end if count == 0 then - --print('') + print_function('{}') end else print_function(string.rep('\t', ident) .. tostring(t)) @@ -26,3 +26,26 @@ function table.pprint(t, options, ident, total_count) end return total_count end + +function table.readonly(t) + return setmetatable({}, { + __index = t, + __newindex = function () + error("Attempt to modify read-only table") + end, + __metatable = false + }); +end + +function string.split(inputstr, sep) + if sep == nil then + sep = "%s" + end + local t={} + for str in string.gmatch(inputstr, "([^"..sep.."]+)") do + table.insert(t, str) + end + return t +end + + From 84ff7bd1b61842e21b4fb325e7123601b300f56d Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 16 Feb 2024 02:13:37 -0300 Subject: [PATCH 2/9] update docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 02e33d7..2de4b84 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ http { # while the friction to quickly debug aproxy will also be increased lua_code_cache off; access_by_lua_block { - require("aproxy.main")() + require("aproxy.main").access() } proxy_http_version 1.1; From 2c1f1553286c79467c9d7e7b0789d4e3b2914c4c Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 16 Feb 2024 17:56:06 -0300 Subject: [PATCH 3/9] update README --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++-------- nginx.conf | 2 +- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2de4b84..c5bd565 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,37 @@ It is an effective replacement to your NGINX installation, but you can have them coexisting (say, NGINX on port 80, OpenResty being reverse proxied by NGINX on port 8069, though I wouldn't recommend it in production environments). +### how does it work + +aproxy has two "hooks" into openresty: + - initialization of the lua vm + - callback for every incoming request + +initialization will run validation of your configuration file and check +if it is valid. if it is not you will see logs emitted about what failed + +when a request comes in, the scripts declared in the aproxy config file will +be executed sequentially (TODO: ensure order on conf file is chain order). + +each script has two types of callbacks: init and request + +init callbacks are called when initializing the request, so that the script +may do some conversion or transform the config data into a more performant +in-memory structure. + +request callbacks are called on each request, as directed by the main script. +scripts define which paths they want to attach to (via PCRE regex), and so +they can do their own filtering. + +look at `scripts/webfinger_allowlist.lua` for an example of how this looks +in a simple form. + ### actually installing aproxy +- get openresty installed + - keep in mind that the specifics of configuring openresty for a double reverse proxy setup aren't included here. + - instructions here are for aproxy's setup in an existing openresty installation + ```sh mkdir /opt git clone https://gitdab.com/luna/aproxy @@ -28,6 +57,9 @@ git clone https://gitdab.com/luna/aproxy cd aproxy mkdir /etc/aproxy cp ./conf.lua /etc/aproxy/conf.lua + +# keep in mind the default configuration will lead to your users not being discovered at all, +# it is provided as an example for you to modify. $EDITOR /etc/aproxy/conf.lua ``` @@ -51,21 +83,24 @@ http { You need to do the following: - Configure OpenResty package path so that it can call aproxy. - - Insert aproxy as a callback on `location / {` block + - insert aproxy hooks for initialization and for callbacks on every request -It'll look something like this: +It'll look something like this if you use a single `location /` block: ```nginx +# set this to 'on' after you have tested that it actually works. +# once you do that, performance will be increased +# while the friction to quickly debug aproxy will also be increased +lua_code_cache off; +lua_package_path '/opt/?.lua;/opt/aproxy/?.lua;;'; +init_by_lua_block { + require("aproxy.main").init() +} + http { - lua_package_path '/opt/?.lua;/opt/aproxy/?.lua;;'; - server { server_name example.com; location / { - # set this to 'on' after you have tested that it actually works. - # once you do that, performance will be increased - # while the friction to quickly debug aproxy will also be increased - lua_code_cache off; access_by_lua_block { require("aproxy.main").access() } diff --git a/nginx.conf b/nginx.conf index 4b6e8bf..4904508 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,10 +1,10 @@ +lua_code_cache off; init_by_lua_block { require("aproxy.main").init() } server { listen 80; - lua_code_cache off; location / { default_type text/html; From 8434492b18743679b8e78cd3bbf1887125a96ff4 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 2 May 2025 18:43:27 -0300 Subject: [PATCH 4/9] add pleroma_restrict_unauthenticated_search.lua --- ...leroma_restrict_unauthenticated_search.lua | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 scripts/pleroma_restrict_unauthenticated_search.lua diff --git a/scripts/pleroma_restrict_unauthenticated_search.lua b/scripts/pleroma_restrict_unauthenticated_search.lua new file mode 100644 index 0000000..6b7904b --- /dev/null +++ b/scripts/pleroma_restrict_unauthenticated_search.lua @@ -0,0 +1,35 @@ +local function webfingerInit(cfg) + return {} -- no ctx +end + +local function webfingerCallback(cfg, _ctx) + local h, err = ngx.req.get_headers() + + if err == "truncated" then + return 400, 'too many headers' + end + + local authheader = h["authorization"] + + if authheader == nil then + return 400, "requires authentication" + else + return nil + end +end + +return { + name='PleromaRestrictUnauthenticatedSearch', + author='luna@l4.pm', + title='restrict unauth search', + description=[[ + Search can be a DoS vector. restrict it without Authorization header. + Useful for small instances. + ]], + version=1, + init=searchInit, + callbacks = { + ['/api/v2/search'] = searchCallback + }, + config={}, +} From 6e3705a6df1a953e903cf2c597412207379f4c49 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 2 May 2025 18:44:53 -0300 Subject: [PATCH 5/9] fix typos --- scripts/pleroma_restrict_unauthenticated_search.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/pleroma_restrict_unauthenticated_search.lua b/scripts/pleroma_restrict_unauthenticated_search.lua index 6b7904b..b907dd3 100644 --- a/scripts/pleroma_restrict_unauthenticated_search.lua +++ b/scripts/pleroma_restrict_unauthenticated_search.lua @@ -1,8 +1,8 @@ -local function webfingerInit(cfg) +local function searchInit(cfg) return {} -- no ctx end -local function webfingerCallback(cfg, _ctx) +local function searchCallback(cfg, _ctx) local h, err = ngx.req.get_headers() if err == "truncated" then From 6e9f673d91f89e18e078843fb5768248f36310f3 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 2 May 2025 18:47:54 -0300 Subject: [PATCH 6/9] return 401 --- scripts/pleroma_restrict_unauthenticated_search.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/pleroma_restrict_unauthenticated_search.lua b/scripts/pleroma_restrict_unauthenticated_search.lua index b907dd3..965f311 100644 --- a/scripts/pleroma_restrict_unauthenticated_search.lua +++ b/scripts/pleroma_restrict_unauthenticated_search.lua @@ -12,7 +12,7 @@ local function searchCallback(cfg, _ctx) local authheader = h["authorization"] if authheader == nil then - return 400, "requires authentication" + return 401, "requires authentication" else return nil end From 08f17f812b6b33a248b822f4f16e9d7758a90ae3 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 2 May 2025 18:47:58 -0300 Subject: [PATCH 7/9] log when config is found --- config.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/config.lua b/config.lua index 451a3e7..0de4de9 100644 --- a/config.lua +++ b/config.lua @@ -7,6 +7,7 @@ local function findConfigFile() local possible_config_path = config_directory .. "/" .. "conf.lua" local fd, res = io.open(possible_config_path, "rb") if fd then + log('config found at ' .. possible_config_path) local data = fd:read("*a") fd:close() return data From b7f63801d4d3df7c7b0e72476620f70e4140a042 Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 22 Nov 2025 01:13:47 -0300 Subject: [PATCH 8/9] add CLAUDE.md --- CLAUDE.md | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..66f46f8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,135 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +aproxy is an Activity Pub Reverse Proxy Framework built on OpenResty (NGINX + LuaJIT). It provides a modular script system to add filtering and protection capabilities in front of ActivityPub implementations. + +## Development Commands + +### Testing +```sh +# Install test dependencies (only needed once) +make testdeps +eval (luarocks-5.1 path --bin) + +# Run test suite +make test +``` + +The test suite uses luajit and luaunit for testing. Tests are located in the `tests/` directory. + +## Architecture + +### Core Components + +- **main.lua**: Entry point with two hooks: + - `init()`: Validates configuration at startup + - `access()`: Called on every request to execute the filter chain + +- **config.lua**: Configuration system + - Searches for `conf.lua` in paths: `.;/etc/aproxy` (or `$APROXY_CONFIG_PATH`) + - Provides schema validation framework + - Schema supports types: `string`, `number`, `table`, `list` (array of values) + +- **ctx.lua**: Request context and filter chain executor + - Loads scripts from config and builds `compiled_chain` + - Matches request URIs against script PCRE regexes + - Executes matching callbacks sequentially + - If any callback returns a status code, request is terminated with that response + +- **util.lua**: Lua standard library extensions + - `table.readonly()`: Create read-only table wrapper + - `table.pprint()`: Pretty-print nested tables + - `string.split()`: Split strings by separator + +### Script Module Structure + +Scripts live in `scripts/` and must export: + +```lua +return { + name = 'ModuleName', + author = 'email', + title = 'Short Title', + description = [[Long description]], + version = 1, + + -- Called once at startup with config + -- Return value becomes module state + init = function(cfg) + return state + end, + + -- Map of PCRE regex patterns to callback functions + callbacks = { + ['/api/path'] = function(cfg, state) + -- Return nil to allow request + -- Return status_code, body to block request + return nil + end + }, + + -- Schema for validating this module's config + config = { + ['field_name'] = { + type = 'string|number|table|list', + schema = {...}, -- for table/list types + description = 'field description' + } + } +} +``` + +**Important**: Module configs are made read-only via `table.readonly()` before being passed to callbacks. Attempting to modify them will error. + +### Request Flow + +1. OpenResty calls `aproxy.main.access()` on each request +2. `ctx:onRequest()` iterates through `compiled_chain` +3. For each script, check if request URI matches any callback regex +4. Execute matching callbacks with their config and state +5. If callback returns `(status_code, body)`, terminate request with that response +6. If callback returns `nil`, continue to next callback +7. If no callbacks block the request, pass through to backend + +### Testing Framework + +Tests use a mock `ngx` object (created by `createNgx()` in test.lua) that simulates OpenResty's API: +- `ngx.var.uri`: Request URI +- `ngx.req.get_uri_args()`: Query parameters +- `ngx.req.get_headers()`: Request headers +- `ngx.status`, `ngx.say()`, `ngx.exit()`: Response control +- `ngx.re.match()`: PCRE regex matching via rex_pcre2 + +Use `setupTest(module_path, config)` to initialize a module for testing and `setupFakeRequest(path, options)` to simulate requests. + +To run the test suite: +```sh +# needed only once ever to setup the environment +make testdeps + +# run once on any new shell +eval (luarocks-5.1 path --bin) + +# actually run suite +make test +``` + +## Configuration + +The default config file (`conf.lua`) structure: + +```lua +return { + version = 1, + wantedScripts = { + ['script_name'] = { + -- script-specific config matching its schema + } + } +} +``` + +Scripts are loaded from `scripts/{script_name}.lua` based on keys in `wantedScripts`. From c01c6d0ba132f0f251eb3dd2adf2c9fa80c30819 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Nov 2025 04:37:40 +0000 Subject: [PATCH 9/9] Change wantedScripts to ordered list format This ensures scripts execute in the order they appear in the config file, resolving the TODO in the README. The config format has been changed from a table with script names as keys to an ordered list of script entries. Changes: - ctx.lua: Use ipairs() instead of pairs() to iterate in order - config.lua: Update validation to handle new list structure - conf.lua: Update example config to use new format - README.md: Remove TODO and clarify execution order New config format: wantedScripts = { {name = 'script1', config = {...}}, {name = 'script2', config = {...}} } --- README.md | 2 +- conf.lua | 7 ++++++- config.lua | 4 +++- ctx.lua | 4 +++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c5bd565..dae4deb 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ initialization will run validation of your configuration file and check if it is valid. if it is not you will see logs emitted about what failed when a request comes in, the scripts declared in the aproxy config file will -be executed sequentially (TODO: ensure order on conf file is chain order). +be executed sequentially in the order they appear in the configuration. each script has two types of callbacks: init and request diff --git a/conf.lua b/conf.lua index ee662d7..13e9e98 100644 --- a/conf.lua +++ b/conf.lua @@ -1,6 +1,11 @@ return { version = 1, wantedScripts = { - ['webfinger_allowlist'] = {accounts = {"example@example.com"}} + { + name = 'webfinger_allowlist', + config = { + accounts = {"example@example.com"} + } + } } } diff --git a/config.lua b/config.lua index 0de4de9..a8417a2 100644 --- a/config.lua +++ b/config.lua @@ -84,7 +84,9 @@ end local function validateConfigFile(config_object) local all_schema_errors = {} - for module_name, module_config in pairs(config_object.wantedScripts) do + for _, script_entry in ipairs(config_object.wantedScripts) do + local module_name = script_entry.name + local module_config = script_entry.config local module_manifest = require('scripts.' .. module_name) local config_schema = module_manifest.config local schema_errors = validateSchema(config_schema, module_config) diff --git a/ctx.lua b/ctx.lua index fb43089..2ab470e 100644 --- a/ctx.lua +++ b/ctx.lua @@ -18,7 +18,9 @@ end function ctx:loadChain() self.compiled_chain = {} - for module_name, module_config in pairs(self._wanted_scripts) do + for _, script_entry in ipairs(self._wanted_scripts) do + local module_name = script_entry.name + local module_config = script_entry.config local module = require('scripts.' .. module_name) local module_config_readonly = table.readonly(module_config) local module_state = module.init(module_config_readonly)