Skip to content

Commit 3017a88

Browse files
authored
[web] support dynamic REST API port in Web UI (#3353)
This commit resolves the issue where the Web UI topology graph failed to display when the agent REST API port differed from the default 8081. Changes: - main.cpp: Added -A and -P CLI args to configure agent REST address and port on WebServer. Stored the parsed port number in a local variable during verification to avoid redundant parsing. - web_server: Added a new endpoint /get_rest_api_info to expose the configured agent REST address and port. - app.js: Dynamically queries /get_rest_api_info on load to initialize host and port; falls back to loopback and 8081 if undefined, and window.location.hostname if looped back. Introduced a new formatRestAddr helper function to format target host/port strings cleanly (wrapping IPv6 loopback/any hosts inside brackets). - s6 run: Passes OT_REST_LISTEN_ADDR and OT_REST_LISTEN_PORT env variables from the docker environment to otbr-web. - dind_web.exp: Refactored the expect script to run a two-stage verification covering both default (127.0.0.1:8081) and custom (0.0.0.0:9090) values, verifying direct REST API responsiveness from the host on the custom port.
1 parent f5a95ce commit 3017a88

6 files changed

Lines changed: 195 additions & 31 deletions

File tree

etc/docker/border-router/rootfs/etc/s6-overlay/s6-rc.d/otbr-web/run

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ readonly OT_WEB_LISTEN_ADDR
3636
OT_WEB_LISTEN_PORT="${OT_WEB_LISTEN_PORT:-8080}"
3737
readonly OT_WEB_LISTEN_PORT
3838

39+
OT_REST_LISTEN_ADDR="${OT_REST_LISTEN_ADDR:-127.0.0.1}"
40+
readonly OT_REST_LISTEN_ADDR
41+
42+
OT_REST_LISTEN_PORT="${OT_REST_LISTEN_PORT:-8081}"
43+
readonly OT_REST_LISTEN_PORT
44+
3945
echo "Starting otbr-web..."
4046

41-
exec stdbuf -oL /usr/sbin/otbr-web -I "${OT_THREAD_IF}" -d6 -s -a "${OT_WEB_LISTEN_ADDR}" -p "${OT_WEB_LISTEN_PORT}"
47+
exec stdbuf -oL /usr/sbin/otbr-web -I "${OT_THREAD_IF}" -d6 -s -a "${OT_WEB_LISTEN_ADDR}" -p "${OT_WEB_LISTEN_PORT}" -A "${OT_REST_LISTEN_ADDR}" -P "${OT_REST_LISTEN_PORT}"

src/web/main.cpp

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,24 @@ int main(int argc, char **argv)
7373
const char *interfaceName = nullptr;
7474
const char *httpListenAddr = nullptr;
7575
const char *httpPort = nullptr;
76+
const char *restListenAddr = nullptr;
77+
const char *restListenPort = nullptr;
7678
otbrLogLevel logLevel = OTBR_LOG_INFO;
7779
int ret = 0;
7880
int opt;
7981
uint16_t port = OT_HTTP_PORT;
8082
bool syslogDisable = false;
8183

82-
while ((opt = getopt(argc, argv, "d:I:p:va:s")) != -1)
84+
while ((opt = getopt(argc, argv, "d:I:p:va:sA:P:")) != -1)
8385
{
8486
switch (opt)
8587
{
8688
case 'a':
8789
httpListenAddr = optarg;
8890
break;
91+
case 'A':
92+
restListenAddr = optarg;
93+
break;
8994
case 'd':
9095
logLevel = static_cast<otbrLogLevel>(atoi(optarg));
9196
break;
@@ -99,6 +104,14 @@ int main(int argc, char **argv)
99104
port = atoi(httpPort);
100105
break;
101106

107+
case 'P':
108+
restListenPort = optarg;
109+
{
110+
int portNum = atoi(restListenPort);
111+
VerifyOrExit(portNum > 0 && portNum <= 65535, ret = -1);
112+
}
113+
break;
114+
102115
case 'v':
103116
PrintVersion();
104117
ExitNow();
@@ -109,7 +122,9 @@ int main(int argc, char **argv)
109122
break;
110123

111124
default:
112-
fprintf(stderr, "Usage: %s [-d DEBUG_LEVEL] [-I interfaceName] [-p port] [-a listenAddress] [-v]\n",
125+
fprintf(stderr,
126+
"Usage: %s [-d DEBUG_LEVEL] [-I interfaceName] [-p port] [-a listenAddress] [-A restListenAddress] "
127+
"[-P restListenPort] [-v]\n",
113128
argv[0]);
114129
ExitNow(ret = -1);
115130
break;
@@ -143,6 +158,7 @@ int main(int argc, char **argv)
143158
signal(SIGINT, HandleSignal);
144159

145160
sServer.reset(new otbr::Web::WebServer());
161+
sServer->SetRestApiInfo(restListenAddr, restListenPort);
146162
if (sServer->StartWebServer(interfaceName, httpListenAddr, port) != OTBR_ERROR_NONE)
147163
{
148164
ret = -1;

src/web/web-service/frontend/res/js/app.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,19 @@
439439
};
440440

441441
$scope.restServerPort = '8081';
442-
$scope.ipAddr = window.location.hostname + ':' + $scope.restServerPort;
442+
var formatRestAddr = function(host, port) {
443+
return (host.indexOf(':') > -1) ? '[' + host + ']:' + port : host + ':' + port;
444+
};
445+
$scope.ipAddr = formatRestAddr(window.location.hostname, $scope.restServerPort);
446+
447+
$http.get('get_rest_api_info').then(function(response) {
448+
if (response.data.error == 0) {
449+
$scope.restServerPort = response.data.port;
450+
var host = response.data.host;
451+
var targetHost = (host && host !== '127.0.0.1' && host !== 'localhost' && host !== '0.0.0.0' && host !== '::1' && host !== '::') ? host : window.location.hostname;
452+
$scope.ipAddr = formatRestAddr(targetHost, $scope.restServerPort);
453+
}
454+
});
443455

444456
// Basic information line
445457
$scope.basicInfo = {

src/web/web-service/web_server.cpp

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "web/web-service/web_server.hpp"
3737

3838
#include <errno.h>
39+
#include <json/json.h>
3940
#include <string.h>
4041

4142
#include "common/code_utils.hpp"
@@ -50,31 +51,49 @@
5051
#define OT_GET_QRCODE_PATH "^/get_qrcode$"
5152
#define OT_SET_NETWORK_PATH "^/settings$"
5253
#define OT_COMMISSIONER_START_PATH "^/commission$"
54+
#define OT_GET_REST_API_INFO_PATH "^/get_rest_api_info$"
5355
#define OT_REQUEST_METHOD_GET "GET"
5456
#define OT_REQUEST_METHOD_POST "POST"
5557
#define OT_RESPONSE_SUCCESS_STATUS "HTTP/1.1 200 OK\r\n"
5658
#define OT_RESPONSE_HEADER_LENGTH "Content-Length: "
5759
#define OT_RESPONSE_HEADER_CSS_TYPE "\r\nContent-Type: text/css"
5860
#define OT_RESPONSE_HEADER_TEXT_HTML_TYPE "\r\nContent-Type: text/html; charset=utf-8"
59-
#define OT_RESPONSE_HEADER_TYPE "Content-Type: application/json\r\n charset=utf-8"
61+
#define OT_RESPONSE_HEADER_TYPE "application/json; charset=utf-8"
6062
#define OT_RESPONSE_PLACEHOLD "\r\n\r\n"
6163
#define OT_RESPONSE_FAILURE_STATUS "HTTP/1.1 400 Bad Request\r\n"
6264
#define OT_BUFFER_SIZE 1024
6365

66+
static const char kDefaultRestListenAddr[] = "127.0.0.1";
67+
static const char kDefaultRestListenPort[] = "8081";
68+
6469
namespace otbr {
6570
namespace Web {
6671

6772
using httplib::Request;
6873
using httplib::Response;
6974

7075
WebServer::WebServer(void)
76+
: mRestListenAddr(kDefaultRestListenAddr)
77+
, mRestListenPort(kDefaultRestListenPort)
7178
{
7279
}
7380

7481
WebServer::~WebServer(void)
7582
{
7683
}
7784

85+
void WebServer::SetRestApiInfo(const char *aRestListenAddr, const char *aRestListenPort)
86+
{
87+
if (aRestListenAddr != nullptr)
88+
{
89+
mRestListenAddr = aRestListenAddr;
90+
}
91+
if (aRestListenPort != nullptr)
92+
{
93+
mRestListenPort = aRestListenPort;
94+
}
95+
}
96+
7897
void WebServer::Init()
7998
{
8099
std::string networkName, extPanId;
@@ -97,6 +116,7 @@ otbrError WebServer::StartWebServer(const char *aIfName, const char *aListenAddr
97116
ResponseGetStatus();
98117
ResponseGetAvailableNetwork();
99118
ResponseCommission();
119+
ResponseGetRestApiInfo();
100120
mServer.set_mount_point("/", WEB_FILE_PATH);
101121

102122
if (!mServer.listen(aListenAddr, aPort))
@@ -284,5 +304,22 @@ std::string WebServer::HandleCommission(const std::string &aCommissionRequest)
284304
return mWpanService.HandleCommission(aCommissionRequest);
285305
}
286306

307+
void WebServer::ResponseGetRestApiInfo(void)
308+
{
309+
mServer.Get(OT_GET_REST_API_INFO_PATH, [this](const Request &aRequest, Response &aResponse) {
310+
OTBR_UNUSED_VARIABLE(aRequest);
311+
Json::Value root;
312+
Json::StreamWriterBuilder writerBuilder;
313+
std::string response;
314+
315+
root["error"] = OTBR_ERROR_NONE;
316+
root["port"] = mRestListenPort;
317+
root["host"] = mRestListenAddr;
318+
319+
response = Json::writeString(writerBuilder, root);
320+
aResponse.set_content(response, OT_RESPONSE_HEADER_TYPE);
321+
});
322+
}
323+
287324
} // namespace Web
288325
} // namespace otbr

src/web/web-service/web_server.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ class WebServer
7979
*/
8080
void StopWebServer(void);
8181

82+
/**
83+
* This method sets the REST API listen address and port.
84+
*
85+
* @param[in] aRestListenAddr The pointer to the REST API listen address.
86+
* @param[in] aRestListenPort The pointer to the REST API listen port.
87+
*/
88+
void SetRestApiInfo(const char *aRestListenAddr, const char *aRestListenPort);
89+
8290
private:
8391
typedef std::string (*HttpRequestCallback)(const std::string &aRequest, void *aUserData);
8492
static std::string HandleJoinNetworkRequest(const std::string &aJoinRequest, void *aUserData);
@@ -110,11 +118,14 @@ class WebServer
110118
void ResponseGetAvailableNetwork(void);
111119
void DefaultHttpResponse(void);
112120
void ResponseCommission(void);
121+
void ResponseGetRestApiInfo(void);
113122

114123
void Init(void);
115124

116125
httplib::Server mServer;
117126
otbr::Web::WpanService mWpanService;
127+
std::string mRestListenAddr;
128+
std::string mRestListenPort;
118129
};
119130

120131
} // namespace Web

tests/scripts/expect/dind_web.exp

Lines changed: 108 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,39 +29,55 @@
2929

3030
source "tests/scripts/expect/_common.exp"
3131

32-
proc start_otbr_docker_web {name sim_app sim_id pty1 pty2} {
32+
proc start_otbr_docker_web {name sim_app sim_id pty1 pty2 {rest_addr ""} {rest_port ""}} {
3333
# Start the container using its native /init entrypoint (s6-overlay) and custom environment variables
34-
exec docker run -d \
35-
--name $name \
36-
--network infrastructure-link \
37-
--cap-add=NET_ADMIN \
38-
--privileged \
39-
--add-host=host.docker.internal:host-gateway \
40-
--sysctl net.ipv6.conf.all.disable_ipv6=0 \
41-
--sysctl net.ipv4.conf.all.forwarding=1 \
42-
--sysctl net.ipv4.conf.default.forwarding=1 \
43-
--sysctl net.ipv6.conf.all.forwarding=1 \
44-
--sysctl net.ipv6.conf.default.forwarding=1 \
45-
-v $pty2:/dev/ttyUSB0 \
46-
-e OT_RCP_DEVICE=spinel+hdlc+uart:///dev/ttyUSB0 \
47-
-e OT_INFRA_IF=eth0 \
48-
-e OT_THREAD_IF=wpan0 \
49-
-e OT_WEB_LISTEN_ADDR=0.0.0.0 \
50-
-p 8080:8080 \
51-
$::env(EXP_OTBR_DOCKER_IMAGE)
52-
sleep 2
34+
set docker_opts [list \
35+
"docker" "run" "-d" \
36+
"--name" $name \
37+
"--network" "infrastructure-link" \
38+
"--cap-add=NET_ADMIN" \
39+
"--privileged" \
40+
"--add-host=host.docker.internal:host-gateway" \
41+
"--sysctl" "net.ipv6.conf.all.disable_ipv6=0" \
42+
"--sysctl" "net.ipv4.conf.all.forwarding=1" \
43+
"--sysctl" "net.ipv4.conf.default.forwarding=1" \
44+
"--sysctl" "net.ipv6.conf.all.forwarding=1" \
45+
"--sysctl" "net.ipv6.conf.default.forwarding=1" \
46+
"-v" "$pty2:/dev/ttyUSB0" \
47+
"-e" "OT_RCP_DEVICE=spinel+hdlc+uart:///dev/ttyUSB0" \
48+
"-e" "OT_INFRA_IF=eth0" \
49+
"-e" "OT_THREAD_IF=wpan0" \
50+
"-e" "OT_WEB_LISTEN_ADDR=0.0.0.0" \
51+
"-p" "8080:8080" \
52+
]
53+
54+
if {$rest_addr != ""} {
55+
lappend docker_opts "-e" "OT_REST_LISTEN_ADDR=$rest_addr"
56+
}
57+
if {$rest_port != ""} {
58+
lappend docker_opts "-e" "OT_REST_LISTEN_PORT=$rest_port"
59+
lappend docker_opts "-p" "$rest_port:$rest_port"
60+
}
61+
62+
lappend docker_opts $::env(EXP_OTBR_DOCKER_IMAGE)
5363

54-
# Now start the simulated RCP binary on the host in a dedicated script restart loop and track its PID
55-
exec bash -c "echo 'while true; do \"\$1\" \"\$2\" <\"\$3\" >\"\$3\"; sleep 1; done' > /tmp/ot_rcp_web_loop.sh"
56-
exec bash -c "bash /tmp/ot_rcp_web_loop.sh \"$sim_app\" \"$sim_id\" \"$pty1\" </dev/null >/dev/null 2>&1 & echo \$! > /tmp/ot_rcp_web_loop.pid"
57-
sleep 5
64+
eval exec $docker_opts
65+
sleep 2
5866
}
5967

6068
set ptys [create_socat 1]
6169
set pty1 [lindex $ptys 0]
6270
set pty2 [lindex $ptys 1]
6371
set container "otbr-test-container-web"
6472

73+
# Start the simulated RCP binary on the host in a dedicated script restart loop keeping PTY open
74+
exec bash -c "exec 3<>$pty1; while true; do \"$::env(EXP_OT_RCP_PATH)\" 2 <&3 >&3; sleep 1; done </dev/null >/tmp/ot_rcp_web.log 2>&1 & echo \$! > /tmp/ot_rcp_web_loop.pid"
75+
sleep 5
76+
77+
# ==========================================
78+
# SCENARIO 1: Verification with Default Values
79+
# ==========================================
80+
send_user -- "--- SCENARIO 1: Testing Web UI and REST API with Default Values ---\n"
6581
start_otbr_docker_web $container $::env(EXP_OT_RCP_PATH) 2 $pty1 $pty2
6682

6783
# Wait for otbr-agent to create the domain socket
@@ -74,7 +90,53 @@ for {set i 0} {$i < 10} {incr i} {
7490
sleep 2
7591
}
7692
if {!$socket_ready} {
77-
send_user "Error: ot-ctl failed to communicate with otbr-agent\n"; exit 1
93+
send_user "Error: ot-ctl failed to communicate with otbr-agent in Scenario 1\n"; exit 1
94+
}
95+
96+
# 1. Verify get_rest_api_info returns default values
97+
send_user "Testing Web UI get_rest_api_info for defaults (127.0.0.1:8081)...\n"
98+
set rest_info [exec curl -s http://127.0.0.1:8080/get_rest_api_info]
99+
send_user "get_rest_api_info response: $rest_info\n"
100+
if {[string first "8081" $rest_info] == -1 || [string first "127.0.0.1" $rest_info] == -1} {
101+
send_user "Error: Default REST API port/address not found in get_rest_api_info response\n"
102+
exit 1
103+
}
104+
send_user "Scenario 1 get_rest_api_info verified successfully!\n"
105+
106+
send_user "Scenario 1 default REST API verified successfully!\n"
107+
108+
# Clean up Scenario 1 container and PTYs
109+
exec docker stop $container
110+
exec docker rm $container
111+
catch {exec bash -c "kill \$(cat /tmp/ot_rcp_web_loop.pid) 2>/dev/null"}
112+
dispose_all
113+
sleep 2
114+
115+
# Re-create socat PTYs and RCP loop for Scenario 2
116+
set ptys [create_socat 1]
117+
set pty1 [lindex $ptys 0]
118+
set pty2 [lindex $ptys 1]
119+
120+
exec bash -c "exec 3<>$pty1; while true; do \"$::env(EXP_OT_RCP_PATH)\" 2 <&3 >&3; sleep 1; done </dev/null >/tmp/ot_rcp_web.log 2>&1 & echo \$! > /tmp/ot_rcp_web_loop.pid"
121+
sleep 5
122+
123+
# ==========================================
124+
# SCENARIO 2: Verification with Custom Values
125+
# ==========================================
126+
send_user -- "--- SCENARIO 2: Testing Web UI and REST API with Custom Values (0.0.0.0:9090) ---\n"
127+
start_otbr_docker_web $container $::env(EXP_OT_RCP_PATH) 2 $pty1 $pty2 "0.0.0.0" "9090"
128+
129+
# Wait for otbr-agent to create the domain socket
130+
set socket_ready false
131+
for {set i 0} {$i < 10} {incr i} {
132+
if {![catch {exec docker exec -i $container ot-ctl state}]} {
133+
set socket_ready true
134+
break
135+
}
136+
sleep 2
137+
}
138+
if {!$socket_ready} {
139+
send_user "Error: ot-ctl failed to communicate with otbr-agent in Scenario 2\n"; exit 1
78140
}
79141

80142
# Stop Thread and down interface initially so we can form network via Web UI
@@ -93,7 +155,7 @@ for {set i 0} {$i < 15} {incr i} {
93155
sleep 2
94156
}
95157
if {!$web_ready} {
96-
send_user "Error: Web UI index.html validation failed\n"
158+
send_user "Error: Web UI index.html validation failed in Scenario 2\n"
97159
exit 1
98160
}
99161
send_user "Web UI index.html validated successfully!\n"
@@ -115,6 +177,26 @@ if {[string first "success" $result] == -1} {
115177
}
116178
send_user "Web UI form_network validated successfully!\n"
117179

180+
# 1. Verify get_rest_api_info returns custom values
181+
send_user "Testing Web UI get_rest_api_info for custom port 9090 and address 0.0.0.0...\n"
182+
set rest_info [exec curl -s http://127.0.0.1:8080/get_rest_api_info]
183+
send_user "get_rest_api_info response: $rest_info\n"
184+
if {[string first "9090" $rest_info] == -1 || [string first "0.0.0.0" $rest_info] == -1} {
185+
send_user "Error: Custom REST API port 9090 or address 0.0.0.0 not found in get_rest_api_info response\n"
186+
exit 1
187+
}
188+
send_user "Scenario 2 get_rest_api_info verified successfully!\n"
189+
190+
# 2. Verify REST API directly from host on custom port 9090
191+
send_user "Testing REST API response from host on custom port 9090...\n"
192+
set rest_node [exec curl -s http://127.0.0.1:9090/api/node]
193+
send_user "api/node response: $rest_node\n"
194+
if {[string first "networkName" $rest_node] == -1} {
195+
send_user "Error: REST API on custom port 9090 failed to respond correctly from host\n"
196+
exit 1
197+
}
198+
send_user "Scenario 2 custom REST API responsiveness verified successfully!\n"
199+
118200
catch {exec bash -c "kill \$(cat /tmp/ot_rcp_web_loop.pid) 2>/dev/null"}
119201

120202
exec docker stop $container

0 commit comments

Comments
 (0)