diff --git a/examples/URIMatcher/README.md b/examples/URIMatcher/README.md new file mode 100644 index 00000000..dc7d1ce3 --- /dev/null +++ b/examples/URIMatcher/README.md @@ -0,0 +1,240 @@ +# AsyncURIMatcher Example + +This example demonstrates the comprehensive URI matching capabilities of the ESPAsyncWebServer library using the `AsyncURIMatcher` class. + +## Overview + +The `AsyncURIMatcher` class provides flexible and powerful URL routing mechanisms that go beyond simple string matching. It supports various matching strategies that can be combined to create sophisticated routing rules. + +**Important**: When using plain strings (not `AsyncURIMatcher` objects), the library uses auto-detection (`URIMatchAuto`) which analyzes the URI pattern and applies appropriate matching rules. This is **not** simple exact matching - it combines exact and folder matching by default! + +## Auto-Detection Behavior + +When you pass a plain string or `const char*` to `server.on()`, the `URIMatchAuto` flag is used, which: + +1. **Empty URI**: Matches everything +2. **Ends with `*`**: Becomes prefix match (`URIMatchPrefix`) +3. **Contains `/*.ext`**: Becomes extension match (`URIMatchExtension`) +4. **Starts with `^` and ends with `$`**: Becomes regex match (if enabled) +5. **Everything else**: Becomes **both** exact and folder match (`URIMatchPrefixFolder | URIMatchExact`) + +This means traditional string-based routes like `server.on("/path", handler)` will match: +- `/path` (exact match) +- `/path/anything` (folder match) + +But will **NOT** match `/path-suffix` (prefix without folder separator). + +## Features Demonstrated + +### 1. **Auto-Detection (Traditional Behavior)** +- When using plain strings, automatically detects the appropriate matching strategy +- Default behavior: combines exact and folder matching +- Special patterns: `*` suffix → prefix, `/*.ext` → extension, regex patterns +- Use cases: Traditional ESP32 web server behavior with enhanced capabilities +- Examples: `/path` matches both `/path` and `/path/sub` + +### 2. **Exact Matching** +- Matches only the exact URL specified +- Use cases: API endpoints, specific pages +- Examples: `/exact`, `/login`, `/dashboard` + +### 3. **Prefix Matching** +- Matches URLs that start with the specified pattern +- Use cases: API versioning, module grouping +- Examples: `/api*` matches `/api/users`, `/api/posts`, etc. + +### 4. **Folder/Directory Matching** +- Matches URLs under a specific directory path +- Use cases: Admin sections, organized content +- Examples: `/admin/` matches `/admin/users`, `/admin/settings` + +### 5. **Extension Matching** +- Matches files with specific extensions under a path +- Use cases: Static file serving, file type handlers +- Examples: `/images/*.jpg` matches any `.jpg` file under `/images/` + +### 6. **Case Insensitive Matching** +- Matches URLs regardless of character case +- Use cases: User-friendly URLs, legacy support +- Examples: `/CaSe` matches `/case`, `/CASE`, etc. + +### 7. **Regular Expression Matching** +- Advanced pattern matching using regex (requires `ASYNCWEBSERVER_REGEX`) +- Use cases: Complex URL patterns, parameter extraction +- Examples: `/user/([0-9]+)` matches `/user/123` and captures the ID + +### 8. **Combined Flags** +- Multiple matching strategies can be combined +- Use cases: Flexible routing requirements +- Examples: Case-insensitive prefix matching + +## Usage Patterns + +### Traditional String-based Routing (Auto-Detection) +```cpp +// Auto-detection with exact + folder matching +server.on("/api", handler); // Matches /api AND /api/anything +server.on("/login", handler); // Matches /login AND /login/sub + +// Auto-detection with prefix matching +server.on("/prefix*", handler); // Matches /prefix, /prefix-test, /prefix/sub + +// Auto-detection with extension matching +server.on("/images/*.jpg", handler); // Matches /images/pic.jpg, /images/sub/pic.jpg +``` + +### Explicit AsyncURIMatcher Syntax +### Explicit AsyncURIMatcher Syntax +```cpp +// Exact matching only +server.on(AsyncURIMatcher("/path", URIMatchExact), handler); + +// Prefix matching only +server.on(AsyncURIMatcher("/api", URIMatchPrefix), handler); + +// Combined flags +server.on(AsyncURIMatcher("/api", URIMatchPrefix | URIMatchCaseInsensitive), handler); +``` + +### Factory Functions +```cpp +// More readable and expressive +server.on(AsyncURIMatcher::exact("/login"), handler); +server.on(AsyncURIMatcher::prefix("/api"), handler); +server.on(AsyncURIMatcher::dir("/admin"), handler); +server.on(AsyncURIMatcher::ext("/images/*.jpg"), handler); +server.on(AsyncURIMatcher::iExact("/case"), handler); + +#ifdef ASYNCWEBSERVER_REGEX +server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), handler); +#endif +``` + +## Available Flags + +| Flag | Description | +|------|-------------| +| `URIMatchAuto` | Auto-detect match type from pattern (default) | +| `URIMatchExact` | Exact URL match | +| `URIMatchPrefix` | Prefix match | +| `URIMatchPrefixFolder` | Folder prefix match (requires trailing /) | +| `URIMatchExtension` | File extension match pattern | +| `URIMatchCaseInsensitive` | Case insensitive matching | +| `URIMatchRegex` | Regular expression matching (requires ASYNCWEBSERVER_REGEX) | + +## Testing the Example + +1. **Upload the sketch** to your ESP32/ESP8266 +2. **Connect to WiFi AP**: `esp-captive` (no password required) +3. **Navigate to**: `http://192.168.4.1/` +4. **Explore the examples** by clicking the organized test links +5. **Monitor Serial output**: Open Serial Monitor to see detailed debugging information for each matched route + +### Test URLs Available (All Clickable from Homepage) + +**Auto-Detection Examples:** +- `http://192.168.4.1/auto` (exact + folder match) +- `http://192.168.4.1/auto/sub` (folder match - same handler!) +- `http://192.168.4.1/wildcard-test` (auto-detected prefix) +- `http://192.168.4.1/auto-images/photo.png` (auto-detected extension) + +**Factory Method Examples:** +- `http://192.168.4.1/exact` (AsyncURIMatcher::exact) +- `http://192.168.4.1/service/status` (AsyncURIMatcher::prefix) +- `http://192.168.4.1/admin/users` (AsyncURIMatcher::dir) +- `http://192.168.4.1/images/photo.jpg` (AsyncURIMatcher::ext) + +**Case Insensitive Examples:** +- `http://192.168.4.1/case` (lowercase) +- `http://192.168.4.1/CASE` (uppercase) +- `http://192.168.4.1/CaSe` (mixed case) + +**Regex Examples (if ASYNCWEBSERVER_REGEX enabled):** +- `http://192.168.4.1/user/123` (captures numeric ID) +- `http://192.168.4.1/user/456` (captures numeric ID) + +**Combined Flags Examples:** +- `http://192.168.4.1/mixedcase-test` (prefix + case insensitive) +- `http://192.168.4.1/MIXEDCASE/sub` (prefix + case insensitive) + +### Console Output + +Each handler provides detailed debugging information via Serial output: +``` +Auto-Detection Match (Traditional) +Matched URL: /auto +Uses auto-detection: exact + folder matching +``` + +``` +Factory Exact Match +Matched URL: /exact +Uses AsyncURIMatcher::exact() factory function +``` + +``` +Regex Match - User ID +Matched URL: /user/123 +Captured User ID: 123 +This regex matches /user/{number} pattern +``` + +## Compilation Options + +### Enable Regex Support +To enable regular expression matching, compile with: +``` +-D ASYNCWEBSERVER_REGEX +``` + +In PlatformIO, add to `platformio.ini`: +```ini +build_flags = -D ASYNCWEBSERVER_REGEX +``` + +In Arduino IDE, add to your sketch: +```cpp +#define ASYNCWEBSERVER_REGEX +``` + +## Performance Considerations + +1. **Exact matches** are fastest +2. **Prefix matches** are very efficient +3. **Regex matches** are slower but most flexible +4. **Case insensitive** matching adds minimal overhead +5. **Auto-detection** adds slight parsing overhead at construction time + +## Real-World Applications + +### REST API Design +```cpp +// API versioning +server.on(AsyncURIMatcher::prefix("/api/v1"), handleAPIv1); +server.on(AsyncURIMatcher::prefix("/api/v2"), handleAPIv2); + +// Resource endpoints with IDs +server.on(AsyncURIMatcher::regex("^/api/users/([0-9]+)$"), handleUserById); +server.on(AsyncURIMatcher::regex("^/api/posts/([0-9]+)/comments$"), handlePostComments); +``` + +### File Serving +```cpp +// Serve different file types +server.on(AsyncURIMatcher::ext("/assets/*.css"), serveCSSFiles); +server.on(AsyncURIMatcher::ext("/assets/*.js"), serveJSFiles); +server.on(AsyncURIMatcher::ext("/images/*.jpg"), serveImageFiles); +``` + +### Admin Interface +```cpp +// Admin section with authentication +server.on(AsyncURIMatcher::dir("/admin"), handleAdminPages); +server.on(AsyncURIMatcher::exact("/admin"), redirectToAdminDashboard); +``` + +## See Also + +- [ESPAsyncWebServer Documentation](https://github.com/ESP32Async/ESPAsyncWebServer) +- [Regular Expression Reference](https://en.cppreference.com/w/cpp/regex) +- Other examples in the `examples/` directory diff --git a/examples/URIMatcher/URIMatcher.ino b/examples/URIMatcher/URIMatcher.ino new file mode 100644 index 00000000..4ae79200 --- /dev/null +++ b/examples/URIMatcher/URIMatcher.ino @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +// +// AsyncURIMatcher Examples - Advanced URI Matching and Routing +// +// This example demonstrates the various ways to use AsyncURIMatcher class +// for flexible URL routing with different matching strategies: +// +// 1. Exact matching +// 2. Prefix matching +// 3. Folder/directory matching +// 4. Extension matching +// 5. Case insensitive matching +// 6. Regex matching (if ASYNCWEBSERVER_REGEX is enabled) +// 7. Factory functions for common patterns +// +// Test URLs: +// - Exact: http://192.168.4.1/exact +// - Prefix: http://192.168.4.1/prefix-anything +// - Folder: http://192.168.4.1/api/users, http://192.168.4.1/api/posts +// - Extension: http://192.168.4.1/images/photo.jpg, http://192.168.4.1/docs/readme.pdf +// - Case insensitive: http://192.168.4.1/CaSe or http://192.168.4.1/case +// - Wildcard: http://192.168.4.1/wildcard-test + +#include +#if defined(ESP32) || defined(LIBRETINY) +#include +#include +#elif defined(ESP8266) +#include +#include +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include +#include +#endif + +#include + +static AsyncWebServer server(80); + +void setup() { + Serial.begin(115200); + Serial.println(); + Serial.println("=== AsyncURIMatcher Example ==="); + +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-captive"); + Serial.print("AP IP address: "); + Serial.println(WiFi.softAPIP()); +#endif + + // ============================================================================= + // 1. AUTO-DETECTION BEHAVIOR - traditional string-based routing + // ============================================================================= + + // Traditional string-based routing with auto-detection + // This uses URIMatchAuto which combines URIMatchPrefixFolder | URIMatchExact + // It will match BOTH "/auto" exactly AND "/auto/" + anything + server.on("/auto", HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Auto-Detection Match (Traditional)"); + Serial.println("Matched URL: " + request->url()); + Serial.println("Uses auto-detection: exact + folder matching"); + request->send(200, "text/plain", "OK - Auto-detection match"); + }); + + // Auto-detection for wildcard patterns (ends with *) + // This auto-detects as URIMatchPrefix + server.on("/wildcard*", HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Auto-Detected Wildcard (Prefix)"); + Serial.println("Matched URL: " + request->url()); + Serial.println("Auto-detected as prefix match due to trailing *"); + request->send(200, "text/plain", "OK - Wildcard prefix match"); + }); + + // Auto-detection for extension patterns (contains /*.ext) + // This auto-detects as URIMatchExtension + server.on("/auto-images/*.png", HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Auto-Detected Extension Pattern"); + Serial.println("Matched URL: " + request->url()); + Serial.println("Auto-detected as extension match due to /*.png pattern"); + request->send(200, "text/plain", "OK - Extension match"); + }); + + // ============================================================================= + // 2. EXACT MATCHING - matches only the exact URL (explicit) + // ============================================================================= + + // Using factory function for exact match + server.on(AsyncURIMatcher::exact("/exact"), HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Factory Exact Match"); + Serial.println("Matched URL: " + request->url()); + Serial.println("Uses AsyncURIMatcher::exact() factory function"); + request->send(200, "text/plain", "OK - Factory exact match"); + }); + + // ============================================================================= + // 3. PREFIX MATCHING - matches URLs that start with the pattern + // ============================================================================= + + // Using factory function for prefix match + server.on(AsyncURIMatcher::prefix("/service"), HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Service Prefix Match (Factory)"); + Serial.println("Matched URL: " + request->url()); + Serial.println("Uses AsyncURIMatcher::prefix() factory function"); + request->send(200, "text/plain", "OK - Factory prefix match"); + }); + + // ============================================================================= + // 4. FOLDER/DIRECTORY MATCHING - matches URLs in a folder structure + // ============================================================================= + + // Folder match using factory function (automatically adds trailing slash) + server.on(AsyncURIMatcher::dir("/admin"), HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Admin Directory Match"); + Serial.println("Matched URL: " + request->url()); + Serial.println("This matches URLs under /admin/ directory"); + Serial.println("Note: /admin (without slash) will NOT match"); + request->send(200, "text/plain", "OK - Directory match"); + }); + + // ============================================================================= + // 5. EXTENSION MATCHING - matches files with specific extensions + // ============================================================================= + + // Image extension matching + server.on(AsyncURIMatcher::ext("/images/*.jpg"), HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("JPG Image Handler"); + Serial.println("Matched URL: " + request->url()); + Serial.println("This matches any .jpg file under /images/"); + request->send(200, "text/plain", "OK - Extension match"); + }); + + // ============================================================================= + // 6. CASE INSENSITIVE MATCHING + // ============================================================================= + + // Case insensitive exact match + server.on(AsyncURIMatcher::exact("/case", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Case Insensitive Match"); + Serial.println("Matched URL: " + request->url()); + Serial.println("This matches /case in any case combination"); + request->send(200, "text/plain", "OK - Case insensitive match"); + }); + +#ifdef ASYNCWEBSERVER_REGEX + // ============================================================================= + // 7. REGEX MATCHING (only available if ASYNCWEBSERVER_REGEX is enabled) + // ============================================================================= + + // Regex match for numeric IDs + server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Regex Match - User ID"); + Serial.println("Matched URL: " + request->url()); + if (request->pathArg(0).length() > 0) { + Serial.println("Captured User ID: " + request->pathArg(0)); + } + Serial.println("This regex matches /user/{number} pattern"); + request->send(200, "text/plain", "OK - Regex match"); + }); +#endif + + // ============================================================================= + // 8. COMBINED FLAGS EXAMPLE + // ============================================================================= + + // Combine multiple flags + server.on(AsyncURIMatcher::prefix("/MixedCase", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Combined Flags Example"); + Serial.println("Matched URL: " + request->url()); + Serial.println("Uses both AsyncURIMatcher::Prefix and AsyncURIMatcher::CaseInsensitive"); + request->send(200, "text/plain", "OK - Combined flags match"); + }); + + // ============================================================================= + // 9. HOMEPAGE WITH NAVIGATION + // ============================================================================= + + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Homepage accessed"); + String response = R"( + + + AsyncURIMatcher Examples + + + +

AsyncURIMatcher Examples

+ + + + + +
+

Case Insensitive Matching

+ /case (lowercase) + /CASE (uppercase) + /CaSe (mixed case) +
+ +)"; +#ifdef ASYNCWEBSERVER_REGEX + response += R"( + +)"; +#endif + response += R"( + + +)"; + request->send(200, "text/html", response); + }); + + // ============================================================================= + // 10. NOT FOUND HANDLER + // ============================================================================= + + server.onNotFound([](AsyncWebServerRequest *request) { + String html = "

404 - Not Found

"; + html += "

The requested URL " + request->url() + " was not found.

"; + html += "

← Back to Examples

"; + request->send(404, "text/html", html); + }); + + server.begin(); + + Serial.println(); + Serial.println("=== Server Started ==="); + Serial.println("Open your browser and navigate to:"); + Serial.println("http://192.168.4.1/ - Main examples page"); + Serial.println(); + Serial.println("Available test endpoints:"); + Serial.println("• Auto-detection: /auto (exact+folder), /wildcard*, /auto-images/*.png"); + Serial.println("• Exact matches: /exact"); + Serial.println("• Prefix matches: /service*"); + Serial.println("• Folder matches: /admin/*"); + Serial.println("• Extension matches: /images/*.jpg"); + Serial.println("• Case insensitive: /case (try /CASE, /Case)"); +#ifdef ASYNCWEBSERVER_REGEX + Serial.println("• Regex matches: /user/123"); +#endif + Serial.println("• Combined flags: /mixedcase*"); + Serial.println(); +} + +void loop() { + // Nothing to do here - the server handles everything asynchronously + delay(1000); +} diff --git a/examples/URIMatcherTest/URIMatcherTest.ino b/examples/URIMatcherTest/URIMatcherTest.ino new file mode 100644 index 00000000..cf560554 --- /dev/null +++ b/examples/URIMatcherTest/URIMatcherTest.ino @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +// +// Test for ESPAsyncWebServer URI matching +// +// Usage: upload, connect to the AP and run test_routes.sh +// + +#include +#if defined(ESP32) || defined(LIBRETINY) +#include +#include +#elif defined(ESP8266) +#include +#include +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include +#include +#endif + +#include + +AsyncWebServer server(80); + +void setup() { + Serial.begin(115200); + +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-captive"); +#endif + + // Status endpoint + server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Exact paths, plus the subpath (/exact matches /exact/sub but not /exact-no-match) + server.on("/exact", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + server.on("/api/users", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Prefix matching + server.on("/api/*", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + server.on("/files/*", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Extensions + server.on("/*.json", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "application/json", "{\"status\":\"OK\"}"); + }); + + server.on("/*.css", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/css", "/* OK */"); + }); + + // ============================================================================= + // NEW ASYNCURIMATCHER FACTORY METHODS TESTS + // ============================================================================= + + // Exact match using factory method (does NOT match subpaths like traditional) + server.on(AsyncURIMatcher::exact("/factory/exact"), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Prefix match using factory method + server.on(AsyncURIMatcher::prefix("/factory/prefix"), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Directory match using factory method (matches /dir/anything but not /dir itself) + server.on(AsyncURIMatcher::dir("/factory/dir"), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Extension match using factory method + server.on(AsyncURIMatcher::ext("/factory/files/*.txt"), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // ============================================================================= + // CASE INSENSITIVE MATCHING TESTS + // ============================================================================= + + // Case insensitive exact match + server.on(AsyncURIMatcher::exact("/case/exact", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Case insensitive prefix match + server.on(AsyncURIMatcher::prefix("/case/prefix", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Case insensitive directory match + server.on(AsyncURIMatcher::dir("/case/dir", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Case insensitive extension match + server.on(AsyncURIMatcher::ext("/case/files/*.PDF", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + +#ifdef ASYNCWEBSERVER_REGEX + // Traditional regex patterns (backward compatibility) + server.on("^/user/([0-9]+)$", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + server.on("^/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // ============================================================================= + // NEW ASYNCURIMATCHER REGEX FACTORY METHODS + // ============================================================================= + + // Regex match using factory method + server.on(AsyncURIMatcher::regex("^/factory/user/([0-9]+)$"), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Case insensitive regex match using factory method + server.on(AsyncURIMatcher::regex("^/factory/search/(.+)$", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Complex regex with multiple capture groups + server.on(AsyncURIMatcher::regex("^/factory/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$"), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); +#endif + + // ============================================================================= + // SPECIAL MATCHERS + // ============================================================================= + + // Match all POST requests (catch-all before 404) + server.on(AsyncURIMatcher::all(), HTTP_POST, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // 404 handler + server.onNotFound([](AsyncWebServerRequest *request) { + request->send(404, "text/plain", "Not Found"); + }); + + server.begin(); + Serial.println("Server ready"); +} + +// not needed +void loop() { + delay(100); +} diff --git a/examples/URIMatcherTest/test_routes.sh b/examples/URIMatcherTest/test_routes.sh new file mode 100755 index 00000000..ebdb27d1 --- /dev/null +++ b/examples/URIMatcherTest/test_routes.sh @@ -0,0 +1,174 @@ +#!/bin/bash + +# URI Matcher Test Script +# Tests all routes defined in URIMatcherTest.ino + +SERVER_IP="${1:-192.168.4.1}" +SERVER_PORT="80" +BASE_URL="http://${SERVER_IP}:${SERVER_PORT}" + +echo "Testing URI Matcher at $BASE_URL" +echo "==================================" + +# Function to test a route +test_route() { + local path="$1" + local expected_status="$2" + local description="$3" + + echo -n "Testing $path ... " + + response=$(curl -s -w "HTTPSTATUS:%{http_code}" "$BASE_URL$path" 2>/dev/null) + status_code=$(echo "$response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2) + + if [ "$status_code" = "$expected_status" ]; then + echo "✅ PASS ($status_code)" + else + echo "❌ FAIL (expected $expected_status, got $status_code)" + return 1 + fi + return 0 +} + +# Test counter +PASS=0 +FAIL=0 + +# Test all routes that should return 200 OK +echo "Testing routes that should work (200 OK):" + +if test_route "/status" "200" "Status endpoint"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/exact" "200" "Exact path"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/exact/" "200" "Exact path ending with /"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/exact/sub" "200" "Exact path with subpath /"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/api/users" "200" "Exact API path"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/api/data" "200" "API prefix match"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/api/v1/posts" "200" "API prefix deep"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/files/document.pdf" "200" "Files prefix"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/files/images/photo.jpg" "200" "Files prefix deep"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/config.json" "200" "JSON extension"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/data/settings.json" "200" "JSON extension in subfolder"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/style.css" "200" "CSS extension"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/assets/main.css" "200" "CSS extension in subfolder"; then ((PASS++)); else ((FAIL++)); fi + +echo "" +echo "Testing AsyncURIMatcher factory methods:" + +# Factory exact match (should NOT match subpaths) +if test_route "/factory/exact" "200" "Factory exact match"; then ((PASS++)); else ((FAIL++)); fi + +# Factory prefix match +if test_route "/factory/prefix" "200" "Factory prefix base"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/factory/prefix-test" "200" "Factory prefix extended"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/factory/prefix/sub" "200" "Factory prefix subpath"; then ((PASS++)); else ((FAIL++)); fi + +# Factory directory match (should NOT match the directory itself) +if test_route "/factory/dir/users" "200" "Factory directory match"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/factory/dir/sub/path" "200" "Factory directory deep"; then ((PASS++)); else ((FAIL++)); fi + +# Factory extension match +if test_route "/factory/files/doc.txt" "200" "Factory extension match"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/factory/files/sub/readme.txt" "200" "Factory extension deep"; then ((PASS++)); else ((FAIL++)); fi + +echo "" +echo "Testing case insensitive matching:" + +# Case insensitive exact +if test_route "/case/exact" "200" "Case exact lowercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/CASE/EXACT" "200" "Case exact uppercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/Case/Exact" "200" "Case exact mixed"; then ((PASS++)); else ((FAIL++)); fi + +# Case insensitive prefix +if test_route "/case/prefix" "200" "Case prefix lowercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/CASE/PREFIX-test" "200" "Case prefix uppercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/Case/Prefix/sub" "200" "Case prefix mixed"; then ((PASS++)); else ((FAIL++)); fi + +# Case insensitive directory +if test_route "/case/dir/users" "200" "Case dir lowercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/CASE/DIR/admin" "200" "Case dir uppercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/Case/Dir/settings" "200" "Case dir mixed"; then ((PASS++)); else ((FAIL++)); fi + +# Case insensitive extension +if test_route "/case/files/doc.pdf" "200" "Case ext lowercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/CASE/FILES/DOC.PDF" "200" "Case ext uppercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/Case/Files/Doc.Pdf" "200" "Case ext mixed"; then ((PASS++)); else ((FAIL++)); fi + +echo "" +echo "Testing special matchers:" + +# Test POST to catch-all (all() matcher) +echo -n "Testing POST /any/path (all matcher) ... " +response=$(curl -s -X POST -w "HTTPSTATUS:%{http_code}" "$BASE_URL/any/path" 2>/dev/null) +status_code=$(echo "$response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2) +if [ "$status_code" = "200" ]; then + echo "✅ PASS ($status_code)" + ((PASS++)) +else + echo "❌ FAIL (expected 200, got $status_code)" + ((FAIL++)) +fi + +# Check if regex is enabled by testing the server +echo "" +echo "Checking for regex support..." +regex_test=$(curl -s "$BASE_URL/user/123" 2>/dev/null) +if curl -s -w "%{http_code}" "$BASE_URL/user/123" 2>/dev/null | grep -q "200"; then + echo "Regex support detected - testing traditional regex routes:" + if test_route "/user/123" "200" "Traditional regex user ID"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/user/456" "200" "Traditional regex user ID 2"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/blog/2023/10/15" "200" "Traditional regex blog date"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/blog/2024/12/25" "200" "Traditional regex blog date 2"; then ((PASS++)); else ((FAIL++)); fi + + echo "Testing AsyncURIMatcher regex factory methods:" + if test_route "/factory/user/123" "200" "Factory regex user ID"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/factory/user/789" "200" "Factory regex user ID 2"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/factory/blog/2023/10/15" "200" "Factory regex blog date"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/factory/blog/2024/12/31" "200" "Factory regex blog date 2"; then ((PASS++)); else ((FAIL++)); fi + + # Case insensitive regex + if test_route "/factory/search/hello" "200" "Factory regex search lowercase"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/FACTORY/SEARCH/WORLD" "200" "Factory regex search uppercase"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/Factory/Search/Test" "200" "Factory regex search mixed"; then ((PASS++)); else ((FAIL++)); fi +else + echo "Regex support not detected (compile with ASYNCWEBSERVER_REGEX to enable)" +fi + +echo "" +echo "Testing routes that should fail (404 Not Found):" + +if test_route "/nonexistent" "404" "Non-existent route"; then ((PASS++)); else ((FAIL++)); fi + +# Test factory exact vs traditional behavior difference +if test_route "/factory/exact/sub" "404" "Factory exact should NOT match subpaths"; then ((PASS++)); else ((FAIL++)); fi + +# Test factory directory requires trailing slash +if test_route "/factory/dir" "404" "Factory directory should NOT match without trailing slash"; then ((PASS++)); else ((FAIL++)); fi + +# Test extension mismatch +if test_route "/factory/files/doc.pdf" "404" "Factory extension mismatch (.pdf vs .txt)"; then ((PASS++)); else ((FAIL++)); fi + +# Test case sensitive when flag not used +if test_route "/exact" "200" "Traditional exact lowercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/EXACT" "404" "Traditional exact should be case sensitive"; then ((PASS++)); else ((FAIL++)); fi + +# Test regex validation +if test_route "/user/abc" "404" "Invalid regex (letters instead of numbers)"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/blog/23/10/15" "404" "Invalid regex (2-digit year)"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/factory/user/abc" "404" "Factory regex invalid (letters)"; then ((PASS++)); else ((FAIL++)); fi + +echo "" +echo "==================================" +echo "Test Results:" +echo "✅ Passed: $PASS" +echo "❌ Failed: $FAIL" +echo "Total: $((PASS + FAIL))" + +if [ $FAIL -eq 0 ]; then + echo "" + echo "🎉 All tests passed! URI matching is working correctly." + exit 0 +else + echo "" + echo "❌ Some tests failed. Check the server and routes." + exit 1 +fi diff --git a/platformio.ini b/platformio.ini index 380f859d..d3f07456 100644 --- a/platformio.ini +++ b/platformio.ini @@ -33,6 +33,8 @@ src_dir = examples/PerfTests ; src_dir = examples/StaticFile ; src_dir = examples/Templates ; src_dir = examples/Upload +; src_dir = examples/URIMatcher +; src_dir = examples/URIMatcherTest ; src_dir = examples/WebSocket ; src_dir = examples/WebSocketEasy diff --git a/src/AsyncJson.cpp b/src/AsyncJson.cpp index c6643f76..6c62b690 100644 --- a/src/AsyncJson.cpp +++ b/src/AsyncJson.cpp @@ -112,11 +112,12 @@ size_t AsyncMessagePackResponse::_fillBuffer(uint8_t *data, size_t len) { // Body handler supporting both content types: JSON and MessagePack #if ARDUINOJSON_VERSION_MAJOR == 6 -AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(const String &uri, ArJsonRequestHandlerFunction onRequest, size_t maxJsonBufferSize) - : _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {} +AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest, size_t maxJsonBufferSize) + : _uri(std::move(uri)), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), + _maxContentLength(16384) {} #else -AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(const String &uri, ArJsonRequestHandlerFunction onRequest) - : _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {} +AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest) + : _uri(std::move(uri)), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {} #endif bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest *request) const { @@ -124,7 +125,7 @@ bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest *request) cons return false; } - if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) { + if (!_uri.matches(request)) { return false; } diff --git a/src/AsyncJson.h b/src/AsyncJson.h index 42ec7b5e..70e8b756 100644 --- a/src/AsyncJson.h +++ b/src/AsyncJson.h @@ -88,7 +88,7 @@ class AsyncMessagePackResponse : public AsyncJsonResponse { class AsyncCallbackJsonWebHandler : public AsyncWebHandler { protected: - String _uri; + AsyncURIMatcher _uri; WebRequestMethodComposite _method; ArJsonRequestHandlerFunction _onRequest; #if ARDUINOJSON_VERSION_MAJOR == 6 @@ -98,9 +98,9 @@ class AsyncCallbackJsonWebHandler : public AsyncWebHandler { public: #if ARDUINOJSON_VERSION_MAJOR == 6 - AsyncCallbackJsonWebHandler(const String &uri, ArJsonRequestHandlerFunction onRequest = nullptr, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); + AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest = nullptr, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); #else - AsyncCallbackJsonWebHandler(const String &uri, ArJsonRequestHandlerFunction onRequest = nullptr); + AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest = nullptr); #endif void setMethod(WebRequestMethodComposite method) { diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index 31267d71..51f50392 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -220,6 +220,7 @@ class AsyncWebServerRequest { friend class AsyncCallbackWebHandler; friend class AsyncFileResponse; friend class AsyncStaticWebHandler; + friend class AsyncURIMatcher; private: AsyncClient *_client; @@ -729,6 +730,333 @@ class AsyncWebServerRequest { String urlDecode(const String &text) const; }; +class AsyncURIMatcher { +public: + // Modifier flags for AsyncURIMatcher behavior + + /** + * @brief No special matching behavior (default) + */ + static constexpr uint16_t None = 0; + + /** + * @brief Enable case-insensitive URI matching + * + * When CaseInsensitive is specified: + * - The URI pattern is converted to lowercase during construction + * - Incoming request URLs are converted to lowercase before matching + * - For regex matchers, the std::regex::icase flag is used + * + * Example usage: + * ```cpp + * // Matches /login, /LOGIN, /Login, /LoGiN, etc. + * server.on(AsyncURIMatcher::exact("/login", AsyncURIMatcher::CaseInsensitive), handler); + * + * // Matches /api/\*, /API/\*, /Api/\*, etc. + * server.on(AsyncURIMatcher::prefix("/api", AsyncURIMatcher::CaseInsensitive), handler); + * + * // Regex with case insensitive matching + * server.on(AsyncURIMatcher::regex("^/user/([a-z]+)$", AsyncURIMatcher::CaseInsensitive), handler); + * ``` + * + * Performance note: Case conversion adds minimal overhead during construction and matching. + */ + static constexpr uint16_t CaseInsensitive = (1 << 0); + + // public constructors + AsyncURIMatcher() : _flags(All) {} + AsyncURIMatcher(String uri, uint16_t flags = None) : AsyncURIMatcher(std::move(uri), Auto, flags) {} + AsyncURIMatcher(const char *uri, uint16_t flags = None) : AsyncURIMatcher(String(uri), Auto, flags) {} + +#ifdef ASYNCWEBSERVER_REGEX + AsyncURIMatcher(const AsyncURIMatcher &c) : _value(c._value), _flags(c._flags) { + if (_isRegex()) { + pattern = new std::regex(*pattern); + } + } + + AsyncURIMatcher(AsyncURIMatcher &&c) : _value(std::move(c._value)), _flags(c._flags) { + c._flags = 0; + } + + ~AsyncURIMatcher() { + if (_isRegex()) { + delete pattern; + } + } + + AsyncURIMatcher &operator=(const AsyncURIMatcher &r) { + _value = r._value; + if (r._isRegex()) { + // Allocate first before we delete our current state + auto p = new std::regex(*r.pattern); + // Safely reassign our pattern + if (_isRegex()) { + delete pattern; + } + pattern = p; + } else { + if (_isRegex()) { + delete pattern; + } + _flags = r._flags; + } + return *this; + } + + AsyncURIMatcher &operator=(AsyncURIMatcher &&r) { + _value = std::move(r._value); + if (_isRegex()) { + delete pattern; + } + _flags = r._flags; + if (r._isRegex()) { + // We have adopted it + r._flags = 0; + } + return *this; + } + +#else + AsyncURIMatcher(const AsyncURIMatcher &) = default; + AsyncURIMatcher(AsyncURIMatcher &&) = default; + ~AsyncURIMatcher() = default; + + AsyncURIMatcher &operator=(const AsyncURIMatcher &) = default; + AsyncURIMatcher &operator=(AsyncURIMatcher &&) = default; +#endif + + bool matches(AsyncWebServerRequest *request) const { + // Match-all is tested first + if (_flags & All) { + return true; + } + +#ifdef ASYNCWEBSERVER_REGEX + if (_isRegex()) { + std::smatch matches; + std::string s(request->url().c_str()); + if (std::regex_search(s, matches, *pattern)) { + for (size_t i = 1; i < matches.size(); ++i) { + request->_pathParams.emplace_back(matches[i].str().c_str()); + } + return true; + } + return false; + } +#endif + String path = request->url(); + if (_flags & (CaseInsensitive << 16)) { + path.toLowerCase(); + } + + // Exact match (should be the most common case) + if ((_flags & Exact) && (_value == path)) { + return true; + } + + // Prefix match types + if ((_flags & Prefix) && path.startsWith(_value)) { + return true; + } + if ((_flags & PrefixFolder) && path.startsWith(_value + "/")) { + return true; + } + + // Extension match + if (_flags & Extension) { + int split = _value.lastIndexOf("/*."); + if (split >= 0 && path.startsWith(_value.substring(0, split)) && path.endsWith(_value.substring(split + 2))) { + return true; + } + } + + // we did not match + return false; + } + + // static factory methods for common match types + + /** + * @brief Create a matcher that matches all URIs unconditionally + * @return AsyncURIMatcher that accepts any request URL + * + * Usage: server.on(AsyncURIMatcher::all(), handler); + */ + static inline AsyncURIMatcher all() { + return AsyncURIMatcher{{}, All, None}; + } + + /** + * @brief Create an exact URI matcher + * @param c The exact URI string to match (e.g., "/login", "/api/status") + * @param flags Optional modifier flags (CaseInsensitive, etc.) + * @return AsyncURIMatcher that matches only the exact URI + * + * Usage: server.on(AsyncURIMatcher::exact("/login"), handler); + * Matches: "/login" + * Doesn't match: "/login/", "/login-page" + * Doesn't match: "/LOGIN" (unless CaseInsensitive flag used) + */ + static inline AsyncURIMatcher exact(String c, uint16_t flags = None) { + return AsyncURIMatcher{std::move(c), Exact, flags}; + } + + /** + * @brief Create a prefix URI matcher + * @param c The URI prefix to match (e.g., "/api", "/static") + * @param flags Optional modifier flags (CaseInsensitive, etc.) + * @return AsyncURIMatcher that matches URIs starting with the prefix + * + * Usage: server.on(AsyncURIMatcher::prefix("/api"), handler); + * Matches: "/api", "/api/users", "/api-v2", "/apitest" + * Note: This is pure prefix matching - does NOT require folder separator + */ + static inline AsyncURIMatcher prefix(String c, uint16_t flags = None) { + return AsyncURIMatcher{std::move(c), Prefix, flags}; + } + + /** + * @brief Create a directory/folder URI matcher + * @param c The directory path (trailing slash automatically added if missing) + * @param flags Optional modifier flags (CaseInsensitive, etc.) + * @return AsyncURIMatcher that matches URIs under the directory + * + * Usage: server.on(AsyncURIMatcher::dir("/admin"), handler); + * Matches: "/admin/users", "/admin/settings", "/admin/sub/path" + * Doesn't match: "/admin" (exact), "/admin-panel" (no folder separator) + * + * The trailing slash is automatically added for convenience and efficiency. + */ + static inline AsyncURIMatcher dir(String c, uint16_t flags = None) { + // Pre-calculate folder for efficiency + if (!c.length()) { + return AsyncURIMatcher{"/", Prefix, flags}; + } + if (c[c.length() - 1] != '/') { + c.concat('/'); + } + return AsyncURIMatcher{std::move(c), Prefix, flags}; + } + + /** + * @brief Create a file extension URI matcher + * @param c The pattern with wildcard extension (e.g., "/images/\*.jpg", "/docs/\*.pdf") + * @param flags Optional modifier flags (CaseInsensitive, etc.) + * @return AsyncURIMatcher that matches files with specific extensions under a path + * + * Usage: server.on(AsyncURIMatcher::ext("/images/\*.jpg"), handler); + * Matches: "/images/photo.jpg", "/images/gallery/pic.jpg" + * Doesn't match: "/images/photo.png", "/img/photo.jpg" + * + * Pattern format: "/path/\*.extension" where "*" is a literal wildcard placeholder. + * The path before "/\*." must match exactly, and the URI must end with the extension. + */ + static inline AsyncURIMatcher ext(String c, uint16_t flags = None) { + return AsyncURIMatcher{std::move(c), Extension, flags}; + } + +#ifdef ASYNCWEBSERVER_REGEX + /** + * @brief Create a regular expression URI matcher + * @param c The regex pattern string (e.g., "^/user/([0-9]+)$", "^/blog/([0-9]{4})/([0-9]{2})$") + * @param flags Optional modifier flags (CaseInsensitive applies to regex compilation) + * @return AsyncURIMatcher that matches URIs using regex with capture groups + * + * Usage: server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), handler); + * Matches: "/user/123", "/user/456" + * Doesn't match: "/user/abc", "/user/123/profile" + * + * Captured groups can be accessed via request->pathArg(index) in the handler. + * Requires ASYNCWEBSERVER_REGEX to be defined during compilation. + * Performance note: Regex matching is slower than other match types. + */ + static inline AsyncURIMatcher regex(String c, uint16_t flags = None) { + return AsyncURIMatcher{std::move(c), Regex, flags}; + } +#endif + +private: + // Matcher types + enum Type : uint16_t { + // Meta flags - low bits + Auto = (1 << 0), // parse _uri at construct time and infer match type(s) + // (_uri may be transformed to remove wildcards) + + All = (1 << 1), // No flags set + Exact = (1 << 2), // matches equivalent to regex: ^{_uri}$ + Prefix = (1 << 3), // matches equivalent to regex: ^{_uri}.* + PrefixFolder = (1 << 4), // matches equivalent to regex: ^{_uri}/.* + Extension = (1 << 5), // non-regular match: /pattern../*.ext + +#ifdef ASYNCWEBSERVER_REGEX + NonRegex = (1 << 0), // bit to use as pointer tag + Regex = (1 << 15), // matches _url as regex +#endif + }; + + // fields + String _value; + union { + intptr_t _flags; +#ifdef ASYNCWEBSERVER_REGEX + // Overlay the pattern pointer storage with the flags. It is treated as a tagged pointer: + // if any of the LSBs are set, it stores flags, as a valid object must be aligned and so + // none of the LSBs can be set in a valid pointer. + std::regex *pattern; +#endif + }; + + // private functions +#ifdef ASYNCWEBSERVER_REGEX + inline bool _isRegex() const { + static_assert( + (std::alignment_of::value % 2) == 0, "Unexpected regex type alignment - please let the ESPAsyncWebServer team know about your platform!" + ); + // pattern is non-null pointer with correct alignment. + // We use the _flags view as it's already a integer type. + return _flags && !(_flags & (std::alignment_of::value - 1)); + } +#endif + + // Core private constructor + AsyncURIMatcher(String uri, Type type = Auto, uint16_t flags = None) : _value(std::move(uri)), _flags(uint32_t(flags) << 16 | type) { +#ifdef ASYNCWEBSERVER_REGEX + if ((type & Regex) || ((type & Auto) && _value.startsWith("^") && _value.endsWith("$"))) { + pattern = new std::regex(_value.c_str(), (flags & CaseInsensitive) ? (std::regex::icase | std::regex::optimize) : (std::regex::optimize)); + return; // no additional processing - flags are overwritten + } +#endif + if (flags & CaseInsensitive) { + _value.toLowerCase(); + } + if (type & Auto) { + // Inspect _value to set flags + // empty URI matches everything + if (!_value.length()) { + _flags = All; + return; // Does not require extra bit for regex disambiguation + } + if (_value.endsWith("*")) { + // wildcard match with * at the end + _flags |= Prefix; + _value = _value.substring(0, _value.length() - 1); + } else if (_value.lastIndexOf("/*.") >= 0) { + // prefix match with /*.ext + // matches any path ending with .ext + // e.g. /images/*.png will match /images/pic.png and /images/2023/pic.png but not /img/pic.png + _flags |= Extension; + } else { + // No special values - use default of folder and exact + _flags |= PrefixFolder | Exact; + } + } +#ifdef ASYNCWEBSERVER_REGEX + _flags |= NonRegex; // disambiguate regex case +#endif + } +}; + /* * FILTER :: Callback to filter AsyncWebRewrite and AsyncWebHandler (done by the Server) * */ @@ -1261,16 +1589,16 @@ class AsyncWebServer : public AsyncMiddlewareChain { AsyncWebHandler &addHandler(AsyncWebHandler *handler); bool removeHandler(AsyncWebHandler *handler); - AsyncCallbackWebHandler &on(const char *uri, ArRequestHandlerFunction onRequest) { - return on(uri, HTTP_ANY, onRequest); + AsyncCallbackWebHandler &on(AsyncURIMatcher uri, ArRequestHandlerFunction onRequest) { + return on(std::move(uri), HTTP_ANY, onRequest); } AsyncCallbackWebHandler &on( - const char *uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload = nullptr, + AsyncURIMatcher uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload = nullptr, ArBodyHandlerFunction onBody = nullptr ); #if ASYNC_JSON_SUPPORT == 1 - AsyncCallbackJsonWebHandler &on(const char *uri, WebRequestMethodComposite method, ArJsonRequestHandlerFunction onBody); + AsyncCallbackJsonWebHandler &on(AsyncURIMatcher uri, WebRequestMethodComposite method, ArJsonRequestHandlerFunction onBody); #endif AsyncStaticWebHandler &serveStatic(const char *uri, fs::FS &fs, const char *path, const char *cache_control = NULL); diff --git a/src/WebHandlerImpl.h b/src/WebHandlerImpl.h index 8dca3f5e..77023bbb 100644 --- a/src/WebHandlerImpl.h +++ b/src/WebHandlerImpl.h @@ -54,7 +54,7 @@ class AsyncStaticWebHandler : public AsyncWebHandler { class AsyncCallbackWebHandler : public AsyncWebHandler { private: protected: - String _uri; + AsyncURIMatcher _uri; WebRequestMethodComposite _method; ArRequestHandlerFunction _onRequest; ArUploadHandlerFunction _onUpload; @@ -63,7 +63,7 @@ class AsyncCallbackWebHandler : public AsyncWebHandler { public: AsyncCallbackWebHandler() : _uri(), _method(HTTP_ANY), _onRequest(NULL), _onUpload(NULL), _onBody(NULL), _isRegex(false) {} - void setUri(const String &uri); + void setUri(AsyncURIMatcher uri); void setMethod(WebRequestMethodComposite method) { _method = method; } diff --git a/src/WebHandlers.cpp b/src/WebHandlers.cpp index 0082937d..8addf311 100644 --- a/src/WebHandlers.cpp +++ b/src/WebHandlers.cpp @@ -295,47 +295,15 @@ AsyncStaticWebHandler &AsyncStaticWebHandler::setTemplateProcessor(AwsTemplatePr return *this; } -void AsyncCallbackWebHandler::setUri(const String &uri) { - _uri = uri; - _isRegex = uri.startsWith("^") && uri.endsWith("$"); +void AsyncCallbackWebHandler::setUri(AsyncURIMatcher uri) { + _uri = std::move(uri); } bool AsyncCallbackWebHandler::canHandle(AsyncWebServerRequest *request) const { if (!_onRequest || !request->isHTTP() || !(_method & request->method())) { return false; } - -#ifdef ASYNCWEBSERVER_REGEX - if (_isRegex) { - std::regex pattern(_uri.c_str()); - std::smatch matches; - std::string s(request->url().c_str()); - if (std::regex_search(s, matches, pattern)) { - for (size_t i = 1; i < matches.size(); ++i) { // start from 1 - request->_pathParams.emplace_back(matches[i].str().c_str()); - } - } else { - return false; - } - } else -#endif - if (_uri.length() && _uri.startsWith("/*.")) { - String uriTemplate = String(_uri); - uriTemplate = uriTemplate.substring(uriTemplate.lastIndexOf(".")); - if (!request->url().endsWith(uriTemplate)) { - return false; - } - } else if (_uri.length() && _uri.endsWith("*")) { - String uriTemplate = String(_uri); - uriTemplate = uriTemplate.substring(0, uriTemplate.length() - 1); - if (!request->url().startsWith(uriTemplate)) { - return false; - } - } else if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) { - return false; - } - - return true; + return _uri.matches(request); } void AsyncCallbackWebHandler::handleRequest(AsyncWebServerRequest *request) { diff --git a/src/WebServer.cpp b/src/WebServer.cpp index 657acbfa..c6d8adfc 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -151,10 +151,10 @@ void AsyncWebServer::_attachHandler(AsyncWebServerRequest *request) { } AsyncCallbackWebHandler &AsyncWebServer::on( - const char *uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody + AsyncURIMatcher uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody ) { AsyncCallbackWebHandler *handler = new AsyncCallbackWebHandler(); - handler->setUri(uri); + handler->setUri(std::move(uri)); handler->setMethod(method); handler->onRequest(onRequest); handler->onUpload(onUpload); @@ -164,8 +164,8 @@ AsyncCallbackWebHandler &AsyncWebServer::on( } #if ASYNC_JSON_SUPPORT == 1 -AsyncCallbackJsonWebHandler &AsyncWebServer::on(const char *uri, WebRequestMethodComposite method, ArJsonRequestHandlerFunction onBody) { - AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler(uri, onBody); +AsyncCallbackJsonWebHandler &AsyncWebServer::on(AsyncURIMatcher uri, WebRequestMethodComposite method, ArJsonRequestHandlerFunction onBody) { + AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler(std::move(uri), onBody); handler->setMethod(method); addHandler(handler); return *handler;