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`. 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)