Skip to content

Commit 917d611

Browse files
Add support for method overrides (_method query param or X-HTTP-Method-Override header) (#344)
1 parent e8d3ca9 commit 917d611

File tree

12 files changed

+244
-3
lines changed

12 files changed

+244
-3
lines changed

lib/php-extension/GoWrappers.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ char* GoContextCallback(int callbackId) {
2222
break;
2323
case CONTEXT_METHOD:
2424
ctx = "METHOD";
25-
ret = server.GetVar("REQUEST_METHOD");
25+
ret = server.GetMethod();
2626
break;
2727
case CONTEXT_ROUTE:
2828
ctx = "ROUTE";

lib/php-extension/RequestProcessor.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ std::string RequestProcessor::GetInitData(const std::string& token) {
99
if (!token.empty()) {
1010
AIKIDO_GLOBAL(token) = token;
1111
}
12-
12+
unordered_map<std::string, std::string> packages = GetPackages();
13+
AIKIDO_GLOBAL(uses_symfony_http_foundation) = packages.find("symfony/http-foundation") != packages.end();
1314
json initData = {
1415
{"token", AIKIDO_GLOBAL(token)},
1516
{"platform_name", AIKIDO_GLOBAL(sapi_name)},
@@ -22,7 +23,7 @@ std::string RequestProcessor::GetInitData(const std::string& token) {
2223
{"disk_logs", AIKIDO_GLOBAL(disk_logs)},
2324
{"localhost_allowed_by_default", AIKIDO_GLOBAL(localhost_allowed_by_default)},
2425
{"collect_api_schema", AIKIDO_GLOBAL(collect_api_schema)},
25-
{"packages", GetPackages()}};
26+
{"packages", packages}};
2627
return NormalizeAndDumpJson(initData);
2728
}
2829

lib/php-extension/Server.cpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,57 @@ std::string Server::GetVar(const char* var) {
3535
return Z_STRVAL_P(data);
3636
}
3737

38+
// Return the method from the query param _method (_GET["_method"])
39+
std::string Server::GetMethodFromQuery() {
40+
zval *get_array;
41+
get_array = zend_hash_str_find(&EG(symbol_table), "_GET", sizeof("_GET") - 1);
42+
if (!get_array || Z_TYPE_P(get_array) != IS_ARRAY) {
43+
return "";
44+
}
45+
46+
zval* query_method = zend_hash_str_find(Z_ARRVAL_P(get_array), "_method", sizeof("_method") - 1);
47+
if (!query_method) {
48+
return "";
49+
}
50+
if (Z_TYPE_P(query_method) != IS_STRING) {
51+
return "";
52+
}
53+
std::string query_method_str = Z_STRVAL_P(query_method);
54+
return ToUppercase(query_method_str);
55+
}
56+
57+
// For frameworks like Symfony, Laravel, method override is supported using X-HTTP-METHOD-OVERRIDE or _method query param
58+
// https://github.com/symfony/symfony/blob/b8eaa4be31f2159918e79e5694bc9ff241e0d692/src/Symfony/Component/HttpFoundation/Request.php#L1169-L1215
59+
std::string Server::GetMethod() {
60+
std::string method = ToUppercase(this->GetVar("REQUEST_METHOD"));
61+
62+
// if symfony http foundation is not used, return the method as is, otherwise we need to check the method override
63+
if (!AIKIDO_GLOBAL(uses_symfony_http_foundation)) {
64+
return method;
65+
}
66+
67+
// only for POST requests, we need to check the method override
68+
if (method != "POST") {
69+
return method;
70+
}
71+
72+
// X-HTTP-METHOD-OVERRIDE
73+
std::string x_http_method_override = ToUppercase(this->GetVar("HTTP_X_HTTP_METHOD_OVERRIDE"));
74+
if (x_http_method_override != "") {
75+
method = x_http_method_override;
76+
}
77+
78+
// in case of X-HTTP-METHOD-OVERRIDE is not set, we check the query param _method
79+
if (x_http_method_override == "") {
80+
std::string query_method = this->GetMethodFromQuery();
81+
if (query_method != "") {
82+
method = query_method;
83+
}
84+
}
85+
86+
return method;
87+
}
88+
3889
std::string Server::GetRoute() {
3990
std::string route = this->GetVar("REQUEST_URI");
4091
size_t pos = route.find("?");

lib/php-extension/Utils.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ std::string ToLowercase(const std::string& str) {
66
return result;
77
}
88

9+
std::string ToUppercase(const std::string& str) {
10+
std::string result = str;
11+
std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return std::toupper(c); });
12+
return result;
13+
}
14+
915
std::string GetRandomNumber() {
1016
std::random_device rd;
1117
std::mt19937 gen(rd());

lib/php-extension/include/Server.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ class Server {
99

1010
std::string GetVar(const char* var);
1111

12+
std::string GetMethod();
13+
14+
std::string GetMethodFromQuery();
15+
1216
std::string GetRoute();
1317

1418
std::string GetStatusCode();

lib/php-extension/include/Utils.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
std::string ToLowercase(const std::string& str);
66

7+
std::string ToUppercase(const std::string& str);
8+
79
std::string GetRandomNumber();
810

911
std::string GetTime();

lib/php-extension/include/php_aikido.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ bool disk_logs; // When enabled, it writes logs to disk instead of stdout. It's
1818
bool collect_api_schema;
1919
bool trust_proxy;
2020
bool localhost_allowed_by_default;
21+
bool uses_symfony_http_foundation; // If true, method override is supported using X-HTTP-METHOD-OVERRIDE or _method query param
2122
unsigned int report_stats_interval_to_agent; // Report once every X requests the collected stats to Agent
2223
std::string log_level_str;
2324
std::string sapi_name;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"AIKIDO_BLOCK": "1",
3+
"AIKIDO_LOCALHOST_ALLOWED_BY_DEFAULT": "0",
4+
"AIKIDO_FEATURE_COLLECT_API_SCHEMA": "1"
5+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"type": "heartbeat",
3+
"routes": [
4+
{
5+
"path": "/",
6+
"method": "GET",
7+
"hits": 8,
8+
"rateLimitedCount": 3
9+
},
10+
{
11+
"path": "/",
12+
"method": "POST",
13+
"hits": 1
14+
},
15+
{
16+
"path": "/",
17+
"method": "PUT",
18+
"hits": 3
19+
},
20+
{
21+
"path": "/",
22+
"method": "DELETE",
23+
"hits": 1
24+
},
25+
{
26+
"path": "/",
27+
"method": "PATCH",
28+
"hits": 1
29+
}
30+
]
31+
}
32+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
4+
$method = $_SERVER['REQUEST_METHOD'];
5+
$_SERVER['REMOTE_ADDR'] = '5.2.190.71';
6+
7+
if ($method === 'POST') {
8+
if (isset($_GET['_method'])) {
9+
$method = strtoupper($_GET['_method']);
10+
}
11+
12+
else if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
13+
$method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
14+
}
15+
}
16+
17+
18+
if (extension_loaded('aikido')) {
19+
$decision = \aikido\should_block_request();
20+
21+
// If the rate limit is exceeded, return a 429 status code
22+
if ($decision->block && $decision->type == "ratelimited" && $decision->trigger == "ip") {
23+
http_response_code(429);
24+
echo "Rate limit exceeded by IP: " . $decision->ip;
25+
exit();
26+
}
27+
}
28+
29+
echo "Method: " . $method;
30+

0 commit comments

Comments
 (0)