Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions components/esp_ot_br_server/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
16 changes: 16 additions & 0 deletions components/esp_ot_br_server/private_include/esp_br_web_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
53 changes: 53 additions & 0 deletions components/esp_ot_br_server/src/esp_br_web.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check the api pointer is NULL before we use it.

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
-----------------------------------------------------*/
Expand Down
47 changes: 46 additions & 1 deletion components/esp_ot_br_server/src/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
60 changes: 60 additions & 0 deletions tools/ci/check_rest_api_version_sync.py
Original file line number Diff line number Diff line change
@@ -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()