diff --git a/src/hos_cosgi_webserver/CMakeLists.txt b/src/hos_cosgi_webserver/CMakeLists.txt index 62744b68..220be69a 100644 --- a/src/hos_cosgi_webserver/CMakeLists.txt +++ b/src/hos_cosgi_webserver/CMakeLists.txt @@ -8,7 +8,6 @@ find_package(catkin REQUIRED COMPONENTS hos_cosgi_core hos_cpp_utils hos_deps - cmake_modules roscpp ) @@ -17,7 +16,6 @@ find_package(APR REQUIRED) find_package(CELIX REQUIRED) find_package(Boost REQUIRED) find_package(libvariant REQUIRED) -find_package(libhttpserver REQUIRED) find_package(Poco REQUIRED COMPONENTS Net NetSSL) @@ -37,6 +35,8 @@ catkin_package( add_definitions( -std=c++11 + # We need to explicitly enable boost nanoseconds +# -DBOOST_DATE_TIME_POSIX_TIME_STD_CONFIG ) include_directories( @@ -45,7 +45,6 @@ include_directories( ${APR_INCLUDE_DIR} ${APR_INCLUDE_DIR}/apr-1.0 ${libvariant_INCLUDE_DIRS} - ${libhttpserver_INCLUDE_DIRS} ${Poco_INCLUDE_DIRS} ) @@ -57,7 +56,7 @@ bundle(hos_cosgi_webserver SOURCES ${PROJECT_SOURCE_DIR}/src/BundleActivator ${PROJECT_SOURCE_DIR}/src/hos/cosgi/service/webserver/WebServerServiceImpl -# ${PROJECT_SOURCE_DIR}/src/hos/cosgi/service/webserver/resources/DevicesHTTPResource + ${PROJECT_SOURCE_DIR}/src/hos/cosgi/service/webserver/resources/DevicesResource ${PROJECT_SOURCE_DIR}/src/hos/cosgi/service/webserver/resources/SchemaResource ) @@ -65,7 +64,6 @@ target_link_libraries(hos_cosgi_webserver ${catkin_LIBRARIES} ${APR_LIBRARIES} ${libvariant_LIBRARIES} - ${libhttpserver_LIBRARIES} ${Poco_LIBRARIES} ) diff --git a/src/hos_cosgi_webserver/include/hos_cosgi_webserver/hos/cosgi/service/webserver/resources/WebResourceHandlerImpl.h b/src/hos_cosgi_webserver/include/hos_cosgi_webserver/hos/cosgi/service/webserver/resources/WebResourceHandlerImpl.h index c0358292..3a9f4fd8 100644 --- a/src/hos_cosgi_webserver/include/hos_cosgi_webserver/hos/cosgi/service/webserver/resources/WebResourceHandlerImpl.h +++ b/src/hos_cosgi_webserver/include/hos_cosgi_webserver/hos/cosgi/service/webserver/resources/WebResourceHandlerImpl.h @@ -30,7 +30,8 @@ class WebResourceHandlerException protected: Poco::Net::HTTPResponse::HTTPStatus statusCode; public: - WebResourceHandlerException(const std::string& msg, Poco::Net::HTTPResponse::HTTPStatus statusCode) + WebResourceHandlerException( const std::string& msg, + Poco::Net::HTTPResponse::HTTPStatus statusCode = Poco::Net::HTTPResponse::HTTPStatus::HTTP_INTERNAL_SERVER_ERROR) : std::runtime_error(msg), statusCode(statusCode) {} Poco::Net::HTTPResponse::HTTPStatus getStatusCode() const { return statusCode; @@ -142,8 +143,10 @@ protected: void handleResponse( const Poco::Net::HTTPServerRequest& request, Poco::Net::HTTPServerResponse &resp, const libvariant::Variant& msg, + const std::string& mime = "text/plain", Poco::Net::HTTPResponse::HTTPStatus status = Poco::Net::HTTPResponse::HTTPStatus::HTTP_OK ) { resp.setStatusAndReason(status); + resp.setContentType(mime); auto format = getResponseDataFormat(request); resp.send() << encodeVariant(msg, request, format); } @@ -151,24 +154,42 @@ protected: void handleResponse( const Poco::Net::HTTPServerRequest& request, Poco::Net::HTTPServerResponse &resp, const std::string& msg, + const std::string& mime = "text/plain", Poco::Net::HTTPResponse::HTTPStatus status = Poco::Net::HTTPResponse::HTTPStatus::HTTP_OK ) { resp.setStatusAndReason(status); + resp.setContentType(mime); resp.send() << msg; } void handleErrorResponse( const Poco::Net::HTTPServerRequest& request, Poco::Net::HTTPServerResponse &resp, - const std::string& msg = "Internal Server Error", + const std::string& msg, Poco::Net::HTTPResponse::HTTPStatus status = Poco::Net::HTTPResponse::HTTPStatus::HTTP_INTERNAL_SERVER_ERROR ) { resp.setStatusAndReason(status); - resp.send() << "ERROR: " << msg; + resp.setContentType("text/plain"); + std::ostream& out = resp.send(); + out << "ERROR: " << msg; + addDebugInfo(out, request, resp); + } + + void handleErrorResponse( const Poco::Net::HTTPServerRequest& request, + Poco::Net::HTTPServerResponse &resp, + Poco::Net::HTTPResponse::HTTPStatus status = Poco::Net::HTTPResponse::HTTPStatus::HTTP_INTERNAL_SERVER_ERROR ) { + resp.setStatusAndReason(status); + resp.setContentType("text/plain"); + std::ostream& out = resp.send(); + out << "ERROR: " << resp.getReason(); + addDebugInfo(out, request, resp); } void handleErrorResponse( const Poco::Net::HTTPServerRequest& request, Poco::Net::HTTPServerResponse &resp, const WebResourceHandlerException& e ) { resp.setStatusAndReason(e.getStatusCode()); - resp.send() << "ERROR: " << e.what(); + resp.setContentType("text/plain"); + std::ostream& out = resp.send(); + out << "ERROR: " << e.what(); + addDebugInfo(out, request, resp); } void handleErrorResponse( const Poco::Net::HTTPServerRequest& request, @@ -176,7 +197,23 @@ protected: const std::exception& e, Poco::Net::HTTPResponse::HTTPStatus status = Poco::Net::HTTPResponse::HTTPStatus::HTTP_INTERNAL_SERVER_ERROR ) { resp.setStatusAndReason(status); - resp.send() << "ERROR: " << e.what(); + resp.setContentType("text/plain"); + std::ostream& out = resp.send(); + out << "ERROR: " << e.what(); + addDebugInfo(out, request, resp); + } + + void addDebugInfo(std::ostream& out, const Poco::Net::HTTPServerRequest& req, Poco::Net::HTTPServerResponse &resp) { + out << std::endl << std::endl + <<"========== DEBUG ==========" << std::endl + << "getURI: " << req.getURI() << std::endl + << "getHost: " << req.getHost() << std::endl + << "getMethod: " << req.getMethod() << std::endl + << "getTransferEncoding: " << req.getTransferEncoding() << std::endl + << "getVersion: " << req.getVersion() << std::endl + << "getContentType: " << req.getContentType() << std::endl + << "getContentLength: " << req.getContentLength() << std::endl + << "======== DEBUG END ========" << std::endl ; } virtual void _handleRequest(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp) { diff --git a/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/WebServerServiceImpl.cpp b/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/WebServerServiceImpl.cpp index 57895826..4cd7a3fc 100644 --- a/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/WebServerServiceImpl.cpp +++ b/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/WebServerServiceImpl.cpp @@ -3,6 +3,7 @@ #include #include #include "resources/SchemaResource.h" +#include "resources/DevicesResource.h" @@ -62,21 +63,9 @@ void WebServerServiceImpl::setRepoService(devicerepository::DeviceRepositoryServ -void WebServerServiceImpl::RequestHandlerFactory::DefaultRequestHandler::handleRequest( +void WebServerServiceImpl::RequestHandlerFactory::DefaultRequestHandler::_handleRequest( Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp ) { - resp.setStatusAndReason(Poco::Net::HTTPResponse::HTTP_FORBIDDEN); - resp.setContentType("text/plain"); - resp.send() - << "ERROR: " << resp.getReason() << std::endl - << "========== DEBUG ==========" << std::endl - << "getURI: " << req.getURI() << std::endl - << "getHost: " << req.getHost() << std::endl - << "getMethod: " << req.getMethod() << std::endl - << "getTransferEncoding: " << req.getTransferEncoding() << std::endl - << "getVersion: " << req.getVersion() << std::endl - << "getContentType: " << req.getContentType() << std::endl - << "getContentLength: " << req.getContentLength() << std::endl - << "======== DEBUG END ========" << std::endl ; + handleErrorResponse(req, resp, Poco::Net::HTTPResponse::HTTPStatus::HTTP_FORBIDDEN); } @@ -118,6 +107,9 @@ void WebServerServiceImpl::init() noexcept { return new resources::SchemaResourceHandler(nodehandle, &coreService->getSchemaLoader(), this); }) ); + registerWebResource("/devices", + std::make_shared(this, repoService) + ); registerWebResource("/default", std::make_shared([=](const Net::HTTPServerRequest&, bool)->Net::HTTPRequestHandler* { return new resources::WebResourceHandlerImpl(); diff --git a/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/WebServerServiceImpl.h b/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/WebServerServiceImpl.h index ef180672..a21766e9 100644 --- a/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/WebServerServiceImpl.h +++ b/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/WebServerServiceImpl.h @@ -3,6 +3,7 @@ #include +#include #include #include #include @@ -16,15 +17,9 @@ -#include -//#include "resources/SchemaHTTPResource.h" -//#include "resources/DevicesHTTPResource.h" - - - #include - #include +#include #include #include #include @@ -44,22 +39,6 @@ namespace service { namespace webserver { -/** - * Used for internal error handling. - */ -class WebResourceHandlerException - : public std::runtime_error { -protected: - Poco::Net::HTTPResponse::HTTPStatus statusCode; -public: - WebResourceHandlerException(const std::string& msg, Poco::Net::HTTPResponse::HTTPStatus statusCode) - : std::runtime_error(msg), statusCode(statusCode) {} - Poco::Net::HTTPResponse::HTTPStatus getStatusCode() { - return statusCode; - } -}; - - class WebServerServiceImpl : public virtual WebServerService, @@ -73,9 +52,9 @@ private: WebServerServiceImpl* parent; bool ssl; class DefaultRequestHandler - : public Poco::Net::HTTPRequestHandler { + : public resources::WebResourceHandlerImpl { public: - virtual void handleRequest(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp) override; + virtual void _handleRequest(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp) override; }; public: RequestHandlerFactory(WebServerServiceImpl* parent, bool ssl = false) @@ -98,9 +77,11 @@ private: void init() noexcept; public: + WebServerServiceImpl() { prefixPath.getPathSegments(prefixPathPieces); } + ~WebServerServiceImpl(); diff --git a/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/resources/DevicesHTTPResource.cpp b/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/resources/DevicesHTTPResource.cpp deleted file mode 100644 index 189501f2..00000000 --- a/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/resources/DevicesHTTPResource.cpp +++ /dev/null @@ -1,298 +0,0 @@ - -#include "DevicesHTTPResource.h" -#include -#include - - -// Service Namespace -namespace hos { -namespace cosgi { -namespace service { -namespace webserver { -namespace resources { - - - -using namespace httpserver; -using namespace libvariant; - - - -/** - * Used for internal error handling. - */ -class ResourceException - : public std::runtime_error { -protected: - int errorCode; -public: - ResourceException(const std::string& msg, int errorCode) - : std::runtime_error(msg), errorCode(errorCode) {} - int getErrorCode() { - return errorCode; - } -}; - - - -void DevicesHTTPResource::init( const std::vector& prefixPath, - const std::string& registeredPath, - const std::string& absolutePath, - httpserver::webserver& webserver, - WebServerService& service ) { - webResource.prefixPath = prefixPath; - webResource.disallow_all(); - webResource.set_allowing(MHD_HTTP_METHOD_GET, true); - webResource.set_allowing(MHD_HTTP_METHOD_POST, true); - webserver.register_resource(absolutePath, &webResource, true); -} - - - -DevicesHTTPResource::WebResource::WebResource(devicerepository::DeviceRepositoryService* repoService) - : repoService(repoService) { - infoCategoryFuncs.insert({"type", [](devicerepository::Device* device, const httpserver::http_request& req, const std::string& category)->Variant { - return device->getDeviceType(); - } }); - infoCategoryFuncs.insert({"status_topic", [](devicerepository::Device* device, const httpserver::http_request& req, const std::string& category)->Variant { - return device->getStatusTopic(); - } }); - infoCategoryFuncs.insert({"command_topic", [](devicerepository::Device* device, const httpserver::http_request& req, const std::string& category)->Variant { - return device->getCmdTopic(); - } }); - infoCategoryFuncs.insert({"current_state", [](devicerepository::Device* device, const httpserver::http_request& req, const std::string& category)->Variant { - return device->getCurrentState(); - } }); - infoCategoryFuncs.insert({"last_state", [](devicerepository::Device* device, const httpserver::http_request& req, const std::string& category)->Variant { - return device->getLastState(); - } }); - /*infoCategoryFuncs.insert({"url", [this](devicerepository::Device* device, const std::string& category, const httpserver::http_request& req)->Variant { - std::stringstream ss; - req. - ss << http - return device->getLastState(); - } });*/ -} - - -void DevicesHTTPResource::WebResource::render_GET(const httpserver::http_request& req, httpserver::http_response** res) { - std::cout << "DevicesHTTPResource::render_GET()" << std::endl; - std::cout << req; - - try { - // Get method parameter - DataMethod dataMethod = DataMethod::GET; // Default method is get - std::string methodValue; - std::string methodRaw = req.get_arg("method"); - if (!methodRaw.empty()) { - if (methodRaw.compare("get") == 0) { - // do nothing - } else if (methodRaw.compare("command") == 0) { - dataMethod = DataMethod::COMMAND; - methodValue = req.get_arg("value"); - if (methodValue.empty()) { - throw ResourceException((boost::format("Method parameter \"%1%\" requires a value parameter.") % methodRaw).str(), 400); - } - } else if (methodRaw.compare("statusupdate") == 0) { - dataMethod = DataMethod::STATUSUPDATE; - methodValue = req.get_arg("value"); - if (methodValue.empty()) { - throw ResourceException((boost::format("Method parameter \"%1%\" requires a value parameter.") % methodRaw).str(), 400); - } - } else { - throw ResourceException((boost::format("Unknown method parameter \"%1%\"") % methodRaw).str(), 400); - } - } - - // No device given - if (req.get_path_pieces_size() <= prefixPath.size() + 1) { - if (dataMethod != DataMethod::GET) { - throw ResourceException("Requested method requires a device.", 400); - } - Variant result; - for (auto d : repoService->getDevices()) { - auto dev = d.lock(); - result[dev->getDeviceID()] = getDeviceInfo(dev.get(), req); - } - *res = new http_response(http_response_builder(SerializeJSON(result, true), 200).string_response()); - - // Device given - } else { - std::string devid = req.get_path_piece(prefixPath.size() + 1); - auto device = repoService->getDevice(devid); - if (!device.expired()) { - switch (dataMethod) { - case DataMethod::GET: - { - std::string category = req.get_path_piece(prefixPath.size() + 2); - std::vector path; - for (int i = prefixPath.size() + 3; i < req.get_path_pieces_size(); i++) { - path.push_back(req.get_path_piece(i)); - } - Variant result = getDeviceInfo(device.lock().get(), req, category, path); - std::string msg; - switch (getDataFormat(req)) { - case DataFormat::JSON: - msg = SerializeJSON(result, true); - break; - case DataFormat::JSONP: - { - std::string padding = req.get_arg("callback"); - if (padding.empty()) { - padding = req.get_arg("jsonp"); - } - if (padding.empty()) { - throw ResourceException("No callback parameter specified for JSONP.", 500); - } - std::stringstream ss; - ss << padding << "(" << SerializeJSON(result, false) << ")"; - msg = ss.str(); - } - break; - case DataFormat::YAML: - msg = SerializeYAML(result); - break; - case DataFormat::XMLPLIST: - msg = SerializeXMLPLIST(result, true); - break; - case DataFormat::BUNDLEHDR: - msg = SerializeBundleHdr(result); - break; - case DataFormat::MSGPACK: - msg = Serialize(result, SERIALIZE_MSGPACK); - break; - default: - throw ResourceException("Internal server error", 500); - } - *res = new http_response(http_response_builder(msg, 200).string_response()); - } - break; - case DataMethod::COMMAND: - { - std::vector path; - for (int i = prefixPath.size() + 2; i < req.get_path_pieces_size(); i++) { - path.push_back(req.get_path_piece(i)); - } - sendDeviceCommand(device.lock().get(), req, methodValue, path); - *res = new http_response(http_response_builder("OK: Command send.", 200).string_response()); - } - break; - case DataMethod::STATUSUPDATE: - throw ResourceException("ToDo: Implement method.", 500); - default: - throw ResourceException("Internal server error", 500); - } - } else { - throw ResourceException((boost::format("Could not find device \"%1%\"") % devid).str(), 404); - } - } - - } catch (ResourceException& e) { - *res = new http_response(http_response_builder((boost::format("ERROR: %1%") % e.what()).str(), e.getErrorCode()).string_response()); - } catch (std::exception& e) { - *res = new http_response(http_response_builder((boost::format("ERROR: %1%") % e.what()).str(), 500).string_response()); - } - - std::cout << **res; -} - - - -void DevicesHTTPResource::WebResource::render_POST(const httpserver::http_request& req, httpserver::http_response** res) { - std::cout << "DevicesHTTPResource::render_POST()" << std::endl; - std::cout << req; - std::cout << "content: " << req.get_content() << std::endl; - *res = new http_response(http_response_builder("ToDo: Implement POST", 500).string_response()); - std::cout << **res; -} - - - -DevicesHTTPResource::WebResource::DataFormat DevicesHTTPResource::WebResource::getDataFormat(const httpserver::http_request& req) { - DataFormat retval = DataFormat::JSON; - std::string formatRaw = req.get_arg("format"); - if (!formatRaw.empty()) { - if (formatRaw.compare("json") == 0) { - // do nothing - } else if (formatRaw.compare("jsonp") == 0) { - retval = DataFormat::JSONP; - } else if (formatRaw.compare("yaml") == 0) { - retval = DataFormat::YAML; - } else if (formatRaw.compare("xmlplist") == 0) { - retval = DataFormat::XMLPLIST; - } else if (formatRaw.compare("bundlehdr") == 0) { - retval = DataFormat::BUNDLEHDR; - } else if (formatRaw.compare("msgpack") == 0) { - retval = DataFormat::MSGPACK; - } else { - throw ResourceException((boost::format("Unknown format parameter \"%1%\"") % formatRaw).str(), 400); - } - } - return retval; -} - -Variant DevicesHTTPResource::WebResource::getDeviceInfo(devicerepository::Device* device, const httpserver::http_request& req, const std::string& category, const std::vector& path) { - Variant retval; - - // We want all info categories - if (category.empty()) { - for (auto c : infoCategoryFuncs) { - retval[c.first] = c.second(device, req, category); - } - - // We want a single info category - } else { - auto c = infoCategoryFuncs.find(category); - if (c != infoCategoryFuncs.end()) { - retval = c->second(device, req, category); - if (path.size() > 0) { - std::stringstream ss; - for (auto p : path) { - ss << "/" << p; - } - std::string pathstr = ss.str(); - if (retval.HasPath(pathstr)) { - retval = retval.GetPath(pathstr); - } else { - throw ResourceException((boost::format("Unknown path \"%1%\" for device info category \"%2%\".") % pathstr % category).str(), 404); - } - } - } else { - throw ResourceException((boost::format("Unknown device info category \"%1%\"") % category).str(), 404); - } - } - - return retval; -} - - -void DevicesHTTPResource::WebResource::sendDeviceCommand(devicerepository::Device* device, const httpserver::http_request& req, const std::string& cmd, const std::vector& path) { - Variant cmdMsg; - VariantRef mPart = cmdMsg.Ref(); - if (path.size() > 0) { - for (int i = 0; i < path.size(); i++) { - mPart[path[i]] = Variant(); - mPart.ReassignRef(mPart[path[i]]); - } - } - /* - * The libvariant json deserializer knows nothing about RFC 7159, therefore non-object or non-array values are rejected. - * As a workaround we use the yaml deserializer (yaml can be considered a super-set of json). - * Fun fact: the json serializer accepts non-object or non-array values. - */ - //mPart = DeserializeJSON(cmd); - mPart = DeserializeYAML(cmd); - device->sendCommand(cmdMsg); -} - - - - -} // namespace resources -} // namespace webserver -} // namespace service -} // namespace cosgi -} // namespace hos - - - diff --git a/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/resources/DevicesResource.cpp b/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/resources/DevicesResource.cpp new file mode 100644 index 00000000..466bd39f --- /dev/null +++ b/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/resources/DevicesResource.cpp @@ -0,0 +1,240 @@ + +#include "DevicesResource.h" +#include +#include +#include +#include +#include + + +// Service Namespace +namespace hos { +namespace cosgi { +namespace service { +namespace webserver { +namespace resources { + + + +using namespace Poco; +using namespace libvariant; + + + +DevicesResourceHandlerFactory::DevicesResourceHandlerFactory(WebServerService* service, devicerepository::DeviceRepositoryService* repoService) + : service(service), repoService(repoService) { + infoCategoryFuncs.insert({"type", [](devicerepository::Device* device, Poco::Net::HTTPServerRequest &req, const std::string& category, bool ssl)->Variant { + return device->getDeviceType(); + } }); + + infoCategoryFuncs.insert({"status_topic", [](devicerepository::Device* device, Poco::Net::HTTPServerRequest &req, const std::string& category, bool ssl)->Variant { + return device->getStatusTopic(); + } }); + + infoCategoryFuncs.insert({"command_topic", [](devicerepository::Device* device, Poco::Net::HTTPServerRequest &req, const std::string& category, bool ssl)->Variant { + return device->getCmdTopic(); + } }); + + infoCategoryFuncs.insert({"current_state", [](devicerepository::Device* device, Poco::Net::HTTPServerRequest &req, const std::string& category, bool ssl)->Variant { + auto retval = device->getCurrentState(); + if (retval.IsMap()) { + retval["timestamp"] = utils::timestamp_to_ISO8601(device->getCurrentStateTimestamp()); + } + return retval; + } }); + + infoCategoryFuncs.insert({"last_state", [](devicerepository::Device* device, Poco::Net::HTTPServerRequest &req, const std::string& category, bool ssl)->Variant { + auto retval = device->getLastState(); + if (retval.IsMap()) { + retval["timestamp"] = utils::timestamp_to_ISO8601(device->getLastStateTimestamp()); + } + return retval; + } }); + + infoCategoryFuncs.insert({"uri", [=](devicerepository::Device* device, Poco::Net::HTTPServerRequest &req, const std::string& category, bool ssl)->Variant { + auto& baseUri = service->getPrefixPath(); + std::stringstream ss; + if (ssl) { + ss << "https://"; + } else { + ss << "http://"; + } + ss << req.getHost() << baseUri.toString() << "/devices/" << device->getDeviceID(); + return ss.str(); + } }); +} + + + +Net::HTTPRequestHandler* DevicesResourceHandlerFactory::createRequestHandler( const Net::HTTPServerRequest& request, bool ssl ) noexcept { + return new DevicesResourceHandler(this, service, repoService, infoCategoryFuncs, ssl); +} + + + +void DevicesResourceHandler::_handleGET(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp) { + // Get method parameter + Net::HTMLForm form(req); + DevicesAPIMethod dataMethod = DevicesAPIMethod::GET; // Default method is get + std::string methodValue; + if (form.has("method")) { + std::string methodRaw = form.get("method"); + bool requireValue = false; + if (methodRaw == "get") { + // do nothing + } else if (methodRaw == "command") { + dataMethod = DevicesAPIMethod::COMMAND; + requireValue = true; + } else if (methodRaw.compare("statusupdate") == 0) { + dataMethod = DevicesAPIMethod::STATUSUPDATE; + requireValue = true; + } else { + throw WebResourceHandlerException((boost::format("Unknown API method \"%1%\"") % methodRaw).str(), + Poco::Net::HTTPResponse::HTTPStatus::HTTP_BAD_REQUEST); + } + if (requireValue) { + if (form.has("value")) { + methodValue = form.get("value"); + } else { + throw WebResourceHandlerException((boost::format("API method \"%1%\" requires a value parameter.") % methodRaw).str(), + Poco::Net::HTTPResponse::HTTPStatus::HTTP_BAD_REQUEST); + } + } + } + + auto& prefixPath = service->getPrefixPathPieces(); + Poco::URI uri(req.getURI()); + std::vector pathPieces; + uri.getPathSegments(pathPieces); + + // No device given + if (pathPieces.size() <= prefixPath.size() + 1) { + if (dataMethod != DevicesAPIMethod::GET) { + throw WebResourceHandlerException("Requested API method requires a device", + Poco::Net::HTTPResponse::HTTPStatus::HTTP_BAD_REQUEST); + } + Variant result; + for (auto d : repoService->getDevices()) { + auto dev = d.lock(); + result[dev->getDeviceID()] = getDeviceInfo(dev.get(), req); + } + handleResponse(req, resp, result); + + // Device given + } else { + std::string devid = pathPieces[prefixPath.size() + 1]; + auto device = repoService->getDevice(devid); + if (!device.expired()) { + switch (dataMethod) { + case DevicesAPIMethod::GET: + { + std::string category; + if (pathPieces.size() > prefixPath.size() + 2) { + category = pathPieces[prefixPath.size() + 2]; + } + std::vector path; + for (int i = prefixPath.size() + 3; i < pathPieces.size(); i++) { + path.push_back(pathPieces[i]); + } + Variant result = getDeviceInfo(device.lock().get(), req, category, path); + handleResponse(req, resp, result); + } + break; + case DevicesAPIMethod::COMMAND: + { + std::vector path; + for (int i = prefixPath.size() + 2; i < pathPieces.size(); i++) { + path.push_back(pathPieces[i]); + } + sendDeviceCommand(device.lock().get(), req, methodValue, path); + handleResponse(req, resp, std::string("OK: Command send")); + } + break; + case DevicesAPIMethod::STATUSUPDATE: + throw WebResourceHandlerException("ToDo: Implement method."); + default: + throw WebResourceHandlerException("Internal Server Error"); + } + } else { + throw WebResourceHandlerException((boost::format("Could not find device \"%1%\"") % devid).str(), + Poco::Net::HTTPResponse::HTTPStatus::HTTP_NOT_FOUND); + } + } +} + + + +void DevicesResourceHandler::_handlePOST(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp) { + throw WebResourceHandlerException("ToDo: Implement POST."); +} + + + +Variant DevicesResourceHandler::getDeviceInfo(devicerepository::Device* device, Net::HTTPServerRequest &req, const std::string& category, const std::vector& path) { + Variant retval; + + // We want all info categories + if (category.empty()) { + for (auto c : infoCategoryFuncs) { + retval[c.first] = c.second(device, req, category, ssl); + } + + // We want a single info category + } else { + auto c = infoCategoryFuncs.find(category); + if (c != infoCategoryFuncs.end()) { + retval = c->second(device, req, category, ssl); + if (path.size() > 0) { + std::stringstream ss; + for (auto p : path) { + ss << "/" << p; + } + std::string pathstr = ss.str(); + if (retval.HasPath(pathstr)) { + retval = retval.GetPath(pathstr); + } else { + throw WebResourceHandlerException((boost::format("Unknown path \"%1%\" for device info category \"%2%\".") % pathstr % category).str(), + Poco::Net::HTTPResponse::HTTPStatus::HTTP_NOT_FOUND); + } + } + } else { + throw WebResourceHandlerException((boost::format("Unknown device info category \"%1%\"") % category).str(), + Poco::Net::HTTPResponse::HTTPStatus::HTTP_NOT_FOUND); + } + } + + return retval; +} + + + +void DevicesResourceHandler::sendDeviceCommand(devicerepository::Device* device, Net::HTTPServerRequest &req, const std::string& cmd, const std::vector& path) { + Variant cmdMsg; + VariantRef mPart = cmdMsg.Ref(); + if (path.size() > 0) { + for (int i = 0; i < path.size(); i++) { + mPart[path[i]] = Variant(); + mPart.ReassignRef(mPart[path[i]]); + } + } + /* + * The libvariant json deserializer knows nothing about RFC 7159, therefore non-object or non-array values are rejected. + * As a workaround we use the yaml deserializer (yaml can be considered a super-set of json). + * Fun fact: the json serializer accepts non-object or non-array values. + */ + //mPart = DeserializeJSON(cmd); + mPart = DeserializeYAML(cmd); + device->sendCommand(cmdMsg); +} + + + + +} // namespace resources +} // namespace webserver +} // namespace service +} // namespace cosgi +} // namespace hos + + + diff --git a/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/resources/DevicesHTTPResource.h b/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/resources/DevicesResource.h similarity index 51% rename from src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/resources/DevicesHTTPResource.h rename to src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/resources/DevicesResource.h index fe23cea2..b3043195 100644 --- a/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/resources/DevicesHTTPResource.h +++ b/src/hos_cosgi_webserver/src/hos/cosgi/service/webserver/resources/DevicesResource.h @@ -3,10 +3,12 @@ #include +#include #include +#include #include #include -#include +#include #include #include @@ -20,6 +22,33 @@ namespace webserver { namespace resources { + +enum class DevicesAPIMethod { + GET, + COMMAND, + STATUSUPDATE +}; + + +class DevicesResourceHandlerFactory + : public virtual WebResourceHandlerFactory { +public: + typedef std::function infoCategoryFunc_t; +private: + WebServerService* service; + devicerepository::DeviceRepositoryService* repoService; + std::map infoCategoryFuncs; +public: + DevicesResourceHandlerFactory(WebServerService* service, devicerepository::DeviceRepositoryService* repoService); + virtual Poco::Net::HTTPRequestHandler* createRequestHandler( const Poco::Net::HTTPServerRequest& request, bool ssl = false ) noexcept override; + +}; + + + /** * Implements a REST API for accessing the device repository and getting info about a device. * @@ -42,66 +71,47 @@ namespace resources { * JSON texts (namely single strings, numbers or boolean values) that are not valid JSON texts according to RFC 4627. * Keep in mind that there are still a lot of JSON libraries and validators out there which still no nothing about RFC 7159. */ -class DevicesHTTPResource - : public virtual webserver::WebResource { +class DevicesResourceHandler + : public virtual WebResourceHandlerImpl { private: + DevicesResourceHandlerFactory* parent; + WebServerService* service; + devicerepository::DeviceRepositoryService* repoService; + const std::map& infoCategoryFuncs; + bool ssl; - /** - * libhttpserver does a static_cast with any class registered as http resource. Unfortunately this breaks classes - * using virtual inheritance. Therefore we register this inner class which does not use virtual inheritance. - */ - class WebResource - : public httpserver::http_resource { + libvariant::Variant getDeviceInfo( devicerepository::Device* device, + Poco::Net::HTTPServerRequest &req, + const std::string& category = std::string(), + const std::vector& path = std::vector() ); - public: - enum class DataMethod { GET, COMMAND, STATUSUPDATE }; - enum class DataFormat { JSON, JSONP, YAML, XMLPLIST, BUNDLEHDR, MSGPACK }; - - typedef std::function infoCategoryFunc_t; - - devicerepository::DeviceRepositoryService* repoService = nullptr; - std::vector prefixPath; - std::map infoCategoryFuncs; - - WebResource(devicerepository::DeviceRepositoryService* repoService); - - void render_GET(const httpserver::http_request& req, httpserver::http_response** res); - - void render_POST(const httpserver::http_request& req, httpserver::http_response** res); - - DataFormat getDataFormat(const httpserver::http_request& req); - - libvariant::Variant getDeviceInfo( devicerepository::Device* device, - const httpserver::http_request& req, - const std::string& category = std::string(), - const std::vector& path = std::vector() ); - - void sendDeviceCommand( devicerepository::Device* device, - const httpserver::http_request& req, - const std::string& cmd, - const std::vector& path = std::vector() ); - - } webResource; + void sendDeviceCommand( devicerepository::Device* device, + Poco::Net::HTTPServerRequest &req, + const std::string& cmd, + const std::vector& path = std::vector() ); public: - DevicesHTTPResource(devicerepository::DeviceRepositoryService* repoService) - : webResource(repoService) {} + DevicesResourceHandler( DevicesResourceHandlerFactory* parent, + WebServerService* service, + devicerepository::DeviceRepositoryService* repoService, + const std::map& infoCategoryFuncs, + bool ssl = false ) + : parent(parent), service(service), repoService(repoService), infoCategoryFuncs(infoCategoryFuncs), ssl(ssl) {} - virtual void init( const std::vector& prefixPath, - const std::string& registeredPath, - const std::string& absolutePath, - httpserver::webserver& webserver, - webserver::WebServerService& service ); + virtual void _handleGET(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp) override; + + virtual void _handlePOST(Poco::Net::HTTPServerRequest &req, Poco::Net::HTTPServerResponse &resp) override; }; + + } // namespace resources } // namespace webserver } // namespace service } // namespace cosgi } // namespace hos + diff --git a/src/hos_cpp_utils/include/hos_cpp_utils/hos/utils/Timestamp.h b/src/hos_cpp_utils/include/hos_cpp_utils/hos/utils/Timestamp.h new file mode 100644 index 00000000..ef4e2020 --- /dev/null +++ b/src/hos_cpp_utils/include/hos_cpp_utils/hos/utils/Timestamp.h @@ -0,0 +1,52 @@ + +#pragma once + + +#include +#include +#include +#include +#include + + +namespace hos { +namespace utils { + + +/* + * It took me way too long to come up with this solution (which I only consider as workaround, + * but currently it seems to be the best solution). + * There definitely needs to be much better support for timestamp-to-string conversion in C++, + * the current situation is way too unsatisfying. And boost isn't much of a help either. + */ +template +std::string timestamp_to_ISO8601(const std::chrono::time_point<_CLOCK>& timestamp) { + std::stringstream ss; + std::tm tm_time; + char temp[256]; + // First convert timestamp to time_t (loosing precision) + std::time_t time = std::chrono::system_clock::to_time_t(timestamp); + // Then convert time_t to tm_time + localtime_r(&time, &tm_time); + // Print first part of ISO8601 string + std::strftime(temp, sizeof(temp), "%FT%H:%M:", &tm_time); + ss << temp; + // Get lost precision by converting out time_t back to a timestamp and substracting it from our given timestamp, + // and add seconds to avoid the "0.xyz" problem + std::chrono::duration msecs = timestamp - std::chrono::system_clock::from_time_t(time) + std::chrono::seconds(tm_time.tm_sec); + // Print seconds part of ISO8601 string + std::snprintf(temp, sizeof(temp), "%09.6f", msecs.count()); // is more compact than C++ counterpart + std::string secpart(temp); + std::replace(secpart.begin(), secpart.end(), '.', ',' ); // ISO 8601 prefers ',' over '.' as decimal mark. + ss << secpart; + // Get and print timezone part of ISO8601 string + std::strftime(temp, sizeof(temp), "%z", &tm_time); + ss << temp; + return ss.str(); +} + + + +} // namespace libvariant +} // namespace hos + diff --git a/src/hos_deps/cmake/Modules/FindPoco.cmake b/src/hos_deps/cmake/Modules/FindPoco.cmake new file mode 100644 index 00000000..543f21dc --- /dev/null +++ b/src/hos_deps/cmake/Modules/FindPoco.cmake @@ -0,0 +1,207 @@ +# - Find the Poco includes and libraries. +# The following variables are set if Poco is found. If Poco is not +# found, Poco_FOUND is set to false. +# Poco_FOUND - True when the Poco include directory is found. +# Poco_INCLUDE_DIRS - the path to where the poco include files are. +# Poco_LIBRARY_DIR - The path to where the poco library files are. +# Poco_BINARY_DIRS - The path to where the poco dlls are. +# Poco_LIBRARIES - list of all libs from requested components. + +# ---------------------------------------------------------------------------- +# If you have installed Poco in a non-standard location. +# Then you have three options. +# In the following comments, it is assumed that +# points to the root directory of the include directory of Poco. e.g +# If you have put poco in C:\development\Poco then is +# "C:/development/Poco" and in this directory there will be two +# directories called "include" and "lib". +# 1) After CMake runs, set Poco_INCLUDE_DIR to /poco<-version> +# 2) Use CMAKE_INCLUDE_PATH to set a path to /poco<-version>. This will allow FIND_PATH() +# to locate Poco_INCLUDE_DIR by utilizing the PATH_SUFFIXES option. e.g. +# SET(CMAKE_INCLUDE_PATH ${CMAKE_INCLUDE_PATH} "/include") +# 3) Set an environment variable called ${POCO_ROOT} that points to the root of where you have +# installed Poco, e.g. . It is assumed that there is at least a subdirectory called +# Foundation/include/Poco in this path. +# +# Note: +# 1) If you are just using the poco headers, then you do not need to use +# Poco_LIBRARY_DIR in your CMakeLists.txt file. +# 2) If Poco has not been installed, then when setting Poco_LIBRARY_DIR +# the script will look for /lib first and, if this fails, then for /stage/lib. +# +# Usage: +# In your CMakeLists.txt file do something like this: +# ... +# # Poco +# FIND_PACKAGE(Poco COMPONENTS XML Net Data...) +# ... +# INCLUDE_DIRECTORIES(${Poco_INCLUDE_DIRS}) +# LINK_DIRECTORIES(${Poco_LIBRARY_DIR}) +# +# In Windows, we make the assumption that, if the Poco files are installed, the default directory +# will be C:\poco or C:\Program Files\Poco or C:\Programme\Poco. + +MESSAGE(STATUS "Searching for Poco library...") + +SET(POCO_INCLUDE_PATH_DESCRIPTION "top-level directory containing the poco include directories. E.g /usr/local/include/ or c:\\poco\\include\\poco-1.3.2") +SET(POCO_INCLUDE_DIR_MESSAGE "Set the Poco_INCLUDE_DIR cmake cache entry to the ${POCO_INCLUDE_PATH_DESCRIPTION}") +SET(POCO_LIBRARY_PATH_DESCRIPTION "top-level directory containing the poco libraries.") +SET(POCO_LIBRARY_DIR_MESSAGE "Set the Poco_LIBRARY_DIR cmake cache entry to the ${POCO_LIBRARY_PATH_DESCRIPTION}") + + +SET(POCO_DIR_SEARCH $ENV{POCO_ROOT}) +IF(POCO_DIR_SEARCH) + FILE(TO_CMAKE_PATH ${POCO_DIR_SEARCH} POCO_DIR_SEARCH) +ENDIF(POCO_DIR_SEARCH) + + +IF(WIN32) + SET(POCO_DIR_SEARCH + ${POCO_DIR_SEARCH} + C:/poco + D:/poco + "C:/Program Files/poco" + "C:/Programme/poco" + "D:/Program Files/poco" + "D:/Programme/poco" + ) +ENDIF(WIN32) + +# Add in some path suffixes. These will have to be updated whenever a new Poco version comes out. +SET(SUFFIX_FOR_INCLUDE_PATH + poco-1.3.2 + poco-1.3.3 + poco-1.3.4 + poco-1.3.5 + poco-1.3.6 +) + +SET(SUFFIX_FOR_LIBRARY_PATH + poco-1.3.2/lib + poco-1.3.2/lib/Linux/i686 + poco-1.3.2/lib/Linux/x86_64 + poco-1.3.3/lib + poco-1.3.3/lib/Linux/i686 + poco-1.3.3/lib/Linux/x86_64 + poco-1.3.4/lib + poco-1.3.4/lib/Linux/i686 + poco-1.3.4/lib/Linux/x86_64 + poco-1.3.5/lib + poco-1.3.5/lib/Linux/i686 + poco-1.3.5/lib/Linux/x86_64 + poco-1.3.6/lib + poco-1.3.6/lib/Linux/i686 + poco-1.3.6/lib/Linux/x86_64 + lib + lib/Linux/i686 + lib/Linux/x86_64 +) + +# +# Look for an installation. +# +FIND_PATH(Poco_INCLUDE_DIR NAMES Foundation/include/Poco/SharedLibrary.h PATH_SUFFIXES ${SUFFIX_FOR_INCLUDE_PATH} PATHS + +# Look in other places. + ${POCO_DIR_SEARCH} + +# Help the user find it if we cannot. + DOC "The ${POCO_INCLUDE_PATH_DESCRIPTION}" +) + +IF(NOT Poco_INCLUDE_DIR) + +# Look for standard unix include paths + FIND_PATH(Poco_INCLUDE_DIR Poco/Poco.h DOC "The ${POCO_INCLUDE_PATH_DESCRIPTION}") + +ENDIF(NOT Poco_INCLUDE_DIR) + +# Assume we didn't find it. +SET(Poco_FOUND 0) + +# Now try to get the include and library path. +IF(Poco_INCLUDE_DIR) + IF(EXISTS "${Poco_INCLUDE_DIR}/Foundation/include/Poco/SharedLibrary.h") + SET(Poco_INCLUDE_DIRS + ${Poco_INCLUDE_DIR}/CppUnit/include + ${Poco_INCLUDE_DIR}/Foundation/include + ${Poco_INCLUDE_DIR}/Net/include + ${Poco_INCLUDE_DIR}/Util/include + ${Poco_INCLUDE_DIR}/XML/include + ) + SET(Poco_FOUND 1) + ELSEIF(EXISTS "${Poco_INCLUDE_DIR}/Poco/Poco.h") + SET(Poco_INCLUDE_DIRS + ${Poco_INCLUDE_DIR} + ) + SET(Poco_FOUND 1) + ENDIF() + + IF(NOT Poco_LIBRARY_DIR) + + FIND_LIBRARY(Poco_FOUNDATION_LIB NAMES PocoFoundation PocoFoundationd PATH_SUFFIXES ${SUFFIX_FOR_LIBRARY_PATH} PATHS + +# Look in other places. + ${Poco_INCLUDE_DIR} + ${POCO_DIR_SEARCH} + + /usr/lib64 + /usr/lib + +# Help the user find it if we cannot. + DOC "The ${POCO_LIBRARY_PATH_DESCRIPTION}" + ) + SET(Poco_LIBRARY_DIR "" CACHE PATH POCO_LIBARARY_PATH_DESCRIPTION) + GET_FILENAME_COMPONENT(Poco_LIBRARY_DIR ${Poco_FOUNDATION_LIB} PATH) + SET(Poco_LIBRARIES "") + SET(Comp_List "") + IF(Poco_LIBRARY_DIR AND Poco_FOUNDATION_LIB) +# Look for the poco binary path. + SET(Poco_BINARY_DIR ${Poco_INCLUDE_DIR}) + IF(Poco_BINARY_DIR AND EXISTS "${Poco_BINARY_DIR}/bin") + SET(Poco_BINARY_DIRS ${Poco_BINARY_DIR}/bin) + ENDIF(Poco_BINARY_DIR AND EXISTS "${Poco_BINARY_DIR}/bin") + ENDIF(Poco_LIBRARY_DIR AND Poco_FOUNDATION_LIB) + IF(Poco_FOUNDATION_LIB) + IF ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + SET(DBG "d") + ELSE ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + SET(DBG "") + ENDIF ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") + SET(Comp_List "Foundation${DBG}") + FOREACH(COMPONENT ${Poco_FIND_COMPONENTS}) + FIND_LIBRARY(LIB${COMPONENT} "Poco${COMPONENT}${DBG}" Poco_LIBRARY_DIR) + IF (LIB${COMPONENT}) + LIST(APPEND Poco_LIBRARIES "${LIB${COMPONENT}}") + LIST(APPEND Comp_List "${COMPONENT}${DBG}") + ENDIF(LIB${COMPONENT}) + ENDFOREACH(COMPONENT) + LIST(REMOVE_DUPLICATES Comp_List) + ENDIF(Poco_FOUNDATION_LIB) + ENDIF(NOT Poco_LIBRARY_DIR) +ENDIF(Poco_INCLUDE_DIR) + +IF(NOT Poco_FOUND) + IF(Poco_FIND_QUIETLY) + MESSAGE(STATUS "Poco was not found. ${POCO_INCLUDE_DIR_MESSAGE}") + ELSE(Poco_FIND_QUIETLY) + IF(Poco_FIND_REQUIRED) + MESSAGE(FATAL_ERROR "Poco was not found. ${POCO_INCLUDE_DIR_MESSAGE}") + ENDIF(Poco_FIND_REQUIRED) + ENDIF(Poco_FIND_QUIETLY) +ELSE(NOT Poco_FOUND) + MESSAGE(STATUS " Found Poco!") + SET(COMPONENT_STR "components found:") + FOREACH(comp ${Comp_List}) + SET(COMPONENT_STR "${COMPONENT_STR}, ${comp}") + + ENDFOREACH(comp ${Comp_List}) + STRING(REPLACE ":," ":" COMPONENT_LSTR ${COMPONENT_STR}) + MESSAGE(STATUS "${COMPONENT_LSTR}.") +ENDIF(NOT Poco_FOUND) + +#I added this in to add "libdl" on non-Windows systems. Technically dl is only neded if the "Foundation" component is used, +#but i doesn't hurt to add it in anyway - mas +if(Poco_FOUND AND NOT WIN32) + LIST(APPEND Poco_LIBRARIES "dl") +endif(Poco_FOUND AND NOT WIN32)