http: Add a more expressive but still nowhere near regex URL pattern matcher.

This commit is contained in:
Cory McWilliams 2024-11-02 19:22:04 -04:00
parent f74ca1c236
commit a09fefab5e
3 changed files with 54 additions and 13 deletions

View File

@ -67,7 +67,6 @@ typedef struct _tf_http_connection_t
typedef struct _tf_http_handler_t typedef struct _tf_http_handler_t
{ {
const char* pattern; const char* pattern;
bool is_wildcard;
tf_http_callback_t* callback; tf_http_callback_t* callback;
tf_http_cleanup_t* cleanup; tf_http_cleanup_t* cleanup;
void* user_data; void* user_data;
@ -129,9 +128,15 @@ static void _http_allocate_buffer(uv_handle_t* handle, size_t suggested_size, uv
*buf = uv_buf_init(connection->incoming, sizeof(connection->incoming)); *buf = uv_buf_init(connection->incoming, sizeof(connection->incoming));
} }
static bool _http_pattern_matches(const char* pattern, const char* path, bool is_wildcard) bool tf_http_pattern_matches(const char* pattern, const char* path)
{ {
if (!pattern || !*pattern || (!is_wildcard && strcmp(path, pattern) == 0)) if (!pattern || !*pattern)
{
return true;
}
const char* k_word = "{word}";
bool is_wildcard = strchr(pattern, '*') || strstr(pattern, k_word);
if (!is_wildcard && strcmp(path, pattern) == 0)
{ {
return true; return true;
} }
@ -140,17 +145,34 @@ static bool _http_pattern_matches(const char* pattern, const char* path, bool is
{ {
int i = 0; int i = 0;
int j = 0; int j = 0;
while (pattern[i] && path[j] && pattern[i] != '*' && pattern[i] == path[j]) while (pattern[i] && path[j] && pattern[i] == path[j])
{ {
i++; i++;
j++; j++;
} }
if (pattern[i] == '*') size_t k_word_len = strlen(k_word);
if (strncmp(pattern + i, k_word, k_word_len) == 0 && ((path[j] >= 'a' && path[j] <= 'z') || (path[j] >= 'A' && path[j] <= 'Z')))
{ {
while (true) while (true)
{ {
if (_http_pattern_matches(pattern + i + 1, path + j, strchr(pattern + i + 1, '*') != NULL)) if (((path[j] >= 'a' && path[j] <= 'z') || (path[j] >= 'A' && path[j] <= 'Z') || (path[j] >= '0' && path[j] <= '9')) &&
tf_http_pattern_matches(pattern + i + k_word_len, path + j + 1))
{
return true;
}
if (!path[j])
{
break;
}
j++;
}
}
else if (pattern[i] == '*')
{
while (true)
{
if (tf_http_pattern_matches(pattern + i + 1, path + j))
{ {
return true; return true;
} }
@ -170,7 +192,7 @@ static bool _http_find_handler(tf_http_t* http, const char* path, tf_http_callba
{ {
for (int i = 0; i < http->handlers_count; i++) for (int i = 0; i < http->handlers_count; i++)
{ {
if (_http_pattern_matches(http->handlers[i].pattern, path, http->handlers[i].is_wildcard)) if (tf_http_pattern_matches(http->handlers[i].pattern, path))
{ {
*out_callback = http->handlers[i].callback; *out_callback = http->handlers[i].callback;
*out_trace_name = http->handlers[i].pattern; *out_trace_name = http->handlers[i].pattern;
@ -741,7 +763,6 @@ void tf_http_add_handler(tf_http_t* http, const char* pattern, tf_http_callback_
http->handlers = tf_resize_vec(http->handlers, sizeof(tf_http_handler_t) * (http->handlers_count + 1)); http->handlers = tf_resize_vec(http->handlers, sizeof(tf_http_handler_t) * (http->handlers_count + 1));
http->handlers[http->handlers_count++] = (tf_http_handler_t) { http->handlers[http->handlers_count++] = (tf_http_handler_t) {
.pattern = tf_strdup(pattern), .pattern = tf_strdup(pattern),
.is_wildcard = pattern && strchr(pattern, '*') != NULL,
.callback = callback, .callback = callback,
.cleanup = cleanup, .cleanup = cleanup,
.user_data = user_data, .user_data = user_data,

View File

@ -228,4 +228,12 @@ void tf_http_request_websocket_upgrade(tf_http_request_t* request);
*/ */
const char* tf_http_status_text(int status); const char* tf_http_status_text(int status);
/**
** Match URL patterns. "*" matches anything, and "{word}" matches [a-zA-Z][a-zA-Z0-9]*".
** @param pattern The pattern to match.
** @param path The path to test.
** @return true if the path matches the pattern.
*/
bool tf_http_pattern_matches(const char* pattern, const char* path);
/** @} */ /** @} */

View File

@ -4,8 +4,8 @@
#include "http.h" #include "http.h"
#include "log.h" #include "log.h"
#include "mem.h" #include "mem.h"
#include "ssb.h"
#include "ssb.db.h" #include "ssb.db.h"
#include "ssb.h"
#include "ssb.tests.h" #include "ssb.tests.h"
#include "util.js.h" #include "util.js.h"
@ -850,16 +850,14 @@ static void _test_httpd(const tf_test_options_t* options)
char command[256]; char command[256];
snprintf(command, sizeof(command), "%s run -b 0 --db-path=out/test_db0.sqlite" TEST_ARGS, options->exe_path); snprintf(command, sizeof(command), "%s run -b 0 --db-path=out/test_db0.sqlite" TEST_ARGS, options->exe_path);
uv_stdio_container_t stdio[] = uv_stdio_container_t stdio[] = {
{
[STDIN_FILENO] = { .flags = UV_IGNORE }, [STDIN_FILENO] = { .flags = UV_IGNORE },
[STDOUT_FILENO] = { .flags = UV_INHERIT_FD }, [STDOUT_FILENO] = { .flags = UV_INHERIT_FD },
[STDERR_FILENO] = { .flags = UV_INHERIT_FD }, [STDERR_FILENO] = { .flags = UV_INHERIT_FD },
}; };
uv_process_t process = { 0 }; uv_process_t process = { 0 };
uv_spawn(&loop, &process, uv_spawn(&loop, &process,
&(uv_process_options_t) &(uv_process_options_t) {
{
.file = options->exe_path, .file = options->exe_path,
.args = (char*[]) { (char*)options->exe_path, "run", "-b0", "--db-path=out/test_db0.sqlite", "--http-port=8080", "--https-port=0", NULL }, .args = (char*[]) { (char*)options->exe_path, "run", "-b0", "--db-path=out/test_db0.sqlite", "--http-port=8080", "--https-port=0", NULL },
.stdio_count = sizeof(stdio) / sizeof(*stdio), .stdio_count = sizeof(stdio) / sizeof(*stdio),
@ -906,6 +904,19 @@ static void _test_httpd(const tf_test_options_t* options)
uv_loop_close(&loop); uv_loop_close(&loop);
} }
static void _test_pattern(const tf_test_options_t* options)
{
assert(tf_http_pattern_matches("/~core/test/", "/~core/test/"));
assert(tf_http_pattern_matches("/~core/test/*", "/~core/test/"));
assert(tf_http_pattern_matches("/~core/test/*", "/~core/test/blah"));
assert(tf_http_pattern_matches("*/~core/test/", "/~core/test/"));
assert(tf_http_pattern_matches("*/~core/test/", "blah/~core/test/"));
assert(tf_http_pattern_matches("/~*/*/", "/~core/test/"));
assert(tf_http_pattern_matches("/~{word}/*", "/~core/test"));
assert(tf_http_pattern_matches("/~{word}/{word}/", "/~core/test/"));
assert(tf_http_pattern_matches("/~{word}/{word}", "/~core/test"));
}
static void _test_auto_process_exit(uv_process_t* process, int64_t status, int termination_signal) static void _test_auto_process_exit(uv_process_t* process, int64_t status, int termination_signal)
{ {
tf_printf("Process exit %d signal=%d.\n", (int)WEXITSTATUS(status), termination_signal); tf_printf("Process exit %d signal=%d.\n", (int)WEXITSTATUS(status), termination_signal);
@ -1012,6 +1023,7 @@ void tf_tests(const tf_test_options_t* options)
_tf_test_run(options, "bip39", _test_bip39, false); _tf_test_run(options, "bip39", _test_bip39, false);
_tf_test_run(options, "http", _test_http, false); _tf_test_run(options, "http", _test_http, false);
_tf_test_run(options, "httpd", _test_httpd, false); _tf_test_run(options, "httpd", _test_httpd, false);
_tf_test_run(options, "pattern", _test_pattern, false);
_tf_test_run(options, "ssb", tf_ssb_test_ssb, false); _tf_test_run(options, "ssb", tf_ssb_test_ssb, false);
_tf_test_run(options, "ssb_id", tf_ssb_test_id_conversion, false); _tf_test_run(options, "ssb_id", tf_ssb_test_id_conversion, false);
_tf_test_run(options, "ssb_following", tf_ssb_test_following, false); _tf_test_run(options, "ssb_following", tf_ssb_test_following, false);