Skip to content

Commit 7a63652

Browse files
committed
feat: add configurable CORS support for REST API
Add CORS configuration via ROS 2 parameters to allow browser-based clients (web UIs) to call the Gateway REST API without code changes. Configuration options (in gateway_params.yaml): - cors.allowed_origins: list of allowed origins (empty = allow all) - cors.allowed_methods: HTTP methods to allow - cors.allowed_headers: headers to allow in requests - cors.allow_credentials: enable credentials support - cors.max_age_seconds: preflight cache duration CORS is disabled when no configuration is provided. Handles preflight OPTIONS requests with 204 No Content response.
1 parent 9984654 commit 7a63652

File tree

6 files changed

+193
-5
lines changed

6 files changed

+193
-5
lines changed

src/ros2_medkit_gateway/config/gateway_params.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,32 @@ ros2_medkit_gateway:
2525
# Valid range: 100-60000 (0.1s to 60s)
2626
refresh_interval_ms: 2000
2727

28+
# CORS Configuration
29+
# Cross-Origin Resource Sharing settings for browser-based clients
30+
# If cors section is missing or empty, CORS handling is disabled
31+
cors:
32+
# List of allowed origins
33+
# Examples: ["http://localhost:5173", "https://example.com"]
34+
# Use ["*"] to allow all origins (not recommended for production)
35+
# Empty list with other settings = allow all origins
36+
allowed_origins: []
37+
38+
# Allowed HTTP methods for CORS requests
39+
# Default: ["GET", "OPTIONS"]
40+
allowed_methods: ["GET", "OPTIONS"]
41+
42+
# Allowed headers in CORS requests
43+
# Use ["*"] to allow all headers
44+
allowed_headers: ["Content-Type", "Accept"]
45+
46+
# Whether to allow credentials (cookies, authorization headers)
47+
# When true, allowed_origins cannot be ["*"]
48+
allow_credentials: false
49+
50+
# How long (in seconds) browsers should cache preflight response
51+
# Default: 86400 (24 hours)
52+
max_age_seconds: 86400
53+
2854
# Data Access Configuration
2955
# Maximum number of concurrent topic samples when fetching component data
3056
# Higher values improve response time but use more system resources
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2025 bburda
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#pragma once
16+
17+
#include <string>
18+
#include <vector>
19+
20+
namespace ros2_medkit_gateway {
21+
22+
/**
23+
* @brief CORS (Cross-Origin Resource Sharing) configuration settings
24+
*/
25+
struct CorsConfig {
26+
bool enabled{false};
27+
std::vector<std::string> allowed_origins;
28+
std::vector<std::string> allowed_methods;
29+
std::vector<std::string> allowed_headers;
30+
bool allow_credentials{false};
31+
int max_age_seconds{86400};
32+
};
33+
34+
} // namespace ros2_medkit_gateway

src/ros2_medkit_gateway/include/ros2_medkit_gateway/gateway_node.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
#include <mutex>
2121
#include <string>
2222
#include <thread>
23+
#include <vector>
2324

2425
#include <rclcpp/rclcpp.hpp>
2526

27+
#include "ros2_medkit_gateway/config.hpp"
2628
#include "ros2_medkit_gateway/models.hpp"
2729
#include "ros2_medkit_gateway/discovery_manager.hpp"
2830
#include "ros2_medkit_gateway/data_access_manager.hpp"
@@ -55,6 +57,7 @@ class GatewayNode : public rclcpp::Node {
5557
std::string server_host_;
5658
int server_port_;
5759
int refresh_interval_ms_;
60+
CorsConfig cors_config_;
5861

5962
// Managers
6063
std::unique_ptr<DiscoveryManager> discovery_mgr_;

src/ros2_medkit_gateway/include/ros2_medkit_gateway/rest_server.hpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,23 @@
1919
#include <expected>
2020
#include <memory>
2121
#include <string>
22+
#include <vector>
2223

2324
#include <nlohmann/json.hpp>
2425

26+
#include "ros2_medkit_gateway/config.hpp"
27+
2528
namespace ros2_medkit_gateway {
2629

2730
class GatewayNode;
2831

2932
class RESTServer {
3033
public:
31-
RESTServer(GatewayNode* node, const std::string& host, int port);
34+
RESTServer(
35+
GatewayNode* node,
36+
const std::string& host,
37+
int port,
38+
const CorsConfig& cors_config = {});
3239
~RESTServer();
3340

3441
void start();
@@ -49,10 +56,13 @@ class RESTServer {
4956

5057
// Helper methods
5158
std::expected<void, std::string> validate_entity_id(const std::string& entity_id) const;
59+
void set_cors_headers(httplib::Response& res, const std::string& origin) const;
60+
bool is_origin_allowed(const std::string& origin) const;
5261

5362
GatewayNode* node_;
5463
std::string host_;
5564
int port_;
65+
CorsConfig cors_config_;
5666
std::unique_ptr<httplib::Server> server_;
5767
};
5868

src/ros2_medkit_gateway/src/gateway_node.cpp

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,30 @@ GatewayNode::GatewayNode()
2828
declare_parameter("server.host", "127.0.0.1");
2929
declare_parameter("server.port", 8080);
3030
declare_parameter("refresh_interval_ms", 2000);
31+
declare_parameter("cors.allowed_origins", std::vector<std::string>{});
32+
declare_parameter("cors.allowed_methods", std::vector<std::string>{"GET", "OPTIONS"});
33+
declare_parameter("cors.allowed_headers", std::vector<std::string>{"Content-Type", "Accept"});
34+
declare_parameter("cors.allow_credentials", false);
35+
declare_parameter("cors.max_age_seconds", 86400);
3136

3237
// Get parameter values
3338
server_host_ = get_parameter("server.host").as_string();
3439
server_port_ = get_parameter("server.port").as_int();
3540
refresh_interval_ms_ = get_parameter("refresh_interval_ms").as_int();
3641

42+
// Get CORS configuration
43+
cors_config_.allowed_origins = get_parameter("cors.allowed_origins").as_string_array();
44+
cors_config_.allowed_methods = get_parameter("cors.allowed_methods").as_string_array();
45+
cors_config_.allowed_headers = get_parameter("cors.allowed_headers").as_string_array();
46+
cors_config_.allow_credentials = get_parameter("cors.allow_credentials").as_bool();
47+
cors_config_.max_age_seconds = get_parameter("cors.max_age_seconds").as_int();
48+
49+
// CORS is enabled if any origins are configured or if methods/headers are non-default
50+
// Treat missing/empty config as disabled for security
51+
cors_config_.enabled = !cors_config_.allowed_origins.empty() ||
52+
!cors_config_.allowed_methods.empty() ||
53+
!cors_config_.allowed_headers.empty();
54+
3755
// Validate port range
3856
if (server_port_ < 1024 || server_port_ > 65535) {
3957
RCLCPP_ERROR(
@@ -77,6 +95,32 @@ GatewayNode::GatewayNode()
7795
refresh_interval_ms_
7896
);
7997

98+
if (cors_config_.enabled) {
99+
std::string origins_str;
100+
for (const auto& origin : cors_config_.allowed_origins) {
101+
if (!origins_str.empty()) origins_str += ", ";
102+
origins_str += origin;
103+
}
104+
if (origins_str.empty()) origins_str = "* (all)";
105+
106+
std::string methods_str;
107+
for (const auto& method : cors_config_.allowed_methods) {
108+
if (!methods_str.empty()) methods_str += ", ";
109+
methods_str += method;
110+
}
111+
112+
RCLCPP_INFO(
113+
get_logger(),
114+
"CORS enabled - origins: [%s], methods: [%s], credentials: %s, max_age: %ds",
115+
origins_str.c_str(),
116+
methods_str.c_str(),
117+
cors_config_.allow_credentials ? "true" : "false",
118+
cors_config_.max_age_seconds
119+
);
120+
} else {
121+
RCLCPP_INFO(get_logger(), "CORS: disabled (no configuration provided)");
122+
}
123+
80124
// Initialize managers
81125
discovery_mgr_ = std::make_unique<DiscoveryManager>(this);
82126
data_access_mgr_ = std::make_unique<DataAccessManager>(this);
@@ -90,8 +134,9 @@ GatewayNode::GatewayNode()
90134
std::bind(&GatewayNode::refresh_cache, this)
91135
);
92136

93-
// Start REST server with configured host and port
94-
rest_server_ = std::make_unique<RESTServer>(this, server_host_, server_port_);
137+
// Start REST server with configured host, port and CORS
138+
rest_server_ = std::make_unique<RESTServer>(
139+
this, server_host_, server_port_, cors_config_);
95140
start_rest_server();
96141

97142
RCLCPP_INFO(

src/ros2_medkit_gateway/src/rest_server.cpp

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,33 @@ using httplib::StatusCode;
2727

2828
namespace ros2_medkit_gateway {
2929

30-
RESTServer::RESTServer(GatewayNode* node, const std::string& host, int port)
31-
: node_(node), host_(host), port_(port)
30+
RESTServer::RESTServer(
31+
GatewayNode* node,
32+
const std::string& host,
33+
int port,
34+
const CorsConfig& cors_config)
35+
: node_(node), host_(host), port_(port), cors_config_(cors_config)
3236
{
3337
server_ = std::make_unique<httplib::Server>();
38+
39+
// Set up pre-routing handler for CORS (only if enabled)
40+
if (cors_config_.enabled) {
41+
server_->set_pre_routing_handler(
42+
[this](const httplib::Request& req, httplib::Response& res) {
43+
std::string origin = req.get_header_value("Origin");
44+
if (!origin.empty() && is_origin_allowed(origin)) {
45+
set_cors_headers(res, origin);
46+
}
47+
48+
// Handle preflight OPTIONS requests
49+
if (req.method == "OPTIONS") {
50+
res.status = 204; // No Content
51+
return httplib::Server::HandlerResponse::Handled;
52+
}
53+
return httplib::Server::HandlerResponse::Unhandled;
54+
});
55+
}
56+
3457
setup_routes();
3558
}
3659

@@ -546,4 +569,51 @@ void RESTServer::handle_component_topic_data(const httplib::Request& req, httpli
546569
}
547570
}
548571

572+
void RESTServer::set_cors_headers(httplib::Response& res, const std::string& origin) const {
573+
res.set_header("Access-Control-Allow-Origin", origin);
574+
575+
// Build methods string from config
576+
std::string methods_str;
577+
for (const auto& method : cors_config_.allowed_methods) {
578+
if (!methods_str.empty()) methods_str += ", ";
579+
methods_str += method;
580+
}
581+
if (!methods_str.empty()) {
582+
res.set_header("Access-Control-Allow-Methods", methods_str);
583+
}
584+
585+
// Build headers string from config
586+
std::string headers_str;
587+
for (const auto& header : cors_config_.allowed_headers) {
588+
if (!headers_str.empty()) headers_str += ", ";
589+
headers_str += header;
590+
}
591+
if (!headers_str.empty()) {
592+
res.set_header("Access-Control-Allow-Headers", headers_str);
593+
}
594+
595+
// Set credentials header if enabled
596+
if (cors_config_.allow_credentials) {
597+
res.set_header("Access-Control-Allow-Credentials", "true");
598+
}
599+
600+
// Set max age
601+
res.set_header("Access-Control-Max-Age", std::to_string(cors_config_.max_age_seconds));
602+
}
603+
604+
bool RESTServer::is_origin_allowed(const std::string& origin) const {
605+
// If no origins configured, allow all (for development)
606+
if (cors_config_.allowed_origins.empty()) {
607+
return true;
608+
}
609+
610+
// Check if origin matches any allowed origin
611+
for (const auto& allowed : cors_config_.allowed_origins) {
612+
if (allowed == "*" || allowed == origin) {
613+
return true;
614+
}
615+
}
616+
return false;
617+
}
618+
549619
} // namespace ros2_medkit_gateway

0 commit comments

Comments
 (0)