Webthing-CPP: a modern CPP implementation of the WebThings API


LCOV - code coverage report
Current view: top level - webthing - server.hpp (source / functions) Coverage Total Hit
Test: filtered_coverage.info Lines: 97.5 % 553 539
Test Date: 2025-03-15 12:45:00 Functions: 95.2 % 124 118

            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
        

Generated by: LCOV version 2.0-1