Line data Source code
1 : // Webthing-CPP
2 : // SPDX-FileCopyrightText: 2023-present Benno Waldhauer
3 : // SPDX-License-Identifier: MIT
4 :
5 : #pragma once
6 :
7 : #include <iostream>
8 : #include <string>
9 : #include <vector>
10 : #include <bw/webthing/mdns.hpp>
11 : #include <bw/webthing/thing.hpp>
12 : #include <bw/webthing/version.hpp>
13 : #include <uwebsockets/App.h>
14 :
15 : namespace bw::webthing {
16 :
17 : typedef uWS::SocketContextOptions SSLOptions;
18 :
19 34 : constexpr bool is_ssl_enabled()
20 : {
21 : #ifdef WT_WITH_SSL
22 : return true;
23 : #else
24 34 : return false;
25 : #endif
26 : }
27 :
28 : #ifdef WT_WITH_SSL
29 : typedef uWS::SSLApp uWebsocketsApp;
30 : #else
31 : typedef uWS::App uWebsocketsApp;
32 : #endif
33 :
34 : typedef uWS::HttpResponse<is_ssl_enabled()> uwsHttpResponse;
35 :
36 :
37 : enum class ThingType { SingleThing, MultipleThings };
38 :
39 : struct ThingContainer
40 : {
41 14 : ThingContainer(std::vector<Thing*> things, std::string name, ThingType type)
42 14 : : things(things)
43 14 : , name(name)
44 14 : , type(type)
45 14 : {}
46 :
47 120 : std::string get_name() const
48 : {
49 120 : return name;
50 : }
51 :
52 135 : ThingType get_type () const
53 : {
54 135 : return type;
55 : }
56 :
57 69 : std::optional<Thing*> get_thing(int index)
58 : {
59 69 : if(index >= things.size())
60 11 : return std::nullopt;
61 :
62 58 : if(type == ThingType::SingleThing)
63 23 : return things[0];
64 :
65 35 : return things[index];
66 : }
67 :
68 62 : std::vector<Thing*> get_things()
69 : {
70 62 : return things;
71 : }
72 :
73 : protected:
74 : std::vector<Thing*> things;
75 : std::string name;
76 : ThingType type;
77 : };
78 :
79 : struct SingleThing : public ThingContainer
80 : {
81 9 : SingleThing(Thing* thing)
82 27 : : ThingContainer({thing}, thing->get_title(), ThingType::SingleThing)
83 9 : {}
84 : };
85 :
86 : struct MultipleThings : public ThingContainer{
87 5 : MultipleThings(std::vector<Thing*> things, std::string name)
88 5 : : ThingContainer(things, name, ThingType::MultipleThings)
89 5 : {}
90 : };
91 :
92 : class WebThingServer
93 : {
94 : public:
95 : struct Builder
96 : {
97 15 : Builder(ThingContainer things)
98 30 : : things_(things)
99 15 : {}
100 :
101 15 : Builder& port(int port)
102 : {
103 15 : port_ = port;
104 15 : return *this;
105 : }
106 :
107 2 : Builder& hostname(std::string hostname)
108 : {
109 2 : hostname_ = hostname;
110 2 : return *this;
111 : }
112 :
113 1 : Builder& base_path(std::string base_path)
114 : {
115 1 : base_path_ = base_path;
116 1 : return *this;
117 : }
118 :
119 2 : Builder& disable_host_validation(bool disable_host_validation)
120 : {
121 2 : disable_host_validation_ = disable_host_validation;
122 2 : return *this;
123 : }
124 :
125 : Builder& ssl_options(SSLOptions options)
126 : {
127 : ssl_options_ = options;
128 : return *this;
129 : }
130 :
131 : Builder& disable_mdns()
132 : {
133 : mdns_enabled_ = false;
134 : return *this;
135 : }
136 :
137 15 : WebThingServer build()
138 : {
139 45 : return WebThingServer(things_, port_, hostname_, base_path_,
140 15 : disable_host_validation_, ssl_options_, mdns_enabled_);
141 : }
142 :
143 : void start()
144 : {
145 : build().start();
146 : }
147 : private:
148 : ThingContainer things_;
149 : int port_ = 80;
150 : std::optional<std::string> hostname_;
151 : /*std::vector<route> additional_routes;*/
152 : SSLOptions ssl_options_;
153 : std::string base_path_ = "/";
154 : bool disable_host_validation_ = false;
155 : bool mdns_enabled_ = true;
156 :
157 : };
158 :
159 : struct Response
160 : {
161 86 : Response(uWS::HttpRequest* req, uwsHttpResponse* res)
162 86 : : req_(req)
163 86 : , res_(res)
164 86 : {}
165 :
166 31 : Response& status(std::string_view status)
167 : {
168 31 : status_ = status;
169 31 : return *this;
170 : }
171 :
172 52 : Response& body(std::string_view body)
173 : {
174 52 : body_ = body;
175 52 : return *this;
176 : }
177 :
178 5 : Response& bad_request()
179 : {
180 5 : return status("400 Bad Request");
181 : }
182 :
183 1 : Response& forbidden()
184 : {
185 1 : return status("403 Forbidden");
186 : }
187 :
188 18 : Response& not_found()
189 : {
190 18 : return status("404 Not Found");
191 : }
192 :
193 2 : Response& method_not_allowed()
194 : {
195 2 : return status("405 Method Not Allowed");
196 : }
197 :
198 1 : Response& moved_permanently()
199 : {
200 1 : return status("301 Moved Permanently");
201 : }
202 :
203 2 : Response& no_content()
204 : {
205 2 : return status("204 No Content");
206 : }
207 :
208 2 : Response& created()
209 : {
210 2 : return status("201 Created");
211 : }
212 :
213 284 : Response& header(std::string_view key, std::string_view value)
214 : {
215 284 : headers_[key] = value;
216 284 : return *this;
217 : }
218 :
219 77 : Response& cors()
220 : {
221 77 : header("Access-Control-Allow-Origin", "*");
222 77 : header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
223 77 : header("Access-Control-Allow-Methods", "GET, HEAD, PUT, POST, DELETE");
224 77 : return *this;
225 : }
226 :
227 51 : Response& json(std::string_view body)
228 : {
229 51 : this->header("Content-Type", "application/json");
230 51 : this->body(body);
231 51 : return *this;
232 : }
233 :
234 1 : Response& html(std::string_view body)
235 : {
236 1 : this->header("Content-Type", "text/html; charset=utf-8");
237 1 : this->body(body);
238 1 : return *this;
239 : }
240 :
241 :
242 77 : void end()
243 : {
244 77 : cors();
245 :
246 77 : res_->writeStatus(status_);
247 361 : for(const auto& kv : headers_)
248 : {
249 284 : res_->writeHeader(kv.first, kv.second);
250 : }
251 77 : res_->end(body_);
252 :
253 77 : if(logger::get_level() == log_level::trace)
254 : {
255 77 : std::ostringstream ss;
256 77 : ss << "http - '" << res_->getRemoteAddressAsText() << "'";
257 77 : ss << " '" << req_->getCaseSensitiveMethod();
258 77 : ss << " " << req_->getFullUrl() << " HTTP/1.1'";
259 77 : ss << " '" << status_ << "'";
260 77 : ss << " " << body_.size() * sizeof(char) << "B";
261 77 : ss << " '" << req_->getHeader("host") << "'";
262 77 : ss << " '" << req_->getHeader("user-agent") << "'";
263 77 : logger::trace(ss.str());
264 77 : }
265 77 : }
266 :
267 : private:
268 : uWS::HttpRequest* req_;
269 : uwsHttpResponse* res_;
270 : std::string_view status_ = uWS::HTTP_200_OK;
271 : std::string_view body_ = {};
272 : std::map<std::string_view, std::string_view> headers_;
273 : };
274 :
275 : public:
276 15 : static Builder host(ThingContainer things)
277 : {
278 15 : return Builder(things);
279 : }
280 :
281 : // disable copy, only allow move
282 : WebThingServer(const WebThingServer& other) = delete;
283 :
284 15 : WebThingServer(ThingContainer things, int port, std::optional<std::string> hostname,
285 : std::string base_path, bool disable_host_validation, SSLOptions ssl_options = {}, bool enable_mdns = true)
286 15 : : things(things)
287 15 : , name(things.get_name())
288 15 : , port(port)
289 15 : , hostname(hostname)
290 15 : , base_path(base_path)
291 15 : , disable_host_validation(disable_host_validation)
292 15 : , ssl_options(ssl_options)
293 15 : , enable_mdns(enable_mdns)
294 : {
295 15 : if(this->base_path.back() == '/')
296 14 : this->base_path.pop_back();
297 :
298 30 : hosts.push_back("localhost");
299 15 : hosts.push_back("localhost:" + std::to_string(port));
300 :
301 15 : auto if_ips = get_addresses();
302 45 : for(auto& ip : if_ips)
303 : {
304 30 : hosts.push_back(ip);
305 30 : hosts.push_back(ip + ":" + std::to_string(port));
306 : }
307 :
308 15 : if(this->hostname)
309 : {
310 2 : auto hn = this->hostname.value();
311 2 : std::transform(hn.begin(), hn.end(), hn.begin(), ::tolower);
312 2 : this->hostname = hn;
313 2 : hosts.push_back(*this->hostname);
314 2 : hosts.push_back(*this->hostname + ":" + std::to_string(port));
315 2 : }
316 :
317 15 : initialize_webthing_routes();
318 15 : }
319 :
320 15 : void initialize_webthing_routes()
321 : {
322 15 : web_server = std::make_unique<uWebsocketsApp>(ssl_options);
323 15 : auto& server = *web_server.get();
324 :
325 15 : bool is_single = things.get_type() == ThingType::SingleThing;
326 :
327 15 : int thing_index = -1;
328 32 : for(auto& thing : things.get_things())
329 : {
330 17 : thing_index++;
331 27 : thing->set_href_prefix(base_path + (is_single ? "" : "/" + std::to_string(thing_index)));
332 17 : thing->add_message_observer([this](auto topic, auto msg)
333 : {
334 29 : handle_thing_message(topic, msg);
335 29 : });
336 15 : }
337 :
338 : #define CREATE_HANDLER(handler_function) [&](auto* res, auto* req) { \
339 : delegate_request(res, req, [&](auto* rs, auto* rq) { handler_function(rs, rq); }); \
340 : }
341 :
342 15 : if(!is_single)
343 : {
344 5 : server.get(base_path, CREATE_HANDLER(handle_things));
345 9 : server.get(base_path + "/", CREATE_HANDLER(handle_things));
346 : }
347 :
348 15 : std::string thing_id_param = is_single ? "" : "/:thing_id";
349 29 : server.get(base_path + thing_id_param, CREATE_HANDLER(handle_thing));
350 32 : server.get(base_path + thing_id_param + "/", CREATE_HANDLER(handle_thing));
351 25 : server.get(base_path + thing_id_param + "/properties", CREATE_HANDLER(handle_properties));
352 23 : server.get(base_path + thing_id_param + "/properties/:property_name", CREATE_HANDLER(handle_property_get));
353 37 : server.put(base_path + thing_id_param + "/properties/:property_name", CREATE_HANDLER(handle_property_put));
354 25 : server.get(base_path + thing_id_param + "/actions", CREATE_HANDLER(handle_actions_get));
355 21 : server.post(base_path + thing_id_param + "/actions", CREATE_HANDLER(handle_actions_post));
356 21 : server.get(base_path + thing_id_param + "/actions/:action_name", CREATE_HANDLER(handle_actions_get));
357 21 : server.post(base_path + thing_id_param + "/actions/:action_name", CREATE_HANDLER(handle_actions_post));
358 23 : server.get(base_path + thing_id_param + "/actions/:action_name/:action_id", CREATE_HANDLER(handle_action_id_get));
359 19 : server.put(base_path + thing_id_param + "/actions/:action_name/:action_id", CREATE_HANDLER(handle_action_id_put));
360 23 : server.del(base_path + thing_id_param + "/actions/:action_name/:action_id", CREATE_HANDLER(handle_action_id_delete));
361 25 : server.get(base_path + thing_id_param + "/events", CREATE_HANDLER(handle_events));
362 25 : server.get(base_path + thing_id_param + "/events/:event_name", CREATE_HANDLER(handle_events));
363 :
364 51 : server.any("/*", CREATE_HANDLER(handle_invalid_requests));
365 47 : server.options("/*", CREATE_HANDLER(handle_options_requests));
366 :
367 32 : for(auto& thing : things.get_things())
368 : {
369 17 : auto thing_id = thing->get_id();
370 17 : uWebsocketsApp::WebSocketBehavior<std::string> ws_behavior;
371 17 : ws_behavior.compression = uWS::SHARED_COMPRESSOR;
372 34 : ws_behavior.open = [thing_id](auto *ws)
373 : {
374 5 : std::string* ws_id = (std::string *) ws->getUserData();
375 5 : ws_id->append(generate_uuid());
376 :
377 5 : logger::trace("websocket open " + *ws_id);
378 5 : ws->subscribe(thing_id + "/properties");
379 5 : ws->subscribe(thing_id + "/actions");
380 22 : };
381 48 : ws_behavior.message = [thing_id, thing](auto *ws, std::string_view message, uWS::OpCode op_code)
382 : {
383 14 : logger::trace("websocket msg " + *((std::string*)ws->getUserData()) + ": " + std::string(message));
384 14 : json j;
385 : try
386 : {
387 15 : j = json::parse(message);
388 : }
389 2 : catch (json::parse_error&)
390 : {
391 15 : json error_message = {{"messageType", "error"}, {"data", {
392 : {"status", "400 Bad Request"},
393 : {"message", "Parsing request failed"}
394 : }}};
395 1 : ws->send(error_message.dump(), op_code);
396 1 : return;
397 1 : }
398 :
399 13 : if(!j.contains("messageType") || !j.contains("data"))
400 : {
401 30 : json error_message = {{"messageType", "error"}, {"data", {
402 : {"status", "400 Bad Request"},
403 : {"message", "Invalid message"}
404 : }}};
405 2 : ws->send(error_message.dump(), op_code);
406 2 : return;
407 2 : }
408 :
409 : // e.g. {"messageType":"addEventSubscription", "data":{"eventName":{}}}
410 11 : std::string message_type = j["messageType"];
411 11 : if(message_type == "addEventSubscription")
412 : {
413 4 : for(auto& evt : j["data"].items())
414 2 : ws->subscribe(thing_id + "/events/" + evt.key());
415 : }
416 9 : else if(message_type == "setProperty")
417 : {
418 14 : for(auto& property_entry : j["data"].items())
419 : {
420 : try
421 : {
422 20 : auto prop_setter = [&](auto val){
423 8 : thing->set_property(property_entry.key(), val);
424 : };
425 :
426 7 : json v = property_entry.value();
427 7 : if(v.is_boolean())
428 1 : prop_setter(v.get<bool>());
429 6 : else if(v.is_string())
430 3 : prop_setter(v.get<std::string>());
431 4 : else if(v.is_number_integer())
432 1 : prop_setter(v.get<int>());
433 3 : else if(v.is_number_float())
434 1 : prop_setter(v.get<double>());
435 : else
436 2 : prop_setter(v);
437 7 : }
438 2 : catch(std::exception& ex)
439 : {
440 15 : json error_message = {{"messageType", "error"}, {"data", {
441 : {"status", "400 Bad Request"},
442 0 : {"message", ex.what()}
443 : }}};
444 1 : ws->send(error_message.dump(), op_code);
445 1 : }
446 : }
447 : }
448 2 : else if(message_type == "requestAction")
449 : {
450 2 : for(auto& action_entry : j["data"].items())
451 : {
452 1 : std::optional<json> input;
453 1 : if(j["data"][action_entry.key()].contains("input"))
454 1 : input = j["data"][action_entry.key()]["input"];
455 :
456 1 : auto action = thing->perform_action(action_entry.key(), std::move(input));
457 1 : if(action)
458 : {
459 1 : std::thread action_runner([action]{
460 1 : action->start();
461 : });
462 1 : action_runner.detach();
463 1 : }
464 : }
465 : }
466 : else
467 : {
468 18 : json error_message = {{"messageType", "error"}, {"data", {
469 : {"status", "400 Bad Request"},
470 : {"message", "Unknown messageType: " + message_type},
471 : {"request", message}
472 : }}};
473 1 : ws->send(error_message.dump(), op_code);
474 1 : }
475 104 : };
476 34 : ws_behavior.close = [thing_id](auto *ws, int /*code*/, std::string_view /*message*/)
477 : {
478 5 : logger::trace("websocket close " + *((std::string*)ws->getUserData()));
479 5 : ws->unsubscribe(thing_id + "/properties");
480 5 : ws->unsubscribe(thing_id + "/actions");
481 5 : ws->unsubscribe(thing_id + "/events/#");
482 22 : };
483 :
484 17 : server.ws<std::string>(thing->get_href(), std::move(ws_behavior));
485 32 : }
486 :
487 15 : server.listen(port, [&](auto *listen_socket) {
488 15 : if (listen_socket) {
489 15 : logger::info("Listening on port " + std::to_string(port));
490 : }
491 15 : });
492 15 : }
493 :
494 15 : void start()
495 : {
496 75 : logger::info("Start WebThingServer v" + std::string(version) + " hosting '" + things.get_name() +
497 60 : "' containing " + std::to_string(things.get_things().size()) + " thing" +
498 30 : std::string(things.get_things().size() == 1 ? "" : "s"));
499 :
500 15 : if(enable_mdns)
501 15 : start_mdns_service();
502 :
503 15 : webserver_loop = uWS::Loop::get();
504 15 : web_server->run();
505 :
506 15 : logger::info("Stopped WebThingServer hosting '" + things.get_name() + "'");
507 15 : }
508 :
509 15 : void stop()
510 : {
511 15 : logger::info("Stop WebThingServer hosting '" + things.get_name() + "'");
512 :
513 15 : if(enable_mdns)
514 15 : stop_mdns_service();
515 :
516 15 : web_server->close();
517 15 : }
518 :
519 9 : std::string get_name() const
520 : {
521 9 : return name;
522 : }
523 :
524 15 : int get_port() const
525 : {
526 15 : return port;
527 : }
528 :
529 15 : std::string get_base_path() const
530 : {
531 15 : return base_path;
532 : }
533 :
534 16 : uWebsocketsApp* get_web_server() const
535 : {
536 16 : return web_server.get();
537 : }
538 :
539 : private:
540 :
541 15 : void start_mdns_service()
542 : {
543 30 : std::thread([this]{
544 :
545 15 : logger::info("Start mDNS service for WebThingServer hosting '" + things.get_name() + "'");
546 :
547 15 : mdns_service = std::make_unique<MdnsService>();
548 45 : mdns_service->start_service(things.get_name(), "_webthing._tcp.local.", port, base_path + "/", is_ssl_enabled());
549 :
550 15 : logger::info("Stopped mDNS service for WebThingServer hosting '" + things.get_name() + "'");
551 :
552 30 : }).detach();
553 15 : }
554 :
555 15 : void stop_mdns_service()
556 : {
557 : using namespace std::chrono;
558 :
559 15 : if(mdns_service)
560 : {
561 15 : logger::info("Stop mDNS service for WebThingServer hosting '" + things.get_name() + "'");
562 15 : mdns_service->stop_service();
563 :
564 15 : auto timeout = milliseconds(5000);
565 15 : auto start = steady_clock::now();
566 15 : bool timeout_reached = false;
567 884 : while(mdns_service->is_running() || timeout_reached)
568 : {
569 869 : std::this_thread::sleep_for(milliseconds(1));
570 869 : auto current = steady_clock::now();
571 869 : timeout_reached = duration_cast<milliseconds>(current - start) >= timeout;
572 : }
573 :
574 : }
575 15 : }
576 :
577 69 : std::optional<Thing*> find_thing_from_url(uWS::HttpRequest* req)
578 : {
579 69 : if(things.get_type() == ThingType::SingleThing)
580 23 : return things.get_thing(0);
581 :
582 46 : auto thing_id_str = req->getParameter(0);
583 : try{
584 46 : int thing_id = std::stoi(thing_id_str.data());
585 46 : return things.get_thing(thing_id);
586 : }
587 0 : catch(std::exception&)
588 : {
589 0 : return std::nullopt;
590 0 : }
591 : }
592 :
593 51 : std::optional<std::string> find_param_after_thing_id_from_url(uWS::HttpRequest* req, int index_after_thing_id = 0)
594 : {
595 51 : int parameter_index = index_after_thing_id;
596 51 : if(things.get_type() == ThingType::MultipleThings)
597 39 : parameter_index += 1;
598 :
599 51 : auto param = req->getParameter(parameter_index);
600 51 : if(param.empty())
601 12 : return std::nullopt;
602 :
603 78 : return std::string(param);
604 : }
605 :
606 15 : std::optional<std::string> find_property_name_from_url(uWS::HttpRequest* req)
607 : {
608 15 : return find_param_after_thing_id_from_url(req);
609 : }
610 :
611 8 : std::optional<std::string> find_event_name_from_url(uWS::HttpRequest* req)
612 : {
613 8 : return find_param_after_thing_id_from_url(req);
614 : }
615 :
616 20 : std::optional<std::string> find_action_name_from_url(uWS::HttpRequest* req)
617 : {
618 20 : return find_param_after_thing_id_from_url(req, 0);
619 : }
620 :
621 8 : std::optional<std::string> find_action_id_from_url(uWS::HttpRequest* req)
622 : {
623 8 : return find_param_after_thing_id_from_url(req, 1);
624 : }
625 :
626 76 : bool validate_host(uWS::HttpRequest* req)
627 : {
628 76 : if(disable_host_validation)
629 2 : return true;
630 :
631 74 : std::string host(req->getHeader("host"));
632 74 : return std::find(hosts.begin(), hosts.end(), host) != hosts.end();
633 74 : }
634 :
635 76 : void delegate_request(uwsHttpResponse* res, uWS::HttpRequest* req,
636 : std::function<void(uwsHttpResponse*, uWS::HttpRequest*)> handler)
637 : {
638 : // default aborted handling
639 76 : if(logger::get_level() == log_level::trace)
640 : {
641 76 : std::ostringstream ss;
642 76 : ss << "http - '" << res->getRemoteAddressAsText() << "'";
643 76 : ss << " '" << req->getCaseSensitiveMethod();
644 76 : ss << " " << req->getFullUrl() << " HTTP/1.1'";
645 76 : ss << " 'ABORTED'";
646 76 : ss << " '" << req->getHeader("host") << "'";
647 76 : ss << " '" << req->getHeader("user-agent") << "'";
648 76 : res->onAborted([str = std::move(ss.str())](){
649 0 : logger::trace(str);
650 0 : });
651 76 : }
652 : else
653 : {
654 0 : res->onAborted([](){/*do nothing*/});
655 : }
656 :
657 : // pre filter
658 76 : if(!validate_host(req))
659 : {
660 1 : Response response(req, res);
661 1 : response.forbidden().end();
662 1 : return;
663 1 : }
664 :
665 : // execute callback
666 75 : handler(res,req);
667 : }
668 :
669 3 : void handle_invalid_requests(uwsHttpResponse* res, uWS::HttpRequest* req)
670 : {
671 3 : Response response(req, res);
672 :
673 3 : auto host = req->getHeader("host");
674 3 : auto path = req->getUrl();
675 :
676 3 : if(path.back() == '/' && path != "/" && path != (base_path + "/"))
677 : {
678 : // redirect to non-trailing slash url
679 3 : auto location = (is_ssl_enabled() ? "https://" : "http://") + std::string(host) + std::string(path.data(), path.size()-1);
680 1 : response.header("Location", location.c_str());
681 1 : response.moved_permanently().end();
682 1 : return;
683 1 : }
684 :
685 2 : response.method_not_allowed().end();
686 3 : }
687 :
688 1 : void handle_options_requests(uwsHttpResponse* res, uWS::HttpRequest* req)
689 : {
690 1 : Response response(req, res);
691 1 : response.no_content().end();
692 1 : }
693 :
694 18 : json prepare_thing_description(Thing* thing, uWS::HttpRequest* req)
695 : {
696 36 : std::string http_protocol = is_ssl_enabled() ? "https" : "http";
697 36 : std::string ws_protocol = http_protocol == "https" ? "wss" : "ws";
698 18 : std::string host = std::string(req->getHeader("host"));
699 18 : std::string ws_href = ws_protocol + "://" + host;
700 :
701 18 : json desc = thing->as_thing_description();
702 18 : desc["href"] = thing->get_href();
703 180 : desc["links"].push_back({{"rel", "alternate"}, {"href", ws_href + thing->get_href()}});
704 18 : desc["base"] = http_protocol + "://" + host + thing->get_href();
705 162 : desc["securityDefinitions"] = {{"nosec_sc", {{"scheme", "nosec"}}}};
706 18 : desc["security"] = "nosec_sc";
707 :
708 36 : return desc;
709 324 : }
710 :
711 2 : void handle_things(uwsHttpResponse* res, uWS::HttpRequest* req)
712 : {
713 2 : Response response(req, res);
714 :
715 2 : json descriptions = json::array();
716 :
717 6 : for(auto thing : things.get_things())
718 : {
719 4 : json desc = prepare_thing_description(thing, req);
720 4 : descriptions.push_back(desc);
721 6 : }
722 :
723 2 : response.json(descriptions.dump()).end();
724 2 : }
725 :
726 15 : void handle_thing(uwsHttpResponse* res, uWS::HttpRequest* req)
727 : {
728 15 : Response response(req, res);
729 :
730 15 : auto thing = find_thing_from_url(req);
731 15 : if(!thing)
732 : {
733 1 : response.not_found().end();
734 1 : return;
735 : }
736 :
737 14 : json description = prepare_thing_description(*thing, req);
738 :
739 14 : response.json(description.dump()).end();
740 15 : }
741 :
742 5 : void handle_properties(uwsHttpResponse* res, uWS::HttpRequest* req)
743 : {
744 5 : Response response(req, res);
745 :
746 5 : auto thing = find_thing_from_url(req);
747 5 : if(!thing)
748 : {
749 1 : response.not_found().end();
750 1 : return;
751 : }
752 :
753 4 : response.json((*thing)->get_properties().dump()).end();
754 5 : }
755 :
756 4 : void handle_property_get(uwsHttpResponse* res, uWS::HttpRequest* req)
757 : {
758 4 : Response response(req, res);
759 :
760 4 : auto thing = find_thing_from_url(req);
761 4 : auto property_name = find_property_name_from_url(req);
762 :
763 4 : if(!thing || !property_name)
764 : {
765 1 : response.not_found().end();
766 1 : return;
767 : }
768 :
769 3 : auto property = (*thing)->find_property(*property_name);
770 :
771 3 : if(!property)
772 : {
773 2 : response.not_found().end();
774 2 : return;
775 : }
776 :
777 1 : response.json(property->get_property_value_object().dump()).end();
778 9 : }
779 :
780 11 : void handle_property_put(uwsHttpResponse* res, uWS::HttpRequest* req)
781 : {
782 11 : Response response(req, res);
783 :
784 11 : auto thing = find_thing_from_url(req);
785 11 : auto property_name_in_url = find_property_name_from_url(req);
786 :
787 11 : if(!thing || !property_name_in_url)
788 : {
789 1 : response.not_found().end();
790 1 : return;
791 : }
792 :
793 10 : auto property = (*thing)->find_property(*property_name_in_url);
794 :
795 10 : if(!property)
796 : {
797 1 : response.not_found().end();
798 1 : return;
799 : }
800 :
801 9 : res->onData([res, req, thing, property_name_in_url, property](std::string_view body_chunk, bool is_last)
802 : {
803 9 : if(is_last)
804 : {
805 9 : Response response(req, res);
806 :
807 : try
808 : {
809 9 : if(body_chunk.empty())
810 3 : throw PropertyError("Empty property request body");
811 :
812 8 : std::string prop_name = *property_name_in_url;
813 8 : json body = json::parse(body_chunk);
814 :
815 8 : if(!body.contains(prop_name))
816 1 : throw PropertyError("Property request body does not contain " + prop_name);
817 :
818 7 : auto v = body[prop_name];
819 7 : auto prop_setter = [&](auto val){
820 7 : (*thing)->set_property(prop_name, val);
821 14 : };
822 :
823 7 : if(v.is_boolean())
824 1 : prop_setter(v.get<bool>());
825 6 : else if(v.is_string())
826 1 : prop_setter(v.get<std::string>());
827 5 : else if(v.is_number_integer())
828 2 : prop_setter(v.get<int>());
829 3 : else if(v.is_number_float())
830 1 : prop_setter(v.get<double>());
831 : else
832 2 : prop_setter(v);
833 :
834 7 : response.json(property->get_property_value_object().dump()).end();
835 9 : }
836 2 : catch(std::exception& ex)
837 : {
838 8 : json body = {{"message", ex.what()}};
839 2 : response.bad_request().json(body.dump()).end();
840 2 : }
841 9 : }
842 17 : });
843 :
844 9 : res->onAborted([]{
845 0 : logger::debug("transfer request body aborted");
846 0 : });
847 14 : }
848 :
849 : // Handles GET requests to:
850 : // * /actions
851 : // * /actions/<action_name>
852 8 : void handle_actions_get(uwsHttpResponse* res, uWS::HttpRequest* req)
853 : {
854 8 : Response response(req, res);
855 :
856 8 : auto thing = find_thing_from_url(req);
857 8 : if(!thing)
858 : {
859 1 : response.not_found().end();
860 1 : return;
861 : }
862 :
863 : // can be std::nullopt which results in a collection of all actions
864 7 : auto action_name = find_action_name_from_url(req);
865 7 : response.json((*thing)->get_action_descriptions(action_name).dump()).end();
866 8 : }
867 :
868 :
869 : // Handles POST requests to:
870 : // * /actions
871 : // * /actions/<action_name>
872 6 : void handle_actions_post(uwsHttpResponse* res, uWS::HttpRequest* req)
873 : {
874 :
875 6 : auto thing = find_thing_from_url(req);
876 6 : if(!thing)
877 : {
878 1 : Response(req, res).not_found().end();
879 1 : return;
880 : }
881 :
882 5 : auto action_name_in_url = find_action_name_from_url(req);
883 :
884 5 : res->onData([res, req, thing, action_name_in_url](std::string_view body_chunk, bool is_last)
885 : {
886 5 : if(is_last)
887 : {
888 5 : Response response(req, res);
889 :
890 : try
891 : {
892 5 : if(body_chunk.empty())
893 3 : throw ActionError("Empty action request body");
894 :
895 4 : json body = json::parse(body_chunk);
896 10 : if(!body.is_object() || body.size() != 1 ||
897 6 : (action_name_in_url && !body.contains(action_name_in_url)))
898 3 : throw ActionError("Invalid action request body");
899 :
900 3 : std::string action_name = action_name_in_url.value_or(body.begin().key());
901 3 : json action_params = body[action_name];
902 :
903 3 : std::optional<json> input;
904 3 : if(action_params.contains("input"))
905 3 : input = action_params["input"];
906 :
907 3 : auto action = (*thing)->perform_action(action_name, std::move(input));
908 3 : if(!action)
909 3 : throw ActionError("Could not perform action");
910 :
911 2 : json response_body = action->as_action_description();
912 4 : std::thread action_runner([action]{
913 2 : action->start();
914 2 : });
915 2 : action_runner.detach();
916 :
917 2 : response.created().json(response_body.dump()).end();
918 8 : }
919 3 : catch(std::exception& ex)
920 : {
921 12 : json body = {{"message", ex.what()}};
922 3 : response.bad_request().json(body.dump()).end();
923 3 : }
924 5 : }
925 17 : });
926 :
927 5 : res->onAborted([]{
928 0 : logger::debug("transfer request body aborted");
929 0 : });
930 :
931 5 : }
932 :
933 4 : void handle_action_id_get(uwsHttpResponse* res, uWS::HttpRequest* req)
934 : {
935 4 : Response response(req, res);
936 :
937 4 : auto thing = find_thing_from_url(req);
938 4 : auto action_name = find_action_name_from_url(req);
939 4 : auto action_id = find_action_id_from_url(req);
940 :
941 4 : if(!thing || !action_name || !action_id)
942 : {
943 1 : response.not_found().end();
944 1 : return;
945 : }
946 :
947 3 : auto action = (*thing)->get_action(*action_name, *action_id);
948 3 : if(!action)
949 : {
950 2 : response.not_found().end();
951 2 : return;
952 : }
953 :
954 1 : response.json(action->as_action_description().dump()).end();
955 12 : }
956 :
957 : // TODO: this is not yet defined in the spec
958 : // also cf. https://webthings.io/api/#actionrequest-resource
959 2 : void handle_action_id_put(uwsHttpResponse* res, uWS::HttpRequest* req)
960 : {
961 2 : Response response(req, res);
962 :
963 2 : auto thing = find_thing_from_url(req);
964 2 : if(!thing)
965 : {
966 1 : response.not_found().end();
967 1 : return;
968 : }
969 :
970 1 : response.end();
971 2 : }
972 :
973 4 : void handle_action_id_delete(uwsHttpResponse* res, uWS::HttpRequest* req)
974 : {
975 4 : Response response(req, res);
976 :
977 4 : auto thing = find_thing_from_url(req);
978 4 : auto action_name = find_action_name_from_url(req);
979 4 : auto action_id = find_action_id_from_url(req);
980 :
981 4 : if(!thing || !action_name || !action_id)
982 : {
983 1 : response.not_found().end();
984 1 : return;
985 : }
986 :
987 3 : auto action = (*thing)->get_action(*action_name, *action_id);
988 3 : if(!action)
989 : {
990 2 : response.not_found().end();
991 2 : return;
992 : }
993 :
994 1 : if(!(*thing)->remove_action(*action_name, *action_id))
995 : {
996 0 : response.not_found().end();
997 0 : return;
998 : }
999 :
1000 1 : response.no_content().end();
1001 12 : }
1002 :
1003 : // Handles requests to:
1004 : // * /events
1005 : // * /events/<event_name>
1006 10 : void handle_events(uwsHttpResponse* res, uWS::HttpRequest* req)
1007 : {
1008 10 : Response response(req, res);
1009 :
1010 10 : auto thing = find_thing_from_url(req);
1011 10 : if(!thing)
1012 : {
1013 2 : response.not_found().end();
1014 2 : return;
1015 : }
1016 :
1017 : // can be std::nullopt which results in a collection of all events
1018 8 : auto event_name = find_event_name_from_url(req);
1019 8 : response.json((*thing)->get_event_descriptions(event_name).dump()).end();
1020 10 : }
1021 :
1022 : // forward thing messages to servers websocket clients
1023 29 : void handle_thing_message(const std::string& topic, const json& message)
1024 : {
1025 29 : if(!webserver_loop)
1026 0 : return;
1027 :
1028 29 : std::string t = topic;
1029 29 : std::string m = message.dump();
1030 :
1031 29 : webserver_loop->defer([this, t, m]{
1032 29 : logger::trace("server broadcast : " + t + " : " + m);
1033 29 : web_server->publish(t, m, uWS::OpCode::TEXT);
1034 29 : });
1035 29 : }
1036 :
1037 : ThingContainer things;
1038 : int port;
1039 : std::string name;
1040 : std::optional<std::string> hostname;
1041 : /*std::vector<route> additional_routes;*/
1042 : SSLOptions ssl_options;
1043 : std::string base_path = "/";
1044 : bool disable_host_validation = false;
1045 : bool enable_mdns = true;
1046 :
1047 : std::vector<std::string> hosts;
1048 :
1049 : uWS::Loop* webserver_loop; // Must be initialized from thread that calls start()
1050 : std::unique_ptr<uWebsocketsApp> web_server;
1051 : std::unique_ptr<MdnsService> mdns_service;
1052 : };
1053 :
1054 : } // bw::webthing
|