Skip to content

Commit f4634fb

Browse files
authored
Custom route parameters (#368)
1 parent 406ff86 commit f4634fb

25 files changed

+800
-21
lines changed

docs/register-param-matcher.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Register custom route parameter matchers
2+
3+
Aikido exposes a new function, called `\aikido\register_param_matcher` that can be used to register custom route parameter matchers.
4+
This allows you to define custom patterns for URL segments that should be treated as route parameters.
5+
6+
## Function signature
7+
8+
```php
9+
bool \aikido\register_param_matcher(string $param, string $pattern)
10+
```
11+
12+
- `$param`: The name of the parameter (e.g., "tenant", "org_id"). Must match `[a-zA-Z_]+`.
13+
- `$pattern`: A pattern string that can contain placeholders like `{digits}` and `{alpha}`.
14+
- Returns: `true` on success, `false` on failure.
15+
16+
## Supported placeholders
17+
18+
The pattern supports the following placeholders:
19+
20+
- `{digits}` - Matches one or more digits (`\d+`)
21+
- `{alpha}` - Matches one or more alphabetic characters (`[a-zA-Z]+`)
22+
23+
## Pattern rules
24+
25+
1. The pattern must contain at least one placeholder (e.g., `{digits}` or `{alpha}`).
26+
2. The pattern cannot contain slashes (`/`).
27+
3. The pattern cannot contain consecutive similar placeholders (e.g., `{digits}{digits}`).
28+
29+
## Usage example
30+
31+
```php
32+
<?php
33+
if (extension_loaded('aikido')) {
34+
// Register a custom matcher for tenant IDs (e.g., "aikido-123")
35+
\aikido\register_param_matcher("tenant", "aikido-{digits}");
36+
37+
// Register a custom matcher for org_ids (e.g., "aikido-foo-123-bar")
38+
\aikido\register_param_matcher("org_id", "aikido-{alpha}-{digits}-{alpha}");
39+
}
40+
?>
41+
```
42+
43+
## When to use
44+
45+
This code needs to run with every request, so you can add it in a middleware, as exemplified [here](./should_block_request.md).
46+
The backend will ignore duplicate registrations, so it's safe to call this function on every request.
47+
48+
## How it works
49+
50+
When Aikido processes URLs to build route patterns, it will check registered custom param matchers first.
51+
If a URL segment matches a custom pattern, it will be replaced with `:param_name` in the route. For example:
52+
53+
- URL: `/posts/aikido-123` → Route: `/posts/:tenant` (if "tenant" matcher is registered)
54+
- URL: `/blog/aikido-foo-123-bar` → Route: `/blog/:org_id` (if "org_id" matcher is registered)
55+
56+
If no custom matcher matches, Zen falls back to its default matchers (numbers, UUIDs, dates, etc.).

lib/API.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ enum EVENT_ID {
77
EVENT_POST_REQUEST,
88
EVENT_SET_USER,
99
EVENT_SET_RATE_LIMIT_GROUP,
10+
EVENT_REGISTER_PARAM_MATCHER,
1011
EVENT_GET_AUTO_BLOCKING_STATUS,
1112
EVENT_GET_BLOCKING_STATUS,
1213
EVENT_PRE_OUTGOING_REQUEST,
@@ -42,6 +43,9 @@ enum CALLBACK_ID {
4243
FUNCTION_NAME,
4344
STACK_TRACE,
4445

46+
PARAM_MATCHER_PARAM,
47+
PARAM_MATCHER_REGEX,
48+
4549
OUTGOING_REQUEST_URL,
4650
OUTGOING_REQUEST_EFFECTIVE_URL, // Effective URL after redirects (the final
4751
// URL were the request was actually made)

lib/php-extension/Action.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ ACTION_STATUS Action::executeStore(json &event) {
3737
return CONTINUE;
3838
}
3939

40+
ACTION_STATUS Action::executeWarningMessage(json &event) {
41+
std::string message = event["message"];
42+
php_printf("%s\n", message.c_str());
43+
return WARNING_MESSAGE;
44+
}
45+
4046
ACTION_STATUS Action::Execute(std::string &event) {
4147
if (event.empty()) {
4248
return CONTINUE;
@@ -54,6 +60,8 @@ ACTION_STATUS Action::Execute(std::string &event) {
5460
return executeExit(eventJson);
5561
} else if (actionType == "store") {
5662
return executeStore(eventJson);
63+
} else if (actionType == "warning_message") {
64+
return executeWarningMessage(eventJson);
5765
}
5866
return CONTINUE;
5967
}

lib/php-extension/Aikido.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ PHP_MSHUTDOWN_FUNCTION(aikido) {
4747

4848
/*
4949
In the case of Apache mod-php servers, the MSHUTDOWN can be called multiple times.
50-
As a consequence, we need to do the unhooking / uninitialization logic based on the current
50+
As a consequence, we need to do the unhooking / uninitialization logic based on the current
5151
PID for which the MSHUTDOWN is called. This logic is part of phpLifecycle.ModuleShutdown().
5252
The same does not apply for CLI mode, where the MSHUTDOWN is called only once.
5353
*/
@@ -59,7 +59,7 @@ PHP_MSHUTDOWN_FUNCTION(aikido) {
5959

6060
PHP_RINIT_FUNCTION(aikido) {
6161
ScopedTimer scopedTimer("request_init", "request_op");
62-
62+
6363
phpLifecycle.RequestInit();
6464
AIKIDO_LOG_DEBUG("RINIT finished!\n");
6565
return SUCCESS;
@@ -93,6 +93,7 @@ static const zend_function_entry ext_functions[] = {
9393
ZEND_NS_FE("aikido", auto_block_request, arginfo_aikido_auto_block_request)
9494
ZEND_NS_FE("aikido", set_token, arginfo_aikido_set_token)
9595
ZEND_NS_FE("aikido", set_rate_limit_group, arginfo_aikido_set_rate_limit_group)
96+
ZEND_NS_FE("aikido", register_param_matcher, arginfo_aikido_register_param_matcher)
9697
ZEND_FE_END
9798
};
9899

lib/php-extension/GoWrappers.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ char* GoContextCallback(int callbackId) {
128128
ctx = "STACK_TRACE";
129129
ret = GetStackTrace();
130130
break;
131+
case PARAM_MATCHER_PARAM:
132+
ctx = "PARAM_MATCHER_PARAM";
133+
ret = eventCache.paramMatcherParam;
134+
break;
135+
case PARAM_MATCHER_REGEX:
136+
ctx = "PARAM_MATCHER_REGEX";
137+
ret = eventCache.paramMatcherRegex;
138+
break;
131139
}
132140
} catch (std::exception& e) {
133141
AIKIDO_LOG_DEBUG("Exception in GoContextCallback: %s\n", e.what());
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#include "Includes.h"
2+
3+
ZEND_FUNCTION(register_param_matcher) {
4+
ScopedTimer scopedTimer("register_param_matcher", "aikido_op");
5+
6+
if (AIKIDO_GLOBAL(disable) == true) {
7+
RETURN_BOOL(false);
8+
}
9+
10+
char *param = nullptr;
11+
size_t paramLength = 0;
12+
char *regex = nullptr;
13+
size_t regexLength = 0;
14+
15+
ZEND_PARSE_PARAMETERS_START(2, 2)
16+
Z_PARAM_STRING(param, paramLength)
17+
Z_PARAM_STRING(regex, regexLength)
18+
ZEND_PARSE_PARAMETERS_END();
19+
20+
if (!param || paramLength == 0 || !regex || regexLength == 0) {
21+
AIKIDO_LOG_ERROR("register_param_matcher: param or regex is null or empty!\n");
22+
RETURN_BOOL(false);
23+
}
24+
25+
eventCache.paramMatcherParam = std::string(param, paramLength);
26+
eventCache.paramMatcherRegex = std::string(regex, regexLength);
27+
28+
try {
29+
std::string outputEvent;
30+
requestProcessor.SendEvent(EVENT_REGISTER_PARAM_MATCHER, outputEvent);
31+
if (action.Execute(outputEvent) == WARNING_MESSAGE) {
32+
// If a warning message is returned, it means that the parameters are invalid, so we return false.
33+
RETURN_BOOL(false);
34+
}
35+
36+
} catch (const std::exception& e) {
37+
AIKIDO_LOG_ERROR("Exception encountered in processing register param matcher event: %s\n", e.what());
38+
RETURN_BOOL(false);
39+
}
40+
41+
AIKIDO_LOG_INFO("Registered param matcher %s -> %s\n", eventCache.paramMatcherParam.c_str(), eventCache.paramMatcherRegex.c_str());
42+
RETURN_BOOL(true);
43+
}

lib/php-extension/config.m4

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,5 @@ if test "$PHP_AIKIDO" != "no"; then
9090
dnl In case of no dependencies
9191
AC_DEFINE(HAVE_AIKIDO, 1, [ Have aikido support ])
9292

93-
PHP_NEW_EXTENSION(aikido, Aikido.cpp GoWrappers.cpp Packages.cpp PhpWrappers.cpp PhpLifecycle.cpp Stats.cpp Server.cpp Environment.cpp RequestProcessor.cpp Agent.cpp Hooks.cpp HookAst.cpp Utils.cpp Handle.cpp HandleUrls.cpp HandleSetToken.cpp HandleUsers.cpp HandleShouldBlockRequest.cpp HandleRateLimitGroup.cpp Cache.cpp HandleFileCompilation.cpp HandleShellExecution.cpp Log.cpp HandleQueries.cpp HandlePathAccess.cpp Action.cpp, $ext_shared)
93+
PHP_NEW_EXTENSION(aikido, Aikido.cpp GoWrappers.cpp Packages.cpp PhpWrappers.cpp PhpLifecycle.cpp Stats.cpp Server.cpp Environment.cpp RequestProcessor.cpp Agent.cpp Hooks.cpp HookAst.cpp Utils.cpp Handle.cpp HandleUrls.cpp HandleSetToken.cpp HandleRegisterParamMatcher.cpp HandleUsers.cpp HandleShouldBlockRequest.cpp HandleRateLimitGroup.cpp Cache.cpp HandleFileCompilation.cpp HandleShellExecution.cpp Log.cpp HandleQueries.cpp HandlePathAccess.cpp Action.cpp, $ext_shared)
9494
fi

lib/php-extension/include/Action.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
enum ACTION_STATUS {
66
CONTINUE,
77
BLOCK,
8-
EXIT
8+
EXIT,
9+
WARNING_MESSAGE
910
};
1011

1112
class Action {
@@ -24,6 +25,8 @@ class Action {
2425

2526
ACTION_STATUS executeStore(json &event);
2627

28+
ACTION_STATUS executeWarningMessage(json &event);
29+
2730
public:
2831
Action() = default;
2932
~Action() = default;

lib/php-extension/include/Cache.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class EventCache {
3131
std::string sqlQuery;
3232
std::string sqlDialect;
3333

34+
std::string paramMatcherParam;
35+
std::string paramMatcherRegex;
36+
3437
EventCache() = default;
3538
void Reset();
3639
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#include "Includes.h"
2+
3+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_aikido_register_param_matcher, 0, 2, _IS_BOOL, 0)
4+
ZEND_ARG_TYPE_INFO(0, param, IS_STRING, 0)
5+
ZEND_ARG_TYPE_INFO(0, regex, IS_STRING, 0)
6+
ZEND_END_ARG_INFO()
7+
8+
ZEND_FUNCTION(register_param_matcher);

0 commit comments

Comments
 (0)