From 4f1d409a0df56ba5be40f85b6aaf7d9430f55f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= <938089+lboue@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:14:50 +0200 Subject: [PATCH] feat(br_server): add REST API discovery endpoint (/.well-known/thread/esp-br-rest) Add a REST API discovery endpoint and a tracked version number for esp-thread-br's own REST API. - GET /.well-known/thread/esp-br-rest: RFC 8615 well-known URI returning the running REST API's version and RFC 8288-style links to its entry points, so clients can discover capabilities at runtime instead of hardcoding/probing endpoint paths. Served on its own esp-thread-br-specific path so clients that need to support both esp-thread-br's and ot-br-posix's REST APIs can probe each implementation's own well-known path to disambiguate them. - ESP_OT_REST_API_VERSION (in esp_br_web_api.h): single source of truth for the REST API's semver version, starting at 1.0.0 and versioned independently from ot-br-posix's OTBR_REST_API_VERSION, since the two implementations' REST surfaces differ. - Bump openapi.yaml's info.version (and ESP_OT_REST_API_VERSION) to 1.1.0, accounting for the ePSKc endpoints (/node/ba-epskc/state, /node/ba-epskc/key) added since the 1.0.0 baseline without a version bump. - tools/ci/check_rest_api_version_sync.py, wired into .pre-commit-config.yaml, fails if ESP_OT_REST_API_VERSION and openapi.yaml's info.version ever drift apart. - components/esp_ot_br_server/CHANGELOG.md tracks the REST API's version history going forward. Closes #215. Co-Authored-By: Claude Sonnet 5 --- .pre-commit-config.yaml | 10 ++++ components/esp_ot_br_server/CHANGELOG.md | 23 +++++++ .../private_include/esp_br_web_api.h | 16 +++++ components/esp_ot_br_server/src/esp_br_web.c | 53 ++++++++++++++++ components/esp_ot_br_server/src/openapi.yaml | 47 ++++++++++++++- tools/ci/check_rest_api_version_sync.py | 60 +++++++++++++++++++ 6 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 components/esp_ot_br_server/CHANGELOG.md create mode 100644 tools/ci/check_rest_api_version_sync.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 991b9b22..7034d876 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,6 +16,16 @@ repos: language: python additional_dependencies: [pyyaml] + - repo: local + hooks: + - id: check-rest-api-version-sync + name: Check REST API version is in sync between esp_br_web_api.h and openapi.yaml + entry: python3 tools/ci/check_rest_api_version_sync.py + language: python + additional_dependencies: [pyyaml] + pass_filenames: false + files: '^components/esp_ot_br_server/(private_include/esp_br_web_api\.h|src/openapi\.yaml)$' + - repo: local hooks: - id: codespell diff --git a/components/esp_ot_br_server/CHANGELOG.md b/components/esp_ot_br_server/CHANGELOG.md new file mode 100644 index 00000000..f8411808 --- /dev/null +++ b/components/esp_ot_br_server/CHANGELOG.md @@ -0,0 +1,23 @@ +# REST API Changelog + +Version history of the esp-thread-br REST API, as reported by `ESP_OT_REST_API_VERSION` +(`private_include/esp_br_web_api.h`) and `openapi.yaml`'s `info.version`. Follows +[Semantic Versioning](https://semver.org/): MAJOR for incompatible API changes, MINOR for +backward-compatible additions, PATCH for backward-compatible fixes. + +## [1.1.0] + +### Added +- Border Agent ephemeral key (ePSKc) endpoints: `GET`/`PUT /node/ba-epskc/state` and + `GET`/`POST`/`DELETE /node/ba-epskc/key`. +- REST API discovery endpoint: `GET /.well-known/thread/esp-br-rest`, returning the running + REST API version and RFC 8288 links to its entry points + (see https://github.com/espressif/esp-thread-br/pull/216). + +## [1.0.0] + +### Added +- Initial versioned REST API surface: `/node`, `/node/rloc`, `/node/rloc16`, `/node/state`, + `/node/ext-address`, `/node/network-name`, `/node/leader-data`, `/node/num-of-router`, + `/node/ext-panid`, `/node/ba-id`, `/node/dataset/active`, `/node/dataset/pending`, and + `/diagnostics`. diff --git a/components/esp_ot_br_server/private_include/esp_br_web_api.h b/components/esp_ot_br_server/private_include/esp_br_web_api.h index 14bf18d3..db869b35 100644 --- a/components/esp_ot_br_server/private_include/esp_br_web_api.h +++ b/components/esp_ot_br_server/private_include/esp_br_web_api.h @@ -20,10 +20,26 @@ extern "C" { */ void esp_br_web_api_init(void); +/** + * @brief REST API semantic version, the single source of truth for this component's REST API version. + * + * Bump this value, and openapi.yaml's `info.version`, together whenever a REST endpoint is added, removed, + * or changed, following semver: + * - MAJOR: an incompatible API change. + * - MINOR: backward-compatible functionality added. + * - PATCH: a backward-compatible bug fix. + * Record the change in ../CHANGELOG.md. + * + * `tools/ci/check_rest_api_version_sync.py` enforces that this value and openapi.yaml's `info.version` + * stay in sync. + */ +#define ESP_OT_REST_API_VERSION "1.1.0" + /*--------------------------------------------------------------------- ESP Thread Border Router Wer Server REST API ----------------------------------------------------------------------*/ /* HTTP GET */ +#define ESP_OT_REST_API_WELL_KNOWN_ESP_BR_REST_PATH "/.well-known/thread/esp-br-rest" #define ESP_OT_REST_API_DIAGNOSTICS_PATH "/diagnostics" #define ESP_OT_REST_API_NODE_PATH "/node" #define ESP_OT_REST_API_NODE_RLOC_PATH "/node/rloc" diff --git a/components/esp_ot_br_server/src/esp_br_web.c b/components/esp_ot_br_server/src/esp_br_web.c index aed0a9e9..0df97233 100644 --- a/components/esp_ot_br_server/src/esp_br_web.c +++ b/components/esp_ot_br_server/src/esp_br_web.c @@ -109,8 +109,15 @@ static esp_err_t esp_otbr_network_node_epskc_state_put_handler(httpd_req_t *req) static esp_err_t esp_otbr_network_node_epskc_key_get_handler(httpd_req_t *req); static esp_err_t esp_otbr_network_node_epskc_key_post_handler(httpd_req_t *req); static esp_err_t esp_otbr_network_node_epskc_key_delete_handler(httpd_req_t *req); +static esp_err_t esp_otbr_well_known_br_rest_get_handler(httpd_req_t *req); static httpd_uri_t s_resource_handlers[] = { + { + .uri = ESP_OT_REST_API_WELL_KNOWN_ESP_BR_REST_PATH, + .method = HTTP_GET, + .handler = esp_otbr_well_known_br_rest_get_handler, + .user_ctx = NULL, + }, { .uri = ESP_OT_REST_API_DIAGNOSTICS_PATH, .method = HTTP_GET, @@ -817,6 +824,52 @@ static esp_err_t esp_otbr_network_node_epskc_key_delete_handler(httpd_req_t *req return ret; } +/** + * @brief REST API discovery endpoint (RFC 8615 well-known URI). Returns the REST API's version and + * RFC 8288-style links to its entry points, so clients can discover them at runtime instead + * of hardcoding or probing endpoint paths. + * + * @param[in] req The request from http client. + * @return + * - ESP_OK : On success + * - ESP_FAIL : Failed to handle @param req + */ +static esp_err_t esp_otbr_well_known_br_rest_get_handler(httpd_req_t *req) +{ + esp_err_t ret = ESP_OK; + cJSON *response = cJSON_CreateObject(); + ESP_RETURN_ON_FALSE(response, ESP_FAIL, WEB_TAG, "Failed to allocate well-known response"); + + cJSON *api = cJSON_CreateObject(); + ESP_GOTO_ON_FALSE(api, ESP_FAIL, exit, WEB_TAG, "Failed to allocate well-known api object"); + cJSON_AddStringToObject(api, "version", ESP_OT_REST_API_VERSION); + cJSON_AddStringToObject(api, "base", "/"); + cJSON_AddItemToObject(response, "api", api); + + cJSON *links = cJSON_AddArrayToObject(response, "links"); + static const struct { + const char *href; + const char *rel; + } entry_points[] = { + {ESP_OT_REST_API_WELL_KNOWN_ESP_BR_REST_PATH, "self"}, + {ESP_OT_REST_API_NODE_PATH, "node"}, + {ESP_OT_REST_API_DIAGNOSTICS_PATH, "diagnostic"}, + }; + for (size_t i = 0; i < sizeof(entry_points) / sizeof(entry_points[0]); i++) { + cJSON *link = cJSON_CreateObject(); + cJSON_AddStringToObject(link, "href", entry_points[i].href); + cJSON_AddStringToObject(link, "rel", entry_points[i].rel); + cJSON *type = cJSON_AddArrayToObject(link, "type"); + cJSON_AddItemToArray(type, cJSON_CreateString(ESP_OT_REST_CONTENT_TYPE_JSON)); + cJSON_AddItemToArray(links, link); + } + + ESP_GOTO_ON_ERROR(httpd_send_packet(req, response), exit, WEB_TAG, "Failed to response %s", req->uri); +exit: + cJSON_Delete(response); + return ret; +} + /*----------------------------------------------------- Note:Openthread WEB GUI API implement -----------------------------------------------------*/ diff --git a/components/esp_ot_br_server/src/openapi.yaml b/components/esp_ot_br_server/src/openapi.yaml index 4302a435..0e5991db 100644 --- a/components/esp_ot_br_server/src/openapi.yaml +++ b/components/esp_ot_br_server/src/openapi.yaml @@ -10,15 +10,29 @@ info: license: name: Apache url: https://github.com/espressif/esp-thread-br/blob/main/LICENSE - version: 1.0.0 + version: 1.1.0 servers: - url: http://localhost:80 tags: + - name: discovery + description: REST API version and entry point discovery. - name: node description: Thread parameters of this node. - name: diagnostics description: Thread network diagnostic. paths: + /.well-known/thread/esp-br-rest: + get: + tags: + - discovery + summary: Discover the REST API version and entry points + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/WellKnownBrRest" /diagnostics: get: tags: @@ -389,6 +403,37 @@ paths: description: Successful operation. components: schemas: + WellKnownBrRest: + type: object + properties: + api: + type: object + properties: + version: + type: string + description: The running REST API's semantic version. + example: "1.1.0" + base: + type: string + description: The base path entry points are relative to. + example: "/" + links: + type: array + description: RFC 8288 web links to REST API entry points. + items: + type: object + properties: + href: + type: string + example: "/node" + rel: + type: string + example: "node" + type: + type: array + items: + type: string + example: ["application/json"] LeaderData: type: object properties: diff --git a/tools/ci/check_rest_api_version_sync.py b/tools/ci/check_rest_api_version_sync.py new file mode 100644 index 00000000..b71e6a77 --- /dev/null +++ b/tools/ci/check_rest_api_version_sync.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# +# SPDX-License-Identifier: Apache-2.0 +""" +Check that ESP_OT_REST_API_VERSION (the runtime source of truth returned by the +REST API discovery endpoint) and openapi.yaml's `info.version` stay in sync, so the +two do not silently drift apart when one is bumped without the other. + +See https://github.com/espressif/esp-thread-br/pull/216#discussion_r3819911931. +""" + +import re +import sys +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parents[2] +HEADER_PATH = REPO_ROOT / "components/esp_ot_br_server/private_include/esp_br_web_api.h" +OPENAPI_PATH = REPO_ROOT / "components/esp_ot_br_server/src/openapi.yaml" + +VERSION_DEFINE_RE = re.compile(r'#define\s+ESP_OT_REST_API_VERSION\s+"([^"]+)"') + + +def get_header_version(path: Path) -> str: + match = VERSION_DEFINE_RE.search(path.read_text()) + if not match: + print(f"Failed to find ESP_OT_REST_API_VERSION in {path}") + sys.exit(1) + return match.group(1) + + +def get_openapi_version(path: Path) -> str: + with open(path) as f: + spec = yaml.safe_load(f) + try: + return spec["info"]["version"] + except (KeyError, TypeError): + print(f"Failed to find info.version in {path}") + sys.exit(1) + + +def main() -> None: + header_version = get_header_version(HEADER_PATH) + openapi_version = get_openapi_version(OPENAPI_PATH) + + if header_version != openapi_version: + print( + f"REST API version mismatch: ESP_OT_REST_API_VERSION in {HEADER_PATH} is " + f"'{header_version}', but info.version in {OPENAPI_PATH} is '{openapi_version}'. " + "Bump both together." + ) + sys.exit(1) + + print(f"REST API version check passed: {header_version}") + + +if __name__ == "__main__": + main()