1313// limitations under the License.
1414
1515#include " ros2_medkit_gateway/rest_server.hpp"
16- #include " ros2_medkit_gateway/gateway_node.hpp"
16+ #include < iomanip>
17+ #include < sstream>
1718#include < rclcpp/rclcpp.hpp>
19+ #include " ros2_medkit_gateway/gateway_node.hpp"
1820
1921using json = nlohmann::json;
22+ using httplib::StatusCode;
2023
2124namespace ros2_medkit_gateway {
2225
@@ -81,6 +84,50 @@ void RESTServer::stop() {
8184 }
8285}
8386
87+ std::expected<void , std::string> RESTServer::validate_entity_id (
88+ const std::string& entity_id
89+ ) const {
90+ // Check for empty string
91+ if (entity_id.empty ()) {
92+ return std::unexpected (" Entity ID cannot be empty" );
93+ }
94+
95+ // Check length (reasonable limit to prevent abuse)
96+ if (entity_id.length () > 256 ) {
97+ return std::unexpected (" Entity ID too long (max 256 characters)" );
98+ }
99+
100+ // Validate characters according to ROS 2 naming conventions
101+ // Allow: alphanumeric (a-z, A-Z, 0-9), underscore (_)
102+ // Reject: hyphen (not allowed in ROS 2 names), forward slash (conflicts with URL routing),
103+ // special characters, escape sequences
104+ for (char c : entity_id) {
105+ bool is_alphanumeric = (c >= ' a' && c <= ' z' ) ||
106+ (c >= ' A' && c <= ' Z' ) ||
107+ (c >= ' 0' && c <= ' 9' );
108+ bool is_allowed_special = (c == ' _' );
109+
110+ if (!is_alphanumeric && !is_allowed_special) {
111+ // For non-printable characters, show the character code
112+ std::string char_repr;
113+ if (c < 32 || c > 126 ) {
114+ std::ostringstream oss;
115+ oss << " 0x" << std::hex << std::setfill (' 0' ) << std::setw (2 )
116+ << static_cast <unsigned int >(static_cast <unsigned char >(c));
117+ char_repr = oss.str ();
118+ } else {
119+ char_repr = std::string (1 , c);
120+ }
121+ return std::unexpected (
122+ " Entity ID contains invalid character: '" + char_repr +
123+ " '. Only alphanumeric and underscore are allowed"
124+ );
125+ }
126+ }
127+
128+ return {};
129+ }
130+
84131void RESTServer::handle_health (const httplib::Request& req, httplib::Response& res) {
85132 (void )req; // Unused parameter
86133
@@ -92,7 +139,7 @@ void RESTServer::handle_health(const httplib::Request& req, httplib::Response& r
92139
93140 res.set_content (response.dump (2 ), " application/json" );
94141 } catch (const std::exception& e) {
95- res.status = 500 ;
142+ res.status = StatusCode::InternalServerError_500 ;
96143 res.set_content (
97144 json{{" error" , " Internal server error" }}.dump (),
98145 " application/json"
@@ -113,7 +160,7 @@ void RESTServer::handle_root(const httplib::Request& req, httplib::Response& res
113160
114161 res.set_content (response.dump (2 ), " application/json" );
115162 } catch (const std::exception& e) {
116- res.status = 500 ;
163+ res.status = StatusCode::InternalServerError_500 ;
117164 res.set_content (
118165 json{{" error" , " Internal server error" }}.dump (),
119166 " application/json"
@@ -135,7 +182,7 @@ void RESTServer::handle_list_areas(const httplib::Request& req, httplib::Respons
135182
136183 res.set_content (areas_json.dump (2 ), " application/json" );
137184 } catch (const std::exception& e) {
138- res.status = 500 ;
185+ res.status = StatusCode::InternalServerError_500 ;
139186 res.set_content (
140187 json{{" error" , " Internal server error" }}.dump (),
141188 " application/json"
@@ -157,7 +204,7 @@ void RESTServer::handle_list_components(const httplib::Request& req, httplib::Re
157204
158205 res.set_content (components_json.dump (2 ), " application/json" );
159206 } catch (const std::exception& e) {
160- res.status = 500 ;
207+ res.status = StatusCode::InternalServerError_500 ;
161208 res.set_content (
162209 json{{" error" , " Internal server error" }}.dump (),
163210 " application/json"
@@ -174,7 +221,7 @@ void RESTServer::handle_area_components(const httplib::Request& req, httplib::Re
174221 try {
175222 // Extract area_id from URL path
176223 if (req.matches .size () < 2 ) {
177- res.status = 400 ;
224+ res.status = StatusCode::BadRequest_400 ;
178225 res.set_content (
179226 json{{" error" , " Invalid request" }}.dump (2 ),
180227 " application/json"
@@ -183,6 +230,22 @@ void RESTServer::handle_area_components(const httplib::Request& req, httplib::Re
183230 }
184231
185232 std::string area_id = req.matches [1 ];
233+
234+ // Validate area_id
235+ auto validation_result = validate_entity_id (area_id);
236+ if (!validation_result) {
237+ res.status = StatusCode::BadRequest_400;
238+ res.set_content (
239+ json{
240+ {" error" , " Invalid area ID" },
241+ {" details" , validation_result.error ()},
242+ {" area_id" , area_id}
243+ }.dump (2 ),
244+ " application/json"
245+ );
246+ return ;
247+ }
248+
186249 const auto cache = node_->get_entity_cache ();
187250
188251 // Check if area exists
@@ -195,7 +258,7 @@ void RESTServer::handle_area_components(const httplib::Request& req, httplib::Re
195258 }
196259
197260 if (!area_exists) {
198- res.status = 404 ;
261+ res.status = StatusCode::NotFound_404 ;
199262 res.set_content (
200263 json{
201264 {" error" , " Area not found" },
@@ -216,7 +279,7 @@ void RESTServer::handle_area_components(const httplib::Request& req, httplib::Re
216279
217280 res.set_content (components_json.dump (2 ), " application/json" );
218281 } catch (const std::exception& e) {
219- res.status = 500 ;
282+ res.status = StatusCode::InternalServerError_500 ;
220283 res.set_content (
221284 json{{" error" , " Internal server error" }}.dump (),
222285 " application/json"
@@ -234,7 +297,7 @@ void RESTServer::handle_component_data(const httplib::Request& req, httplib::Res
234297 try {
235298 // Extract component_id from URL path
236299 if (req.matches .size () < 2 ) {
237- res.status = 400 ;
300+ res.status = StatusCode::BadRequest_400 ;
238301 res.set_content (
239302 json{{" error" , " Invalid request" }}.dump (2 ),
240303 " application/json"
@@ -243,9 +306,22 @@ void RESTServer::handle_component_data(const httplib::Request& req, httplib::Res
243306 }
244307
245308 component_id = req.matches [1 ];
246- // TODO(mfaferek93): Add input validation for component_id
247- // Should validate against ROS 2 naming conventions (alphanumeric, /, _)
248- // and URL-decode if necessary
309+
310+ // Validate component_id
311+ auto validation_result = validate_entity_id (component_id);
312+ if (!validation_result) {
313+ res.status = StatusCode::BadRequest_400;
314+ res.set_content (
315+ json{
316+ {" error" , " Invalid component ID" },
317+ {" details" , validation_result.error ()},
318+ {" component_id" , component_id}
319+ }.dump (2 ),
320+ " application/json"
321+ );
322+ return ;
323+ }
324+
249325 const auto cache = node_->get_entity_cache ();
250326
251327 // Find component in cache
@@ -261,7 +337,7 @@ void RESTServer::handle_component_data(const httplib::Request& req, httplib::Res
261337 }
262338
263339 if (!component_found) {
264- res.status = 404 ;
340+ res.status = StatusCode::NotFound_404 ;
265341 res.set_content (
266342 json{
267343 {" error" , " Component not found" },
@@ -278,7 +354,7 @@ void RESTServer::handle_component_data(const httplib::Request& req, httplib::Res
278354
279355 res.set_content (component_data.dump (2 ), " application/json" );
280356 } catch (const std::exception& e) {
281- res.status = 500 ;
357+ res.status = StatusCode::InternalServerError_500 ;
282358 res.set_content (
283359 json{
284360 {" error" , " Failed to retrieve component data" },
0 commit comments