From ea796160af8958938fe045b5913329de35ff0d7e Mon Sep 17 00:00:00 2001 From: Paulchen-Panther Date: Fri, 12 Jul 2019 16:54:26 +0200 Subject: [PATCH] - The first part - Added CodeDocs config file for customization - Fixing LGTM alerts - LGTM bug fixed again - added token option to hyperion-remote - fix DBManager::getDB() - next bugfix - correct broken signal from SettingManager to Hyperion - Token list is created after the schema is fetched Signed-off-by: Paulchen-Panther --- .codedocs | 76 ++++ CMakeLists.txt | 5 +- README.md | 1 + assets/webconfig/content/conf_general.html | 21 + assets/webconfig/css/hyperion.css | 8 +- assets/webconfig/i18n/de.json | 20 + assets/webconfig/i18n/en.json | 20 + assets/webconfig/js/content_general.js | 45 ++ assets/webconfig/js/content_index.js | 12 +- assets/webconfig/js/content_network.js | 23 +- assets/webconfig/js/hyperion.js | 27 +- assets/webconfig/js/settings.js | 2 + assets/webconfig/js/ui_utils.js | 11 + cmake/FindGitVersion.cmake | 5 +- cmake/packages.cmake | 4 +- config/hyperion.config.json.commented | 11 +- config/hyperion.config.json.default | 11 +- doc/CMakeLists.txt | 34 -- doc/hyperion-footer.html | 8 - doc/hyperion.in.doxygen | 2 +- include/api/JsonAPI.h | 49 ++- include/db/AuthTable.h | 226 ++++++++++ include/db/DBManager.h | 130 ++++++ include/db/SettingsTable.h | 114 +++++ include/flatbufserver/FlatBufferServer.h | 3 + include/hyperion/AuthManager.h | 160 +++++++ include/hyperion/SettingsManager.h | 29 +- include/protoserver/ProtoServer.h | 2 + include/udplistener/UDPListener.h | 4 + include/utils/NetOrigin.h | 54 +++ include/webserver/WebServer.h | 2 +- libsrc/CMakeLists.txt | 1 + .../api/JSONRPC_schema/schema-authorize.json | 44 ++ libsrc/api/JSONRPC_schema/schema.json | 2 +- libsrc/api/JSONRPC_schemas.qrc | 1 + libsrc/api/JsonAPI.cpp | 278 +++++++++++- libsrc/db/CMakeLists.txt | 18 + libsrc/db/DBManager.cpp | 408 ++++++++++++++++++ libsrc/flatbufserver/FlatBufferServer.cpp | 19 +- libsrc/hyperion/AuthManager.cpp | 164 +++++++ libsrc/hyperion/CMakeLists.txt | 1 + libsrc/hyperion/Hyperion.cpp | 13 +- libsrc/hyperion/SettingsManager.cpp | 147 ++++--- libsrc/hyperion/hyperion.schema.json | 4 + libsrc/hyperion/resource.qrc | 1 + libsrc/hyperion/schema/schema-network.json | 54 +++ libsrc/jsonserver/JsonClientConnection.cpp | 46 +- libsrc/jsonserver/JsonClientConnection.h | 4 +- libsrc/jsonserver/JsonServer.cpp | 17 +- libsrc/leddevice/dev_net/ProviderUdp.cpp | 2 +- libsrc/leddevice/dev_net/ProviderUdp.h | 4 +- libsrc/protoserver/ProtoServer.cpp | 19 +- libsrc/udplistener/UDPListener.cpp | 24 +- libsrc/utils/NetOrigin.cpp | 76 ++++ libsrc/webserver/CgiHandler.cpp | 1 - libsrc/webserver/QtHttpClientWrapper.cpp | 25 +- libsrc/webserver/QtHttpClientWrapper.h | 45 +- libsrc/webserver/QtHttpServer.cpp | 200 ++++----- libsrc/webserver/QtHttpServer.h | 80 ++-- libsrc/webserver/StaticFileServing.cpp | 2 +- libsrc/webserver/WebJsonRpc.cpp | 7 +- libsrc/webserver/WebJsonRpc.h | 4 +- libsrc/webserver/WebServer.cpp | 3 +- libsrc/webserver/WebSocketClient.cpp | 44 +- .../webserver/WebSocketClient.h | 12 +- .../webserver/WebSocketUtils.h | 0 src/hyperion-remote/JsonConnection.cpp | 19 + src/hyperion-remote/JsonConnection.h | 3 + src/hyperion-remote/hyperion-remote.cpp | 90 ++-- src/hyperiond/CMakeLists.txt | 4 +- src/hyperiond/hyperiond.cpp | 16 + src/hyperiond/hyperiond.h | 10 +- 72 files changed, 2546 insertions(+), 485 deletions(-) create mode 100644 .codedocs delete mode 100644 doc/CMakeLists.txt delete mode 100644 doc/hyperion-footer.html create mode 100644 include/db/AuthTable.h create mode 100644 include/db/DBManager.h create mode 100644 include/db/SettingsTable.h create mode 100644 include/hyperion/AuthManager.h create mode 100644 include/utils/NetOrigin.h create mode 100644 libsrc/api/JSONRPC_schema/schema-authorize.json create mode 100644 libsrc/db/CMakeLists.txt create mode 100644 libsrc/db/DBManager.cpp create mode 100644 libsrc/hyperion/AuthManager.cpp create mode 100644 libsrc/hyperion/schema/schema-network.json create mode 100644 libsrc/utils/NetOrigin.cpp rename {include => libsrc}/webserver/WebSocketClient.h (87%) rename {include => libsrc}/webserver/WebSocketUtils.h (100%) diff --git a/.codedocs b/.codedocs new file mode 100644 index 00000000..ead816de --- /dev/null +++ b/.codedocs @@ -0,0 +1,76 @@ +# Hyperion.NG .codedocs Configuration File + +#--------------------------------------------------------------------------- +# CodeDocs Configuration +#--------------------------------------------------------------------------- + +# Include the Doxygen configuration from another file. +# The file must be a relative path with respect to the root of the repository. + +DOXYFILE = + +# Specify external repository to link documentation with. +# This is similar to Doxygen's TAGFILES option, but will automatically link to +# tags of other repositories already using CodeDocs. List each repository to +# link with by giving its location in the form of owner/repository. +# For example: +# TAGLINKS = doxygen/doxygen CodeDocs/osg +# Note: these repositories must already be built on CodeDocs. + +TAGLINKS = + +#--------------------------------------------------------------------------- +# Doxygen Configuration +#--------------------------------------------------------------------------- + +# Doxygen configuration may also be placed in this file. +# Currently, the following Doxygen configuration options are available. Refer +# to http://doxygen.org/manual/config.html for detailed explanation of the +# options. To request support for more options, contact support@codedocs.xyz. +# +# ABBREVIATE_BRIEF = +# ALIASES = +# ALPHABETICAL_INDEX = +# ALWAYS_DETAILED_SEC = +# CASE_SENSE_NAMES = +# CLASS_DIAGRAMS = +# DISABLE_INDEX = +# DISTRIBUTE_GROUP_DOC = +# EXAMPLE_PATH = + EXCLUDE = .ci/ \ + assets/ \ + bin/ + config/ \ + effects/ \ + test/ \ +# EXCLUDE_PATTERNS = +# EXCLUDE_SYMBOLS = +# EXTENSION_MAPPING = +# EXTRACT_LOCAL_CLASSES = +# FILE_PATTERNS = +# GENERATE_TAGFILE = +# GENERATE_TREEVIEW = +# HIDE_COMPOUND_REFERENCE = +# HIDE_SCOPE_NAMES = +# HIDE_UNDOC_CLASSES = +# HIDE_UNDOC_MEMBERS = +# HTML_TIMESTAMP = +# INLINE_GROUPED_CLASSES = +# INPUT_ENCODING = +# INTERNAL_DOCS = +# OPTIMIZE_OUTPUT_FOR_C = + PROJECT_BRIEF = "The successor to Hyperion aka Hyperion Next Generation" + PROJECT_NAME = "Hyperion.NG" +# PROJECT_NUMBER = +# SHORT_NAMES = +# SHOW_FILES = +# SHOW_INCLUDE_FILES = +# SHOW_NAMESPACES = +# SORT_BRIEF_DOCS = +# SORT_BY_SCOPE_NAME = +# SORT_MEMBER_DOCS = +# STRICT_PROTO_MATCHING = +# TYPEDEF_HIDES_STRUCT = + USE_MDFILE_AS_MAINPAGE = README.md +# VERBATIM_HEADERS = +# diff --git a/CMakeLists.txt b/CMakeLists.txt index 189ab92d..68c7f382 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -311,7 +311,7 @@ IF ( CMAKE_CROSSCOMPILING ) ENDIF() SET(QT_MIN_VERSION "5.5.0") -find_package(Qt5 COMPONENTS Core Gui Network SerialPort REQUIRED) +find_package(Qt5 COMPONENTS Core Gui Network SerialPort Sql REQUIRED) message( STATUS "Found Qt Version: ${Qt5Core_VERSION}" ) IF ( "${Qt5Core_VERSION}" VERSION_LESS "${QT_MIN_VERSION}" ) message( FATAL_ERROR "Your Qt version is to old! Minimum required ${QT_MIN_VERSION}" ) @@ -354,9 +354,6 @@ endif () # Add resources directory add_subdirectory(resources) -# Add the doxygen generation directory -add_subdirectory(doc) - # remove generated files on make cleaan too LIST( APPEND GENERATED_QRC ${CMAKE_BINARY_DIR}/EffectEngine.qrc diff --git a/README.md b/README.md index b784280d..e639811a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Azure-Pipeline](https://dev.azure.com/Hyperion-Project/Hyperion.NG/_apis/build/status/Hyperion.NG?branchName=master)](https://dev.azure.com/Hyperion-Project/Hyperion.NG/_build/latest?definitionId=7&branchName=master) [![Travis-CI](https://travis-ci.org/hyperion-project/hyperion.ng.svg?branch=master)](https://travis-ci.org/hyperion-project/hyperion.ng) [![LGTM](https://img.shields.io/lgtm/alerts/g/hyperion-project/hyperion.ng.svg)](https://lgtm.com/projects/g/hyperion-project/hyperion.ng/alerts/) +[![Documentation](https://codedocs.xyz/hyperion-project/hyperion.ng.svg)](https://codedocs.xyz/hyperion-project/hyperion.ng/) ## About Hyperion diff --git a/assets/webconfig/content/conf_general.html b/assets/webconfig/content/conf_general.html index ff6d8011..a2014f0e 100644 --- a/assets/webconfig/content/conf_general.html +++ b/assets/webconfig/content/conf_general.html @@ -18,6 +18,27 @@ +
+
+
+
+
+
+
+
+
+

+
+
+ +
+
+
+ +
+
+
+
diff --git a/assets/webconfig/css/hyperion.css b/assets/webconfig/css/hyperion.css index 6919f11c..e2dbaa51 100644 --- a/assets/webconfig/css/hyperion.css +++ b/assets/webconfig/css/hyperion.css @@ -10,11 +10,11 @@ body{font-family:Roboto,"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:15 padding-bottom:50px; } body{ - overflow-y: scroll; + overflow-y: scroll; } .btn{margin: 2px 0;} /* -#page-wrapper a[target=_blank]::after { +#page-wrapper a[target=_blank]::after { content:"\f08e"; position:relative; font:normal normal normal 10px/1 FontAwesome; @@ -109,7 +109,7 @@ table label{margin:0} .colorpicker-2x .colorpicker-saturation {width: 200px;height: 200px;} .colorpicker-2x .colorpicker-hue,.colorpicker-2x .colorpicker-alpha {width: 30px;height: 200px;} .colorpicker-2x .colorpicker-color,.colorpicker-2x .colorpicker-color div {height: 30px;} - + /*Hint*/ .info-hint{ background-color:rgb(236,236,236); @@ -123,7 +123,7 @@ table label{margin:0} box-shadow: 1px 1px 1px 0px rgba(0,0,0,0.25); font-size:97%; } - + /*Support page*/ .unlink,.unlink:hover{color:#333;text-decoration:none;} .support-container ul{padding-left:0px;list-style-type: none;} diff --git a/assets/webconfig/i18n/de.json b/assets/webconfig/i18n/de.json index 81f2329d..fe1bca99 100644 --- a/assets/webconfig/i18n/de.json +++ b/assets/webconfig/i18n/de.json @@ -33,6 +33,7 @@ "general_btn_ok": "OK", "general_btn_cancel": "Abbrechen", "general_btn_continue": "Fortfahren", + "general_btn_delete" : "Löschen", "general_btn_save": "Speichern", "general_btn_saverestart": "Speichern und neustarten", "general_btn_saveandreload": "Speichern und neu laden", @@ -99,6 +100,14 @@ "conf_general_impexp_l2": "Exportiere eine Konfiguration, indem du auf \"Exportieren\" klickst. Dein Browser startet einen Download.", "conf_general_impexp_impbtn": "Importieren", "conf_general_impexp_expbtn": "Exportieren", + "conf_general_tok_title" : "Token Management", + "conf_general_tok_desc" : "Tokens erlauben andere Anwendungen auf die Hyperion API zuzugreifen. Eine Anwendung kann ein Token anfordern welches von dir bestätigt werden muss oder du erstellst dir selbst ein neues Token. Diese Tokens werden nur benötigt, wenn \"API Autorisierung\" in den Netzwerkeinstellungen aktiviert ist.", + "conf_general_tok_cidhead" : "Beschreibung", + "conf_general_tok_lastuse" : "Zuletzt genutzt", + "conf_general_tok_comment_title" : "Token Beschreibung", + "conf_general_createToken_btn" : "Erstelle Token", + "conf_general_tok_diaTitle" : "Neues Token erstellt!", + "conf_general_tok_diaMsg" : "Hier ist dein neues Token, welches für den Zugriff auf die Hyperion API verwendet werden kann. Aus Sicherheitsgründen können Tokens nach der Erstellung nur einmalig eingesehen werden, notiere es dir daher jetzt.", "conf_helptable_option": "Option", "conf_helptable_expl": "Erklärung", "conf_effect_path_intro": "Hier kannst du Ordner angeben, die beim Laden von Effekten berücksichtig werden sollen. Zusätzlich können Effekte anhand ihres Namens deaktiviert werden um sie aus Listen zu löschen.", @@ -167,6 +176,7 @@ "conf_colors_color_intro": "Erstelle Kalibrierungsprofile die einzelnen Komponenten zugewisen werden können. Passe dabei Farben, Gamma, Helligkeit, Kompensation und mehr an.", "conf_colors_smoothing_intro": "Glätte den Farbverlauf und Helligkeitsänderungen um nicht von schnellen Übergängen abgelenkt zu werden.", "conf_colors_blackborder_intro": "Ignoriere schwarze Balken, jeder Modus nutzt einen anderen Algorithmus um diese zu erkennen. Erhöhe die Schwelle, sollte es nicht funktionieren.", + "conf_network_net_intro" : "Einstellungen zum Netzwerk die für alle Netzwerk-Dienste gelten", "conf_network_json_intro": "Der JSON-RPC-Port dieser Hyperion-Instanz, wird genutzt zur Fernsteuerung.", "conf_network_bobl_intro": "Boblight Empfänger", "conf_network_udpl_intro": "UDP Empfänger", @@ -575,6 +585,16 @@ "edt_conf_fw_proto_title": "Liste von Proto zielen", "edt_conf_fw_proto_expl": "Ein Proto Ziel pro Zeile. Bestehend aus IP:PORT (Beispiel: 127.0.0.1:19401)", "edt_conf_fw_proto_itemtitle": "Proto Ziel", + "edt_conf_net_heading_title" : "Network", + "edt_conf_net_internetAccessAPI_title":"Internet API Zugriff", + "edt_conf_net_internetAccessAPI_expl":"Erlaube Zugriff auf die Hyperion API/Webinterface aus dem Internet, deaktivieren für höhere Sicherheit.", + "edt_conf_net_ipWhitelist_title":"Erlaubte IP's", + "edt_conf_net_ipWhitelist_expl":"Anstatt den Zugriff für alle Verbindungen aus dem Internet zu erlauben kannst du hier Ausnahmen für zugelassene IP Adressen hinzufügen.", + "edt_conf_net_ip_itemtitle":"IP", + "edt_conf_net_apiAuth_title":"API Authentifizierung", + "edt_conf_net_apiAuth_expl":"Zwinge alle Anwendungen welche die Hyperion API nutzen sich zu authentifizieren. Aktivieren für höhere Sicherheit, da nun jede neue Anwendung einmalig von dir bestätigt werden muss.", + "edt_conf_net_localApiAuth_title" : "Lokale API Authentifizierung", + "edt_conf_net_localApiAuth_expl" : "Wenn aktiviert, müssen Verbindungen aus dem Heimnetzwerk mit einem Token authentifiziert werden.", "edt_conf_js_heading_title": "JSON Server", "edt_conf_fbs_heading_title": "Flatbuffers Server", "edt_conf_fbs_timeout_title": "Zeitüberschreitung", diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index bdec0fb5..4a72979d 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -32,6 +32,7 @@ "general_btn_yes" : "Yes", "general_btn_ok" : "OK", "general_btn_cancel" : "Cancel", + "general_btn_delete" : "Delete", "general_btn_continue" : "Continue", "general_btn_save" : "Save", "general_btn_saverestart" : "Save and restart", @@ -99,6 +100,14 @@ "conf_general_impexp_l2" : "Export a configuration by clicking on \"Export\". Your browser starts a download.", "conf_general_impexp_impbtn" : "Import", "conf_general_impexp_expbtn" : "Export", + "conf_general_tok_title" : "Token management", + "conf_general_tok_desc" : "Tokens grant other applications access to the Hyperion API, an application can request a token where you need to accept it or you create them on your own below. These tokens are just required when \"API Authorization\" is enabled in network settings.", + "conf_general_tok_cidhead" : "Description", + "conf_general_tok_lastuse" : "Last use", + "conf_general_tok_comment_title" : "Token description", + "conf_general_createToken_btn" : "Create Token", + "conf_general_tok_diaTitle" : "New Token created!", + "conf_general_tok_diaMsg" : "Here is your new token which can be used to grant an application access to the Hyperion API. For security reasons you can't view it again so use/note it now.", "conf_helptable_option" : "Option", "conf_helptable_expl" : "Explanation", "conf_effect_path_intro" : "Load effects from the defined paths. Additional you can disable single effects by name to hide them from all effect lists.", @@ -167,6 +176,7 @@ "conf_colors_color_intro" : "Create one or more calibration profiles, adjust each color, brightness, linearization and more.", "conf_colors_smoothing_intro" : "Smoothing flattens color/brightness changes to reduce annoying distraction.", "conf_colors_blackborder_intro" : "Skip black bars wherever they are. Each mode use another detection algorithm which is tuned for special situations. Higher the threshold if it doesn't work for you.", + "conf_network_net_intro" : "Network related settings which are applied to all network services.", "conf_network_json_intro" : "The JSON-RPC-Port of this Hyperion instance, used for remote control.", "conf_network_bobl_intro" : "Receiver for Boblight", "conf_network_udpl_intro" : "Receiver for UDP", @@ -575,6 +585,16 @@ "edt_conf_fw_proto_title" : "List of proto clients", "edt_conf_fw_proto_expl" : "One proto target per line. Contains IP:PORT (Example: 127.0.0.1:19401)", "edt_conf_fw_proto_itemtitle" : "Proto target", + "edt_conf_net_heading_title" : "Network", + "edt_conf_net_internetAccessAPI_title":"Internet API Access", + "edt_conf_net_internetAccessAPI_expl":"Allow access to the Hyperion API/Webinterface from the internet, disable for higher security.", + "edt_conf_net_ipWhitelist_title":"Whitelisted IP's", + "edt_conf_net_ipWhitelist_expl":"You can whitelist IP addresses instead allowing all connections from internet to connect to the Hyperion API/Webinterface.", + "edt_conf_net_ip_itemtitle":"IP", + "edt_conf_net_apiAuth_title":"API Authentication", + "edt_conf_net_apiAuth_expl":"Enforce all applications that use the Hyperion API to authenticate themself against Hyperion. Higher security, as you control the access and revoke it at any time.", + "edt_conf_net_localApiAuth_title" : "Local API Authentication", + "edt_conf_net_localApiAuth_expl" : "When enabled, connections from your home network needs to authenticate themself against Hyperion too.", "edt_conf_js_heading_title" : "JSON Server", "edt_conf_fbs_heading_title" : "Flatbuffers Server", "edt_conf_fbs_timeout_title" : "Timeout", diff --git a/assets/webconfig/js/content_general.js b/assets/webconfig/js/content_general.js index 614f2ba5..aad6df7c 100644 --- a/assets/webconfig/js/content_general.js +++ b/assets/webconfig/js/content_general.js @@ -25,6 +25,50 @@ $(document).ready( function() { requestWriteConfig(conf_editor.getValue()); }); + // Token handling + function buildTokenList() + { + console.log(tokenList) + $('.tktbody').html(""); + for(var key in tokenList) + { + var lastUse = (tokenList[key].last_use) ? tokenList[key].last_use : "-"; + var btn = ''; + $('.tktbody').append(createTableRow([tokenList[key].comment, lastUse, btn], false, true)); + $('#tok'+tokenList[key].id).off().on('click', handleDeleteToken); + } + } + + createTable('tkthead', 'tktbody', 'tktable'); + $('.tkthead').html(createTableRow([$.i18n('conf_general_tok_cidhead'), $.i18n('conf_general_tok_lastuse'), $.i18n('general_btn_delete')], true, true)); + buildTokenList(); + + function handleDeleteToken(e) + { + var key = e.currentTarget.id.replace("tok",""); + requestTokenDelete(key); + $('#tok'+key).parent().parent().remove(); + // rm deleted token id + tokenList = tokenList.filter(function( obj ) { + return obj.id !== key; + }); + } + + $('#btn_create_tok').off().on('click',function() { + requestToken($('#tok_comment').val()) + $('#tok_comment').val("") + $('#btn_create_tok').attr('disabled', true) + }); + $('#tok_comment').off().on('input',function(e) { + (e.currentTarget.value.length >= 10) ? $('#btn_create_tok').attr('disabled', false) : $('#btn_create_tok').attr('disabled', true); + }); + $(window.hyperion).off("cmd-authorize-createToken").on("cmd-authorize-createToken", function(event) { + var val = event.response.info; + showInfoDialog("newToken",$.i18n('conf_general_tok_diaTitle'),$.i18n('conf_general_tok_diaMsg')+'
'+val.token+'
') + tokenList.push(val) + buildTokenList() + }); + //import function dis_imp_btn(state) { @@ -104,6 +148,7 @@ $(document).ready( function() { //create introduction if(window.showOptHelp) createHint("intro", $.i18n('conf_general_intro'), "editor_container"); + createHint("intro", $.i18n('conf_general_tok_desc'), "tok_desc_cont"); removeOverlay(); }); diff --git a/assets/webconfig/js/content_index.js b/assets/webconfig/js/content_index.js index 59dd580b..5003eb12 100644 --- a/assets/webconfig/js/content_index.js +++ b/assets/webconfig/js/content_index.js @@ -34,6 +34,11 @@ $(document).ready( function() { updateSessions(); }); + $(window.hyperion).one("cmd-authorize-getTokenList", function(event) { + tokenList = event.response.info; + requestServerInfo(); + }); + $(window.hyperion).on("cmd-sysinfo", function(event) { requestServerInfo(); window.sysInfo = event.response.info; @@ -45,6 +50,7 @@ $(document).ready( function() { $(window.hyperion).one("cmd-config-getschema", function(event) { window.serverSchema = event.response.info; requestServerConfig(); + requestTokenInfo(); window.schema = window.serverSchema.properties; }); @@ -62,12 +68,16 @@ $(document).ready( function() { } }); + $(window.hyperion).one("cmd-authorize-login", function(event) { + requestServerConfigSchema(); + }); + $(window.hyperion).on("error",function(event){ showInfoDialog("error","Error", event.reason); }); $(window.hyperion).on("open",function(event){ - requestServerConfigSchema(); + requestAuthorization(); }); $(window.hyperion).one("ready", function(event) { diff --git a/assets/webconfig/js/content_network.js b/assets/webconfig/js/content_network.js index 68d14ed6..53764910 100644 --- a/assets/webconfig/js/content_network.js +++ b/assets/webconfig/js/content_network.js @@ -1,6 +1,7 @@ $(document).ready( function() { performTranslation(); + var conf_editor_net = null; var conf_editor_json = null; var conf_editor_proto = null; var conf_editor_fbs = null; @@ -10,6 +11,11 @@ $(document).ready( function() { if(window.showOptHelp) { + //network + $('#conf_cont').append(createRow('conf_cont_net')) + $('#conf_cont_net').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_net_heading_title"), 'editor_container_net', 'btn_submit_net')); + $('#conf_cont_net').append(createHelpTable(window.schema.network.properties, $.i18n("edt_conf_net_heading_title"))); + //jsonserver $('#conf_cont').append(createRow('conf_cont_json')) $('#conf_cont_json').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_js_heading_title"), 'editor_container_jsonserver', 'btn_submit_jsonserver')); @@ -46,6 +52,7 @@ $(document).ready( function() { else { $('#conf_cont').addClass('row'); + $('#conf_cont').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_net_heading_title"), 'editor_container_net', 'btn_submit_net')); $('#conf_cont').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_js_heading_title"), 'editor_container_jsonserver', 'btn_submit_jsonserver')); $('#conf_cont').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_fbs_heading_title"), 'editor_container_fbserver', 'btn_submit_fbserver')); $('#conf_cont').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_pbs_heading_title"), 'editor_container_protoserver', 'btn_submit_protoserver')); @@ -54,7 +61,20 @@ $(document).ready( function() { if(storedAccess != 'default') $('#conf_cont').append(createOptPanel('fa-sitemap', $.i18n("edt_conf_fw_heading_title"), 'editor_container_forwarder', 'btn_submit_forwarder')); } - + + // net + conf_editor_net = createJsonEditor('editor_container_net', { + network : window.schema.network + }, true, true); + + conf_editor_net.on('change',function() { + conf_editor_net.validate().length ? $('#btn_submit_net').attr('disabled', true) : $('#btn_submit_net').attr('disabled', false); + }); + + $('#btn_submit_net').off().on('click',function() { + requestWriteConfig(conf_editor_net.getValue()); + }); + //json conf_editor_json = createJsonEditor('editor_container_jsonserver', { jsonServer : window.schema.jsonServer @@ -139,6 +159,7 @@ $(document).ready( function() { //create introduction if(window.showOptHelp) { + createHint("intro", $.i18n('conf_network_net_intro'), "editor_container_net"); createHint("intro", $.i18n('conf_network_json_intro'), "editor_container_jsonserver"); createHint("intro", $.i18n('conf_network_fbs_intro'), "editor_container_fbserver"); createHint("intro", $.i18n('conf_network_proto_intro'), "editor_container_protoserver"); diff --git a/assets/webconfig/js/hyperion.js b/assets/webconfig/js/hyperion.js index dc6c9717..a92472fb 100644 --- a/assets/webconfig/js/hyperion.js +++ b/assets/webconfig/js/hyperion.js @@ -26,6 +26,7 @@ window.watchdog = 0; window.debugMessagesActive = true; window.wSess = []; window.comps = []; +tokenList = {}; function initRestart() { @@ -162,7 +163,31 @@ function sendToHyperion(command, subcommand, msg) // ----------------------------------------------------------- // wrapped server commands -// also used for watchdog +function requestAuthorization() +{ + sendToHyperion("authorize","login",'"username": "Hyperion", "password": "hyperion"'); +} + +function requestToken(comment) +{ + sendToHyperion("authorize","createToken",'"comment": "'+comment+'"'); +} + +function requestTokenInfo() +{ + sendToHyperion("authorize","getTokenList",""); +} + +function requestHandleTokenRequest(id, state) +{ + sendToHyperion("authorize","answerRequest",'"id":"'+id+'", "accept":'+state); +} + +function requestTokenDelete(id) +{ + sendToHyperion("authorize","deleteToken",'"id":"'+id+'"'); +} + function requestServerInfo() { sendToHyperion("serverinfo","",'"subscribe":["components-update","sessions-update","priorities-update", "imageToLedMapping-update", "adjustment-update", "videomode-update", "effects-update", "settings-update"]'); diff --git a/assets/webconfig/js/settings.js b/assets/webconfig/js/settings.js index 9278edb9..065fd223 100644 --- a/assets/webconfig/js/settings.js +++ b/assets/webconfig/js/settings.js @@ -125,6 +125,7 @@ $(document).ready( function() { for (var i = 0; i -1 && window.wSess[i].address.length == 36) @@ -133,6 +134,7 @@ $(document).ready( function() { hyperionAddress = 'http://'+window.wSess[i].address+':'+window.wSess[i].port $('#id_select').append(createSelOpt(hyperionAddress, window.wSess[i].host)) + } } $('#id_btn_saveset').off().on('click',function() { diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index 43422b63..50910db3 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -198,6 +198,17 @@ function showInfoDialog(type,header,message) $('#id_body').append(message); $('#id_footer').html(''); } + else if (type == "newToken") + { + $('#id_body').html('Redefine ambient light!'); + $('#id_footer').html(''); + } + else if (type == "grantToken") + { + $('#id_body').html('Redefine ambient light!'); + $('#id_footer').html(''); + $('#id_footer').append(''); + } $('#id_body').append('

'+header+'

'); $('#id_body').append(message); diff --git a/cmake/FindGitVersion.cmake b/cmake/FindGitVersion.cmake index 1cb1a7d0..b3cbf65d 100644 --- a/cmake/FindGitVersion.cmake +++ b/cmake/FindGitVersion.cmake @@ -7,5 +7,6 @@ STRING ( STRIP "${BUILD_ID}" BUILD_ID ) STRING ( STRIP "${VERSION_ID}" VERSION_ID ) STRING ( STRIP "${GIT_REMOTE_PATH}" GIT_REMOTE_PATH ) SET ( HYPERION_BUILD_ID "${VERSION_ID} (${BUILD_ID}) Git Remote: ${GIT_REMOTE_PATH}" ) -SET ( HYPERION_VERSION "${HYPERION_VERSION_CHANNEL}.${HYPERION_VERSION_MAJOR}.${HYPERION_VERSION_MINOR}.${HYPERION_VERSION_PATCH}" ) -message ( STATUS "Current Version: ${HYPERION_VERSION} (${HYPERION_BUILD_ID})" ) +SET ( HYPERION_VERSION "${HYPERION_VERSION_CHANNEL} ${HYPERION_VERSION_MAJOR}.${HYPERION_VERSION_MINOR}.${HYPERION_VERSION_PATCH}" ) +message ( STATUS "Current Version: ${HYPERION_VERSION}" ) +message ( STATUS " - Build: ${HYPERION_BUILD_ID}" ) diff --git a/cmake/packages.cmake b/cmake/packages.cmake index aaa25fe7..a9b9ba9b 100644 --- a/cmake/packages.cmake +++ b/cmake/packages.cmake @@ -13,11 +13,11 @@ ENDIF() find_package(RpmBuilder) find_package(DebBuilder) IF(RPM_BUILDER_FOUND) - message("CPACK: Found RPM builder") + message(STATUS "CPACK: Found RPM builder") SET ( CPACK_GENERATOR ${CPACK_GENERATOR} "RPM") ENDIF() IF(DEB_BUILDER_FOUND) - message("CPACK: Found DEB builder") + message(STATUS "CPACK: Found DEB builder") SET ( CPACK_GENERATOR ${CPACK_GENERATOR} "DEB") ENDIF() diff --git a/config/hyperion.config.json.commented b/config/hyperion.config.json.commented index 1559119a..409c3ac7 100644 --- a/config/hyperion.config.json.commented +++ b/config/hyperion.config.json.commented @@ -307,13 +307,22 @@ ] }, - "instCapture" : { + "instCapture" : + { "systemEnable" : true, "systemPriority" : 250, "v4lEnable" : false, "v4lPriority" : 240 }, + "network" : + { + "internetAccessAPI" : false, + "ipWhitelist" : [], + "apiAuth" : true, + "localApiAuth" : false + }, + /// Recreate and save led layouts made with web config. These values are just helpers for ui, not for Hyperion. "ledConfig" : { diff --git a/config/hyperion.config.json.default b/config/hyperion.config.json.default index dc078ec4..33e05044 100644 --- a/config/hyperion.config.json.default +++ b/config/hyperion.config.json.default @@ -173,13 +173,22 @@ "disable": [""] }, - "instCapture" : { + "instCapture" : + { "systemEnable" : true, "systemPriority" : 250, "v4lEnable" : false, "v4lPriority" : 240 }, + "network" : + { + "internetAccessAPI" : false, + "ipWhitelist" : [], + "apiAuth" : true, + "localApiAuth" : false + }, + "ledConfig" : { "top" : 8, diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt deleted file mode 100644 index 93601451..00000000 --- a/doc/CMakeLists.txt +++ /dev/null @@ -1,34 +0,0 @@ -option(BUILD_HYPERION_DOC "Build hyperion documentation" OFF) - -# Find doxygen and check if Doxygen is installed -find_package(Doxygen QUIET) - -if (BUILD_HYPERION_DOC) - if (DOXYGEN_FOUND) - - # set input and output files - set(DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/hyperion.in.doxygen) - set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/hyperion.doxygen) - - # request to configure the file - configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY) - message(STATUS "Doxygen build started") - - # Define all static (i.e. not generated) documentation files - set(StaticDocumentationFiles hyperion-footer.html) - - # Loop over all static documentation files - foreach(StaticDocumentationFile ${StaticDocumentationFiles}) - # Copy the file to the bindary documentation directory - configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${StaticDocumentationFile} ${CMAKE_CURRENT_BINARY_DIR}/html/${StaticDocumentationFile} COPYONLY) - endforeach() - - add_custom_target( doc_doxygen ALL - COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT} - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMENT "Generating API documentation with Doxygen" - VERBATIM ) - else(DOXYGEN_FOUND) - message(WARNING "Doxygen not found, unable to generate documenation!") - endif(DOXYGEN_FOUND) -endif() diff --git a/doc/hyperion-footer.html b/doc/hyperion-footer.html deleted file mode 100644 index dc33270e..00000000 --- a/doc/hyperion-footer.html +++ /dev/null @@ -1,8 +0,0 @@ -
- - - - -
Generated at $datetime for $projectname by doxygen $doxygenversion.
- - diff --git a/doc/hyperion.in.doxygen b/doc/hyperion.in.doxygen index 69c37750..4c3a983c 100644 --- a/doc/hyperion.in.doxygen +++ b/doc/hyperion.in.doxygen @@ -2234,7 +2234,7 @@ INTERACTIVE_SVG = NO # found. If left blank, it is assumed the dot tool can be found in the path. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_PATH = "" +DOT_PATH = # The DOTFILE_DIRS tag can be used to specify one or more directories that # contain dot files that are included in the documentation (see the \dotfile diff --git a/include/api/JsonAPI.h b/include/api/JsonAPI.h index fd268e0f..1997601f 100644 --- a/include/api/JsonAPI.h +++ b/include/api/JsonAPI.h @@ -12,6 +12,7 @@ #include class JsonCB; +class AuthManager; class JsonAPI : public QObject { @@ -24,16 +25,17 @@ public: /// @param peerAddress provide the Address of the peer /// @param log The Logger class of the creator /// @param parent Parent QObject + /// @param localConnection True when the sender has origin home network /// @param noListener if true, this instance won't listen for hyperion push events /// - JsonAPI(QString peerAddress, Logger* log, QObject* parent, bool noListener = false); + JsonAPI(QString peerAddress, Logger* log, const bool& localConnection, QObject* parent, bool noListener = false); /// /// Handle an incoming JSON message /// /// @param message the incoming message as string /// - void handleMessage(const QString & message); + void handleMessage(const QString & message, const QString& httpAuthHeader = ""); public slots: /// @@ -48,6 +50,24 @@ public slots: /// process and push new log messages from logger (if enabled) void incommingLogMessage(const Logger::T_LOG_MESSAGE&); +private slots: + /// + /// @brief Handle emits from AuthManager of new request, just _userAuthorized sessions are allowed to handle them + /// @param id The id of the request + /// @param The comment which needs to be accepted + /// + void handlePendingTokenRequest(const QString& id, const QString& comment); + + /// + /// @brief Handle emits from AuthManager of accepted/denied/timeouts token request, just if QObject matches with this instance we are allowed to send response. + /// @param success If true the request was accepted else false and no token was created + /// @param caller The origin caller instance who requested this token + /// @param token The new token that is now valid + /// @param comment The comment that was part of the request + /// @param id The id that was part of the request + /// + void handleTokenResponse(const bool& success, QObject* caller, const QString& token, const QString& comment, const QString& id); + signals: /// /// Signal emits with the reply message provided with handleMessage() @@ -60,6 +80,16 @@ signals: void forwardJsonMessage(QJsonObject); private: + /// Auth management pointer + AuthManager* _authManager; + + /// Reflect auth status of this client + bool _authorized; + bool _userAuthorized; + + /// Reflect auth required + bool _apiAuthRequired; + // true if further callbacks are forbidden (http) bool _noListener; @@ -214,6 +244,21 @@ private: /// void handleVideoModeCommand(const QJsonObject & message, const QString &command, const int tan); + /// Handle an incoming JSON plugin message + /// + /// @param message the incoming message + /// + void handleAuthorizeCommand(const QJsonObject & message, const QString &command, const int tan); + + /// + /// Handle HTTP on-the-fly token authorization + /// @param command The command + /// @param tan The tan + /// @param token The token to verify + /// @return True on succcess else false (pushes failed client feedback) + /// + const bool handleHTTPAuth(const QString& command, const int& tan, const QString& token); + /// /// Handle an incoming JSON Clearall message /// diff --git a/include/db/AuthTable.h b/include/db/AuthTable.h new file mode 100644 index 00000000..1332869c --- /dev/null +++ b/include/db/AuthTable.h @@ -0,0 +1,226 @@ +#pragma once + +// hyperion +#include +#include + +// qt +#include +#include + +/// +/// @brief Authentication table interface +/// +class AuthTable : public DBManager +{ + +public: + /// construct wrapper with auth table + AuthTable(const QString& rootPath, QObject* parent = nullptr) + : DBManager(parent) + { + // Init Hyperion database usage + setRootPath(rootPath); + setDB("hyperion"); + + // init Auth table + setTable("auth"); + // create table columns + createTable(QStringList()<<"user TEXT"<<"password BLOB"<<"token BLOB"<<"salt BLOB"<<"comment TEXT"<<"id TEXT"<<"created_at TEXT"<<"last_use TEXT"); + }; + ~AuthTable(){}; + + /// + /// @brief Create a user record, if called on a existing user the auth is recreated + /// @param[in] user The username + /// @param[in] pw The password + /// @return true on success else false + /// + inline bool createUser(const QString& user, const QString& pw) + { + // new salt + QByteArray salt = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex(); + QVariantMap map; + map["user"] = user; + map["salt"] = salt; + map["password"] = hashPasswordWithSalt(pw,salt); + map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + VectorPair cond; + cond.append(CPair("user",user)); + return createRecord(cond, map); + } + + /// + /// @brief Test if user record exists + /// @param[in] user The user id + /// @return true on success else false + /// + inline bool userExist(const QString& user) + { + VectorPair cond; + cond.append(CPair("user",user)); + return recordExists(cond); + } + + /// + /// @brief Test if a user is authorized for access with given pw. + /// @param user The user name + /// @param pw The password + /// @return True on success else false + /// + inline bool isUserAuthorized(const QString& user, const QString& pw) + { + if(userExist(user) && (calcPasswordHashOfUser(user, pw) == getPasswordHashOfUser(user))) + { + updateUserUsed(user); + return true; + } + return false; + } + + /// + /// @brief Update 'last_use' column entry for the corresponding user + /// @param[in] user The user to search for + /// + inline void updateUserUsed(const QString& user) + { + QVariantMap map; + map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + VectorPair cond; + cond.append(CPair("user", user)); + updateRecord(cond, map); + } + + /// + /// @brief Test if token record exists, updates last_use on success + /// @param[in] token The token id + /// @return true on success else false + /// + inline bool tokenExist(const QString& token) + { + QVariantMap map; + map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + VectorPair cond; + cond.append(CPair("token", hashToken(token))); + if(recordExists(cond)) + { + // update it + createRecord(cond,map); + return true; + } + return false; + } + + /// + /// @brief Create a new token record with comment + /// @param[in] token The token id as plaintext + /// @param[in] comment The comment for the token (eg a human readable identifier) + /// @param[in] id The id for the token + /// @return true on success else false + /// + inline bool createToken(const QString& token, const QString& comment, const QString& id) + { + QVariantMap map; + map["comment"] = comment; + map["id"] = idExist(id) ? QUuid::createUuid().toString().remove("{").remove("}").left(5) : id; + map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + VectorPair cond; + cond.append(CPair("token", hashToken(token))); + return createRecord(cond, map); + } + + /// + /// @brief Delete token record by id + /// @param[in] id The token id + /// @return true on success else false + /// + inline bool deleteToken(const QString& id) + { + VectorPair cond; + cond.append(CPair("id", id)); + return deleteRecord(cond); + } + + /// + /// @brief Get all 'comment', 'last_use' and 'id' column entries + /// @return A vector of all lists + /// + inline const QVector getTokenList() + { + QVector results; + getRecords(results, QStringList() << "comment" << "id" << "last_use"); + + return results; + } + + /// + /// @brief Test if id exists + /// @param[in] id The id + /// @return true on success else false + /// + inline bool idExist(const QString& id) + { + + VectorPair cond; + cond.append(CPair("id", id)); + return recordExists(cond); + } + + /// + /// @brief Get the passwort hash of a user from db + /// @param user The user name + /// @return password as hash + /// + inline const QByteArray getPasswordHashOfUser(const QString& user) + { + QVariantMap results; + VectorPair cond; + cond.append(CPair("user", user)); + getRecord(cond, results, QStringList()<<"password"); + + return results["password"].toByteArray(); + } + + /// + /// @brief Calc the password hash of a user based on user name and password + /// @param user The user name + /// @param pw The password + /// @return The calced password hash + /// + inline const QByteArray calcPasswordHashOfUser(const QString& user, const QString& pw) + { + // get salt + QVariantMap results; + VectorPair cond; + cond.append(CPair("user", user)); + getRecord(cond, results, QStringList()<<"salt"); + + // calc + return hashPasswordWithSalt(pw,results["salt"].toByteArray()); + } + + /// + /// @brief Create a password hash of plaintex password + salt + /// @param pw The plaintext password + /// @param salt The salt + /// @return The password hash with salt + /// + inline const QByteArray hashPasswordWithSalt(const QString& pw, const QByteArray& salt) + { + return QCryptographicHash::hash(pw.toUtf8().append(salt), QCryptographicHash::Sha512).toHex(); + } + + /// + /// @brief Create a token hash + /// @param token The plaintext token + /// @return The token hash + /// + inline const QByteArray hashToken(const QString& token) + { + return QCryptographicHash::hash(token.toUtf8(), QCryptographicHash::Sha512).toHex(); + } +}; diff --git a/include/db/DBManager.h b/include/db/DBManager.h new file mode 100644 index 00000000..b0cffe54 --- /dev/null +++ b/include/db/DBManager.h @@ -0,0 +1,130 @@ +#pragma once + +#include +#include +#include +#include +#include + +class QSqlDatabase; +class QSqlQuery; + +typedef QPair CPair; +typedef QVector VectorPair; + +/// +/// @brief Database interface for SQLite3. +/// Inherit this class to create component specific methods based on this interface +/// Usage: setTable(name) once before you use read/write actions +/// To use another database use setDb(newDB) (defaults to "hyperion") +/// +/// Incompatible functions with SQlite3: +/// QSqlQuery::size() returns always -1 +/// +class DBManager : public QObject +{ + Q_OBJECT + +public: + DBManager(QObject* parent = nullptr); + ~DBManager(); + + /// set root path + void setRootPath(const QString& rootPath); + /// define the database to work with + void setDB(const QString& dbn); + /// set a table to work with + void setTable(const QString& table); + + /// get current database object set with setDB() + QSqlDatabase getDB() const; + + /// + /// @brief Create a table (if required) with the given columns. Older tables will be updated accordingly with missing columns + /// Does not delete or migrate old columns + /// @param[in] columns The columns of the table. Requires at least one entry! + /// @return True on success else false + /// + bool createTable(QStringList& columns) const; + + /// + /// @brief Create a column if the column already exists returns false and logs error + /// @param[in] column The column of the table + /// @return True on success else false + /// + bool createColumn(const QString& column) const; + + /// + /// @brief Check if at least one record exists in table with the conditions + /// @param[in] conditions The search conditions (WHERE) + /// @return True on success else false + /// + bool recordExists(const VectorPair& conditions) const; + + /// + /// @brief Create a new record in table when the conditions find no existing entry. Add additional key:value pairs in columns + /// DO NOT repeat column keys between 'conditions' and 'columns' as they will be merged on creation + /// @param[in] conditions conditions to search for, as a record may exist and should be updated instead (WHERE) + /// @param[in] columns columns to create or update (optional) + /// @return True on success else false + /// + bool createRecord(const VectorPair& conditions, const QVariantMap& columns = QVariantMap()) const; + + /// + /// @brief Update a record with conditions and additional key:value pairs in columns + /// @param[in] conditions conditions which rows should be updated (WHERE) + /// @param[in] columns columns to update + /// @return True on success else false + /// + bool updateRecord(const VectorPair& conditions, const QVariantMap& columns) const; + + /// + /// @brief Get data of a single record, multiple records are not supported + /// @param[in] conditions condition to search for (WHERE) + /// @param[out] results results of query + /// @param[in] tColumns target columns to search in (optional) if not provided returns all columns + /// @return True on success else false + /// + bool getRecord(const VectorPair& conditions, QVariantMap& results, const QStringList& tColumns = QStringList()) const; + + /// + /// @brief Get data of multiple records, you need to specify the columns. This search is without conditions. Good to grab all data from db + /// @param[in] conditions condition to search for (WHERE) + /// @param[out] results results of query + /// @param[in] tColumns target columns to search in (optional) if not provided returns all columns + /// @return True on success else false + /// + bool getRecords(QVector& results, const QStringList& tColumns = QStringList()) const; + + /// + /// @brief Delete a record determined by conditions + /// @param[in] conditions conditions of the row to delete it (WHERE) + /// @return True on success; on error or not found false + /// + bool deleteRecord(const VectorPair& conditions) const; + + /// + /// @brief Check if table exists in current database + /// @param[in] table The name of the table + /// @return True on success else false + /// + bool tableExists(const QString& table) const; + + /// + /// @brief Delete a table, fails silent (return true) if table is not found + /// @param[in] table The name of the table + /// @return True on success else false + /// + bool deleteTable(const QString& table) const; + +private: + + Logger* _log; + /// databse connection & file name, defaults to hyperion + QString _dbn = "hyperion"; + /// table in database + QString _table; + + /// addBindValue to query given by QVariantList + void doAddBindValue(QSqlQuery& query, const QVariantList& variants) const; +}; diff --git a/include/db/SettingsTable.h b/include/db/SettingsTable.h new file mode 100644 index 00000000..713848f0 --- /dev/null +++ b/include/db/SettingsTable.h @@ -0,0 +1,114 @@ +#pragma once + +// hyperion +#include + +// qt +#include +#include + +/// +/// @brief settings table db interface +/// +class SettingsTable : public DBManager +{ + +public: + /// construct wrapper with settings table + SettingsTable(const quint8& instance, QObject* parent = nullptr) + : DBManager(parent) + , _hyperion_inst(instance) + { + setTable("settings"); + // create table columns + createTable(QStringList()<<"type TEXT"<<"config TEXT"<<"hyperion_inst INTEGER"<<"updated_at TEXT"); + }; + ~SettingsTable(){}; + + /// + /// @brief Create or update a settings record + /// @param[in] type type of setting + /// @param[in] config The configuration data + /// @return true on success else false + /// + inline bool createSettingsRecord(const QString& type, const QString& config) const + { + QVariantMap map; + map["config"] = config; + map["updated_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isSettingGlobal(type)) + cond.append(CPair("AND hyperion_inst",_hyperion_inst)); + return createRecord(cond, map); + } + + /// + /// @brief Test if record exist, type can be global setting or local (instance) + /// @param[in] type type of setting + /// @param[in] hyperion_inst The instance of hyperion assigned (might be empty) + /// @return true on success else false + /// + inline bool recordExist(const QString& type) const + { + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isSettingGlobal(type)) + cond.append(CPair("AND hyperion_inst",_hyperion_inst)); + return recordExists(cond); + } + + /// + /// @brief Get 'config' column of settings entry as QJsonDocument + /// @param[in] type The settings type + /// @return The QJsonDocument + /// + inline QJsonDocument getSettingsRecord(const QString& type) const + { + QVariantMap results; + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isSettingGlobal(type)) + cond.append(CPair("AND hyperion_inst",_hyperion_inst)); + getRecord(cond, results, QStringList("config")); + return QJsonDocument::fromJson(results["config"].toByteArray()); + } + + /// + /// @brief Get 'config' column of settings entry as QString + /// @param[in] type The settings type + /// @return The QString + /// + inline QString getSettingsRecordString(const QString& type) const + { + QVariantMap results; + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isSettingGlobal(type)) + cond.append(CPair("AND hyperion_inst",_hyperion_inst)); + getRecord(cond, results, QStringList("config")); + return results["config"].toString(); + } + + inline bool isSettingGlobal(const QString& type) const + { + // list of global settings + QStringList list; + // server port services + list << "jsonServer" << "protoServer" << "flatbufServer" << "udpListener" << "webConfig" << "network" + // capture + << "framegrabber" << "grabberV4L2" + // other + << "logger"; + + return list.contains(type); + } + +private: + const quint8 _hyperion_inst; +}; diff --git a/include/flatbufserver/FlatBufferServer.h b/include/flatbufserver/FlatBufferServer.h index 5f01b7cf..f06a76d1 100644 --- a/include/flatbufserver/FlatBufferServer.h +++ b/include/flatbufserver/FlatBufferServer.h @@ -9,6 +9,8 @@ class QTcpServer; class FlatBufferClient; +class NetOrigin; + /// /// @brief A TcpServer to receive images of different formats with Google Flatbuffer @@ -56,6 +58,7 @@ private: private: QTcpServer* _server; + NetOrigin* _netOrigin; Logger* _log; int _timeout; quint16 _port; diff --git a/include/hyperion/AuthManager.h b/include/hyperion/AuthManager.h new file mode 100644 index 00000000..f783d2e4 --- /dev/null +++ b/include/hyperion/AuthManager.h @@ -0,0 +1,160 @@ +#pragma once + +#include +#include + +//qt +#include + +class AuthTable; +class QTimer; + +/// +/// @brief Manage the authorization of user and tokens. This class is created once as part of the HyperionDaemon +/// To work with the global instance use AuthManager::getInstance() +/// +class AuthManager : public QObject +{ + Q_OBJECT +private: + friend class HyperionDaemon; + /// constructor is private, can be called from HyperionDaemon + AuthManager(const QString& rootPath, QObject* parent = 0); + +public: + struct AuthDefinition{ + QString id; + QString comment; + QObject* caller; + uint64_t timeoutTime; + QString token; + QString lastUse; + }; + + /// + /// @brief Get all available token entries + /// + const QVector getTokenList(); + + /// + /// @brief Check authorization is required according to the user setting + /// @return True if authorization required else false + /// + const bool & isAuthRequired(); + + /// + /// @brief Check if authorization is required for local network connections + /// @return True if authorization required else false + /// + const bool & isLocalAuthRequired(); + + /// + /// @brief Create a new token and skip the usual chain + /// @param comment The comment that should be used for + /// @return The new Auth definition + /// + const AuthDefinition createToken(const QString& comment); + + /// + /// @brief Check if user is authorized + /// @param user The username + /// @param pw The password + /// @return True if authorized else false + /// + const bool isUserAuthorized(const QString& user, const QString& pw); + + /// + /// @brief Check if token is authorized + /// @param token The token + /// @return True if authorized else false + /// + const bool isTokenAuthorized(const QString& token); + + /// + /// @brief Generate a new pending token request with the provided comment and id as identifier helper + /// @param caller The QObject of the caller to deliver the reply + /// @param comment The comment as ident helper + /// @param id The id created by the caller + /// + void setNewTokenRequest(QObject* caller, const QString& comment, const QString& id); + + /// + /// @brief Accept a token request by id, generate token and inform token caller + /// @param id The id of the request + /// @return True on success, false if not found + /// + const bool acceptTokenRequest(const QString& id); + + /// + /// @brief Deny a token request by id, inform the requester + /// @param id The id of the request + /// @return True on success, false if not found + /// + const bool denyTokenRequest(const QString& id); + + /// + /// @brief Get pending requests + /// @return All pending requests + /// + const QMap getPendingRequests(); + + /// + /// @brief Delete a token by id + /// @param id The token id + /// @return True on success else false (or not found) + /// + const bool deleteToken(const QString& id); + + /// Pointer of this instance + static AuthManager* manager; + /// Get Pointer of this instance + static AuthManager* getInstance() { return manager; }; + +public slots: + /// + /// @brief Handle settings update from Hyperion Settingsmanager emit + /// @param type settings type from enum + /// @param config configuration object + /// + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); + +signals: + /// + /// @brief Emits whenever a new token Request has been created along with the id and comment + /// @param id The id of the request + /// @param comment The comment of the request + /// + void newPendingTokenRequest(const QString& id, const QString& comment); + + /// + /// @brief Emits when the user has accepted or denied a token + /// @param success If true the request was accepted else false and no token will be available + /// @param caller The origin caller instance who requested this token + /// @param token The new token that is now valid + /// @param comment The comment that was part of the request + /// @param id The id that was part of the request + /// + void tokenResponse(const bool& success, QObject* caller, const QString& token, const QString& comment, const QString& id); + +private: + /// Database interface for auth table + AuthTable* _authTable; + + /// All pending requests + QMap _pendingRequests; + + /// Reflect state of global auth + bool _authRequired; + + /// Reflect state of local auth + bool _localAuthRequired; + + /// Timer for counting against pendingRequest timeouts + QTimer* _timer; + +private slots: + /// + /// @brief Check timeout of pending requests + /// + void checkTimeout(); +}; diff --git a/include/hyperion/SettingsManager.h b/include/hyperion/SettingsManager.h index 91895428..43720887 100644 --- a/include/hyperion/SettingsManager.h +++ b/include/hyperion/SettingsManager.h @@ -7,6 +7,7 @@ #include class Hyperion; +class SettingsTable; /// /// @brief Manage the settings read write from/to config file, on settings changed will emit a signal to update components accordingly @@ -17,43 +18,38 @@ class SettingsManager : public QObject public: /// /// @brief Construct a settings manager and assign a hyperion instance - /// @params hyperion The parent hyperion instance - /// @params instance Instance number of Hyperion + /// @params instance Instance number of Hyperion + /// @params configFile The config file + /// @params hyperion The parent hyperion instance /// - SettingsManager(Hyperion* hyperion, const quint8& instance, const QString& configFile); - - /// - /// @brief Construct a settings manager for HyperionDaemon - /// - SettingsManager(const quint8& instance, const QString& configFile); - ~SettingsManager(); + SettingsManager(const quint8& instance, const QString& configFile, Hyperion* hyperion = nullptr); /// /// @brief Save a complete json config /// @param config The entire config object /// @param correct If true will correct json against schema before save - /// @return True on success else false + /// @return True on success else false /// bool saveSettings(QJsonObject config, const bool& correct = false); /// /// @brief get a single setting json from config - /// @param type The settings::type from enum - /// @return The requested json data as QJsonDocument + /// @param type The settings::type from enum + /// @return The requested json data as QJsonDocument /// const QJsonDocument getSetting(const settings::type& type); /// /// @brief get the full settings object of this instance (with global settings) - /// @return The requested json + /// @return The requested json /// const QJsonObject & getSettings() { return _qconfig; }; signals: /// /// @brief Emits whenever a config part changed. - /// @param type The settings type from enum - /// @param data The data as QJsonDocument + /// @param type The settings type from enum + /// @param data The data as QJsonDocument /// void settingsChanged(const settings::type& type, const QJsonDocument& data); @@ -64,6 +60,9 @@ private: /// Logger instance Logger* _log; + /// instance of database table interface + SettingsTable* _sTable; + /// the schema static QJsonObject schemaJson; diff --git a/include/protoserver/ProtoServer.h b/include/protoserver/ProtoServer.h index 25cbd5a7..179bebab 100644 --- a/include/protoserver/ProtoServer.h +++ b/include/protoserver/ProtoServer.h @@ -9,6 +9,7 @@ class QTcpServer; class ProtoClientConnection; +class NetOrigin; /// /// @brief This class creates a TCP server which accepts connections wich can then send @@ -58,6 +59,7 @@ private: private: QTcpServer* _server; + NetOrigin* _netOrigin; Logger* _log; int _timeout; quint16 _port; diff --git a/include/udplistener/UDPListener.h b/include/udplistener/UDPListener.h index 167ccb7d..317732d0 100644 --- a/include/udplistener/UDPListener.h +++ b/include/udplistener/UDPListener.h @@ -18,6 +18,7 @@ class BonjourServiceRegister; class QUdpSocket; +class NetOrigin; /// /// This class creates a UDP server which accepts connections from boblight clients. @@ -106,4 +107,7 @@ private: QHostAddress _listenAddress; uint16_t _listenPort; QAbstractSocket::BindFlag _bondage; + + /// Check Network Origin + NetOrigin* _netOrigin; }; diff --git a/include/utils/NetOrigin.h b/include/utils/NetOrigin.h new file mode 100644 index 00000000..e1b53013 --- /dev/null +++ b/include/utils/NetOrigin.h @@ -0,0 +1,54 @@ +#pragma once + +// qt +#include +#include + +// utils +#include +#include + +/// +/// @brief Checks the origin ip addresses for access allowed +/// +class NetOrigin : public QObject +{ + Q_OBJECT +private: + friend class HyperionDaemon; + NetOrigin(QObject* parent = 0, Logger* log = Logger::getInstance("NETWORK")); + +public: + /// + /// @brief Check if address is allowed to connect. The local address is the network adapter ip this connection comes from + /// @param address The peer address + /// @param local The local address of the socket (Differs based on NetworkAdapter IP or localhost) + /// @return True when allowed, else false + /// + bool accessAllowed(const QHostAddress& address, const QHostAddress& local); + + /// + /// @brief Check if address is in subnet of local + /// @return True or false + /// + bool isLocalAddress(const QHostAddress& address, const QHostAddress& local); + + static NetOrigin* getInstance(){ return instance; }; + static NetOrigin* instance; + +private slots: + /// + /// @brief Handle settings update from SettingsManager + /// @param type settingyType from enum + /// @param config configuration object + /// + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); + +private: + Logger* _log; + /// True when internet access is allowed + bool _internetAccessAllowed; + /// Whitelisted ip addresses + QList _ipWhitelist; + +}; diff --git a/include/webserver/WebServer.h b/include/webserver/WebServer.h index 573b53df..f4aaffc8 100644 --- a/include/webserver/WebServer.h +++ b/include/webserver/WebServer.h @@ -29,7 +29,7 @@ public: quint16 getPort() { return _port; }; /// check if server has been inited - bool isInited() { return _inited; }; + const bool isInited() { return _inited; }; /// /// @brief Set a new description, if empty the description is NotFound for clients diff --git a/libsrc/CMakeLists.txt b/libsrc/CMakeLists.txt index f858b74d..8493f0fe 100644 --- a/libsrc/CMakeLists.txt +++ b/libsrc/CMakeLists.txt @@ -18,5 +18,6 @@ add_subdirectory(utils) add_subdirectory(effectengine) add_subdirectory(grabber) add_subdirectory(webserver) +add_subdirectory(db) add_subdirectory(api) add_subdirectory(python) diff --git a/libsrc/api/JSONRPC_schema/schema-authorize.json b/libsrc/api/JSONRPC_schema/schema-authorize.json new file mode 100644 index 00000000..264ef991 --- /dev/null +++ b/libsrc/api/JSONRPC_schema/schema-authorize.json @@ -0,0 +1,44 @@ +{ + "type":"object", + "required":true, + "properties":{ + "command": { + "type" : "string", + "required" : true, + "enum" : ["authorize"] + }, + "subcommand" : { + "type" : "string", + "required" : true, + "enum" : ["requestToken","createToken","deleteToken","getTokenList","logout","login","required","answerRequest","getPendingRequests"] + }, + "tan" : { + "type" : "integer" + }, + "username": { + "type": "string", + "minLength" : 3 + }, + "password": { + "type": "string", + "minLength" : 8 + }, + "token": { + "type": "string", + "minLength" : 36 + }, + "comment" : { + "type" : "string", + "minLength" : 5 + }, + "id" : { + "type" : "string", + "minLength" : 5, + "maxLength" : 5 + }, + "accept" : { + "type" : "boolean" + } + }, + "additionalProperties": false +} diff --git a/libsrc/api/JSONRPC_schema/schema.json b/libsrc/api/JSONRPC_schema/schema.json index 26d77bf9..2c45a78d 100644 --- a/libsrc/api/JSONRPC_schema/schema.json +++ b/libsrc/api/JSONRPC_schema/schema.json @@ -5,7 +5,7 @@ "command": { "type" : "string", "required" : true, - "enum" : ["color", "image", "effect", "create-effect", "delete-effect", "serverinfo", "clear", "clearall", "adjustment", "sourceselect", "config", "componentstate", "ledcolors", "logging", "processing", "sysinfo", "videomode", "transform", "correction" , "temperature"] + "enum" : ["color", "image", "effect", "create-effect", "delete-effect", "serverinfo", "clear", "clearall", "adjustment", "sourceselect", "config", "componentstate", "ledcolors", "logging", "processing", "sysinfo", "videomode", "authorize", "transform", "correction" , "temperature"] } } } diff --git a/libsrc/api/JSONRPC_schemas.qrc b/libsrc/api/JSONRPC_schemas.qrc index 7595d1ee..421ca75b 100644 --- a/libsrc/api/JSONRPC_schemas.qrc +++ b/libsrc/api/JSONRPC_schemas.qrc @@ -18,6 +18,7 @@ JSONRPC_schema/schema-logging.json JSONRPC_schema/schema-processing.json JSONRPC_schema/schema-videomode.json + JSONRPC_schema/schema-authorize.json JSONRPC_schema/schema-hyperion-classic.json JSONRPC_schema/schema-hyperion-classic.json diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index 84fc31d6..ef97d2a1 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -35,10 +35,17 @@ // api includes #include +// auth manager +#include + using namespace hyperion; -JsonAPI::JsonAPI(QString peerAddress, Logger* log, QObject* parent, bool noListener) +JsonAPI::JsonAPI(QString peerAddress, Logger* log, const bool& localConnection, QObject* parent, bool noListener) : QObject(parent) + , _authManager(AuthManager::getInstance()) + , _authorized(false) + , _userAuthorized(false) + , _apiAuthRequired(_authManager->isAuthRequired()) , _noListener(noListener) , _peerAddress(peerAddress) , _log(log) @@ -50,6 +57,14 @@ JsonAPI::JsonAPI(QString peerAddress, Logger* log, QObject* parent, bool noListe { Q_INIT_RESOURCE(JSONRPC_schemas); + // if this is localConnection and network allows unauth locals, set authorized flag + if(_apiAuthRequired && localConnection) + _authorized = !_authManager->isLocalAuthRequired(); + + // setup auth interface + connect(_authManager, &AuthManager::newPendingTokenRequest, this, &JsonAPI::handlePendingTokenRequest); + connect(_authManager, &AuthManager::tokenResponse, this, &JsonAPI::handleTokenResponse); + // the JsonCB creates json messages you can subscribe to e.g. data change events; forward them to the parent client connect(_jsonCB, &JsonCB::newCallback, this, &JsonAPI::callbackMessage); @@ -57,7 +72,7 @@ JsonAPI::JsonAPI(QString peerAddress, Logger* log, QObject* parent, bool noListe connect(this, &JsonAPI::forwardJsonMessage, _hyperion, &Hyperion::forwardJsonMessage); } -void JsonAPI::handleMessage(const QString& messageString) +void JsonAPI::handleMessage(const QString& messageString, const QString& httpAuthHeader) { const QString ident = "JsonRpc@"+_peerAddress; QJsonObject message; @@ -84,6 +99,29 @@ void JsonAPI::handleMessage(const QString& messageString) } int tan = message["tan"].toInt(); + + // client auth before everything else but not for http + if (!_noListener && command == "authorize") + { + handleAuthorizeCommand(message, command, tan); + return; + } + + // on the fly auth available for http from http Auth header, on failure we return and auth handler sends a failure + if(_noListener && _apiAuthRequired && !_authorized) + { + // extract token from http header + QString cToken = httpAuthHeader.mid(5).trimmed(); + if(!handleHTTPAuth(command, tan, cToken)) + return; + } + + // on strong api auth you need a auth for all cmds + if(_apiAuthRequired && !_authorized) + { + sendErrorReply("No Authorization", command, tan); + return; + } // switch over all possible commands and handle them if (command == "color") handleColorCommand (message, command, tan); @@ -104,13 +142,14 @@ void JsonAPI::handleMessage(const QString& messageString) else if (command == "videomode") handleVideoModeCommand (message, command, tan); // BEGIN | The following commands are derecated but used to ensure backward compatibility with hyperion Classic remote control - else if (command == "clearall") handleClearallCommand(message, command, tan); + else if (command == "clearall") + handleClearallCommand(message, command, tan); else if (command == "transform" || command == "correction" || command == "temperature") sendErrorReply("The command " + command + "is deprecated, please use the Hyperion Web Interface to configure"); // END // handle not implemented commands - else handleNotImplemented (); + else handleNotImplemented(); } void JsonAPI::handleColorCommand(const QJsonObject& message, const QString& command, const int tan) @@ -949,6 +988,205 @@ void JsonAPI::handleVideoModeCommand(const QJsonObject& message, const QString & sendSuccessReply(command, tan); } +void JsonAPI::handleAuthorizeCommand(const QJsonObject & message, const QString &command, const int tan) +{ + const QString& subc = message["subcommand"].toString().trimmed(); + // catch test if auth is required + if(subc == "required") + { + QJsonObject req; + req["required"] = _apiAuthRequired; + sendSuccessDataReply(QJsonDocument(req), command+"-"+subc, tan); + return; + } + + // catch logout + if(subc == "logout") + { + _authorized = false; + _userAuthorized = false; + sendSuccessReply(command+"-"+subc, tan); + return; + } + + // token created from ui + if(subc == "createToken") + { + const QString& c = message["comment"].toString().trimmed(); + // for user authorized sessions + if(_userAuthorized) + { + AuthManager::AuthDefinition def = _authManager->createToken(c); + QJsonObject newTok; + newTok["comment"] = def.comment; + newTok["id"] = def.id; + newTok["token"] = def.token; + + sendSuccessDataReply(QJsonDocument(newTok), command+"-"+subc, tan); + return; + } + sendErrorReply("No Authorization",command+"-"+subc, tan); + return; + } + + // delete token + if(subc == "deleteToken") + { + const QString& did = message["id"].toString().trimmed(); + // for user authorized sessions + if(_userAuthorized) + { + _authManager->deleteToken(did); + sendSuccessReply(command+"-"+subc, tan); + return; + } + sendErrorReply("No Authorization",command+"-"+subc, tan); + return; + } + + // catch token request + if(subc == "requestToken") + { + const QString& comment = message["comment"].toString().trimmed(); + const QString& id = message["id"].toString().trimmed(); + _authManager->setNewTokenRequest(this, comment, id); + // client should wait for answer + return; + } + + // get pending token requests + if(subc == "getPendingRequests") + { + if(_userAuthorized) + { + QMap map = _authManager->getPendingRequests(); + QJsonArray arr; + for(const auto& entry : map) + { + QJsonObject obj; + obj["comment"] = entry.comment; + obj["id"] = entry.id; + obj["timeout"] = int(entry.timeoutTime - QDateTime::currentMSecsSinceEpoch()); + arr.append(obj); + } + sendSuccessDataReply(QJsonDocument(arr),command+"-"+subc, tan); + } + else + sendErrorReply("No Authorization", command+"-"+subc, tan); + + return; + } + + // accept token request + if(subc == "answerRequest") + { + const QString& id = message["id"].toString().trimmed(); + const bool& accept = message["accept"].toBool(false); + if(_userAuthorized) + { + if(accept) + _authManager->acceptTokenRequest(id); + else + _authManager->denyTokenRequest(id); + } + else + sendErrorReply("No Authorization", command+"-"+subc, tan); + + return; + } + // deny token request + if(subc == "acceptRequest") + { + const QString& id = message["id"].toString().trimmed(); + if(_userAuthorized) + { + _authManager->acceptTokenRequest(id); + } + else + sendErrorReply("No Authorization", command+"-"+subc, tan); + + return; + } + + // cath get token list + if(subc == "getTokenList") + { + if(_userAuthorized) + { + QVector defVect = _authManager->getTokenList(); + QJsonArray tArr; + for(const auto& entry : defVect) + { + QJsonObject subO; + subO["comment"] = entry.comment; + subO["id"] = entry.id; + subO["last_use"] = entry.lastUse; + + tArr.append(subO); + } + + sendSuccessDataReply(QJsonDocument(tArr),command+"-"+subc, tan); + return; + } + sendErrorReply("No Authorization",command+"-"+subc, tan); + return; + } + + // login + if(subc == "login") + { + // catch token auth + const QString& token = message["token"].toString().trimmed(); + + if(!token.isEmpty()) + { + if(token.count() >= 36) + { + if(_authManager->isTokenAuthorized(token)) + { + _authorized = true; + sendSuccessReply(command+"-"+subc, tan); + } + else + sendErrorReply("No Authorization", command+"-"+subc, tan); + } + else + sendErrorReply("Token is too short", command+"-"+subc, tan); + + return; + } + + // user & password + const QString& user = message["username"].toString().trimmed(); + const QString& password = message["password"].toString().trimmed(); + + if(user.count() >= 3 && password.count() >= 8) + { + if(_authManager->isUserAuthorized(user, password)) + { + _authorized = true; + _userAuthorized = true; + sendSuccessReply(command+"-"+subc, tan); + } + else + sendErrorReply("No Authorization", command+"-"+subc, tan); + } + else + sendErrorReply("User or password string too short", command+"-"+subc, tan); + } +} + +const bool JsonAPI::handleHTTPAuth(const QString& command, const int& tan, const QString& token) +{ + if(_authManager->isTokenAuthorized(token)) + { + _authorized = true; + return true; + } + sendErrorReply("No Authorization", command, tan); + return false; +} + void JsonAPI::handleClearallCommand(const QJsonObject& message, const QString& command, const int tan) { emit forwardJsonMessage(message); @@ -1093,3 +1331,35 @@ void JsonAPI::incommingLogMessage(const Logger::T_LOG_MESSAGE &msg) // send the result emit callbackMessage(_streaming_logging_reply); } + +void JsonAPI::handlePendingTokenRequest(const QString& id, const QString& comment) +{ + // just user sessions are allowed to react on this, to prevent that token authorized instances authorize new tokens on their own + if(_userAuthorized) + { + QJsonObject obj; + obj["command"] = "authorize-event"; + obj["comment"] = comment; + obj["id"] = id; + + emit callbackMessage(obj); + } +} + +void JsonAPI::handleTokenResponse(const bool& success, QObject* caller, const QString& token, const QString& comment, const QString& id) +{ + // if this is the requester, we send the reply + if(this == caller) + { + const QString cmd = "authorize-requestToken"; + QJsonObject result; + result["token"] = token; + result["comment"] = comment; + result["id"] = id; + + if(success) + sendSuccessDataReply(QJsonDocument(result), cmd); + else + sendErrorReply("Token request timeout or denied", cmd); + } +} diff --git a/libsrc/db/CMakeLists.txt b/libsrc/db/CMakeLists.txt new file mode 100644 index 00000000..e420fd00 --- /dev/null +++ b/libsrc/db/CMakeLists.txt @@ -0,0 +1,18 @@ +find_package(Qt5 COMPONENTS Sql REQUIRED) + +# Define the current source locations +SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/db) +SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/db) + +FILE ( GLOB DB_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) + +add_library(database + ${DB_SOURCES} +) + +target_link_libraries(database + hyperion + hyperion-utils + Qt5::Core + Qt5::Sql +) diff --git a/libsrc/db/DBManager.cpp b/libsrc/db/DBManager.cpp new file mode 100644 index 00000000..fb54cfbc --- /dev/null +++ b/libsrc/db/DBManager.cpp @@ -0,0 +1,408 @@ +#include + +#include +#include +#include +#include +#include + +// not in header because of linking +static QString _rootPath; + +DBManager::DBManager(QObject* parent) + : QObject(parent) + , _log(Logger::getInstance("DB")) +{ +} + +DBManager::~DBManager() +{ +} + +void DBManager::setRootPath(const QString& rootPath) +{ + _rootPath = rootPath; + // create directory + QDir().mkpath(_rootPath+"/db"); +} + +void DBManager::setDB(const QString& dbn) +{ + _dbn = dbn; + // new database connection if not found + if(!QSqlDatabase::contains(dbn)) + { + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE",dbn); + db.setDatabaseName(_rootPath+"/db/"+dbn+".db"); + if(!db.open()) + { + Error(_log, QSTRING_CSTR(db.lastError().text())); + throw std::runtime_error("Failed to open database connection!"); + } + } +} + +void DBManager::setTable(const QString& table) +{ + _table = table; +} + +QSqlDatabase DBManager::getDB() const +{ + QSqlDatabase db = QSqlDatabase::database(_dbn); + + if (db.isOpen() && db.isValid()) + { + return db; + } + else + { + db = QSqlDatabase::addDatabase("QSQLITE", _dbn); + db.setDatabaseName(_rootPath+"/db/"+_dbn+".db"); + } + + return db; +} + +bool DBManager::createRecord(const VectorPair& conditions, const QVariantMap& columns) const +{ + if(recordExists(conditions)) + { + // if there is no column data, return + if(columns.isEmpty()) + return true; + + if(!updateRecord(conditions, columns)) + return false; + + return true; + } + + QSqlDatabase idb = getDB(); + QSqlQuery query(idb); + query.setForwardOnly(true); + + QVariantList cValues; + QStringList prep; + QStringList placeh; + // prep merge columns & condition + QVariantMap::const_iterator i = columns.constBegin(); + while (i != columns.constEnd()) { + prep.append(i.key()); + cValues += i.value(); + placeh.append("?"); + + ++i; + } + for(const auto& pair : conditions) + { + // remove the condition statements + QString tmp = pair.first; + prep << tmp.remove("AND"); + cValues << pair.second; + placeh.append("?"); + } + query.prepare(QString("INSERT INTO %1 ( %2 ) VALUES ( %3 )").arg(_table,prep.join(", ")).arg(placeh.join(", "))); + // add column & condition values + doAddBindValue(query, cValues); + if(!query.exec()) + { + Error(_log, "Failed to create record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prep.join(", ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); + return false; + } + return true; +} + +bool DBManager::recordExists(const VectorPair& conditions) const +{ + if(conditions.isEmpty()) + return false; + + QSqlDatabase idb = getDB(); + QSqlQuery query(idb); + query.setForwardOnly(true); + + QStringList prepCond; + QVariantList bindVal; + prepCond << "WHERE"; + + for(const auto& pair : conditions) + { + prepCond << pair.first+"=?"; + bindVal << pair.second; + } + query.prepare(QString("SELECT * FROM %1 %2").arg(_table,prepCond.join(" "))); + doAddBindValue(query, bindVal); + if(!query.exec()) + { + Error(_log, "Failed recordExists(): '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); + return false; + } + + int entry = 0; + while (query.next()) { + entry++; + } + + if(entry) + return true; + + return false; +} + +bool DBManager::updateRecord(const VectorPair& conditions, const QVariantMap& columns) const +{ + QSqlDatabase idb = getDB(); + QSqlQuery query(idb); + query.setForwardOnly(true); + + QVariantList values; + QStringList prep; + + // prepare columns valus + QVariantMap::const_iterator i = columns.constBegin(); + while (i != columns.constEnd()) { + prep += i.key()+"=?"; + values += i.value(); + + ++i; + } + + // prepare condition values + QStringList prepCond; + QVariantList prepBindVal; + if(!conditions.isEmpty()) + prepCond << "WHERE"; + + for(const auto& pair : conditions) + { + prepCond << pair.first+"=?"; + prepBindVal << pair.second; + } + + query.prepare(QString("UPDATE %1 SET %2 %3").arg(_table,prep.join(", ")).arg(prepCond.join(" "))); + // add column values + doAddBindValue(query, values); + // add condition values + doAddBindValue(query, prepBindVal); + if(!query.exec()) + { + Error(_log, "Failed to update record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); + return false; + } + return true; +} + +bool DBManager::getRecord(const VectorPair& conditions, QVariantMap& results, const QStringList& tColumns) const +{ + QSqlDatabase idb = getDB(); + QSqlQuery query(idb); + query.setForwardOnly(true); + + QString sColumns("*"); + if(!tColumns.isEmpty()) + sColumns = tColumns.join(", "); + + // prep conditions + QStringList prepCond; + QVariantList bindVal; + if(!conditions.isEmpty()) + prepCond << "WHERE"; + + for(const auto& pair : conditions) + { + prepCond << pair.first+"=?"; + bindVal << pair.second; + } + query.prepare(QString("SELECT %1 FROM %2 %3").arg(sColumns,_table).arg(prepCond.join(" "))); + doAddBindValue(query, bindVal); + + if(!query.exec()) + { + Error(_log, "Failed to get record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); + return false; + } + + // go to first row + query.next(); + + QSqlRecord rec = query.record(); + for(int i = 0; i& results, const QStringList& tColumns) const +{ + QSqlDatabase idb = getDB(); + QSqlQuery query(idb); + query.setForwardOnly(true); + + QString sColumns("*"); + if(!tColumns.isEmpty()) + sColumns = tColumns.join(", "); + + query.prepare(QString("SELECT %1 FROM %2").arg(sColumns,_table)); + + if(!query.exec()) + { + Error(_log, "Failed to get records: '%s' in table: '%s' Error: %s", QSTRING_CSTR(sColumns), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); + return false; + } + + // iterate through all found records + while(query.next()) + { + QVariantMap entry; + QSqlRecord rec = query.record(); + for(int i = 0; i #include "FlatBufferClient.h" +// util +#include + // qt #include #include @@ -24,6 +27,7 @@ FlatBufferServer::~FlatBufferServer() void FlatBufferServer::initServer() { + _netOrigin = NetOrigin::getInstance(); connect(_server, &QTcpServer::newConnection, this, &FlatBufferServer::newConnection); // apply config @@ -58,11 +62,16 @@ void FlatBufferServer::newConnection() { if(QTcpSocket* socket = _server->nextPendingConnection()) { - Debug(_log, "New connection from %s", QSTRING_CSTR(socket->peerAddress().toString())); - FlatBufferClient *client = new FlatBufferClient(socket, _timeout, this); - // internal - connect(client, &FlatBufferClient::clientDisconnected, this, &FlatBufferServer::clientDisconnected); - _openConnections.append(client); + if(_netOrigin->accessAllowed(socket->peerAddress(), socket->localAddress())) + { + Debug(_log, "New connection from %s", QSTRING_CSTR(socket->peerAddress().toString())); + FlatBufferClient *client = new FlatBufferClient(socket, _timeout, this); + // internal + connect(client, &FlatBufferClient::clientDisconnected, this, &FlatBufferServer::clientDisconnected); + _openConnections.append(client); + } + else + socket->close(); } } } diff --git a/libsrc/hyperion/AuthManager.cpp b/libsrc/hyperion/AuthManager.cpp new file mode 100644 index 00000000..f0959539 --- /dev/null +++ b/libsrc/hyperion/AuthManager.cpp @@ -0,0 +1,164 @@ +#include + +// util +#include + +// qt +#include +#include + +AuthManager* AuthManager::manager = nullptr; + +AuthManager::AuthManager(const QString& rootPath, QObject* parent) + : QObject(parent) + , _authTable(new AuthTable(rootPath, this)) + , _pendingRequests() + , _authRequired(true) + , _timer(new QTimer(this)) +{ + AuthManager::manager = this; + + // setup timer + _timer->setInterval(1000); + connect(_timer, &QTimer::timeout, this, &AuthManager::checkTimeout); + + // init with default user and password + if(!_authTable->userExist("Hyperion")) + { + _authTable->createUser("Hyperion","hyperion"); + } +} + +const bool & AuthManager::isAuthRequired() +{ + return _authRequired; +} + +const bool & AuthManager::isLocalAuthRequired() +{ + return _localAuthRequired; +} + +const AuthManager::AuthDefinition AuthManager::createToken(const QString& comment) +{ + const QString token = QUuid::createUuid().toString().mid(1, 36); + const QString id = QUuid::createUuid().toString().mid(1, 36).left(5); + + _authTable->createToken(token, comment, id); + + AuthDefinition def; + def.comment = comment; + def.token = token; + def.id = id; + + return def; +} + +const QVector AuthManager::getTokenList() +{ + QVector vector = _authTable->getTokenList(); + QVector finalVec; + for(const auto& entry : vector) + { + AuthDefinition def; + def.comment = entry["comment"].toString(); + def.id = entry["id"].toString(); + def.lastUse = entry["last_use"].toString(); + + // don't add empty ids + if(!entry["id"].toString().isEmpty()) + finalVec.append(def); + } + return finalVec; +} + +const bool AuthManager::isUserAuthorized(const QString& user, const QString& pw) +{ + return _authTable->isUserAuthorized(user, pw); +} + +const bool AuthManager::isTokenAuthorized(const QString& token) +{ + return _authTable->tokenExist(token); +} + +void AuthManager::setNewTokenRequest(QObject* caller, const QString& comment, const QString& id) +{ + if(!_pendingRequests.contains(id)) + { + AuthDefinition newDef {id, comment, caller, uint64_t(QDateTime::currentMSecsSinceEpoch()+60000)}; + _pendingRequests[id] = newDef; + _timer->start(); + emit newPendingTokenRequest(id, comment); + } +} + +const bool AuthManager::acceptTokenRequest(const QString& id) +{ + if(_pendingRequests.contains(id)) + { + const QString token = QUuid::createUuid().toString().remove("{").remove("}"); + AuthDefinition def = _pendingRequests.take(id); + _authTable->createToken(token, def.comment, id); + emit tokenResponse(true, def.caller, token, def.comment, id); + return true; + } + return false; +} + +const bool AuthManager::denyTokenRequest(const QString& id) +{ + if(_pendingRequests.contains(id)) + { + AuthDefinition def = _pendingRequests.take(id); + emit tokenResponse(false, def.caller, QString(), def.comment, id); + return true; + } + return false; +} + +const QMap AuthManager::getPendingRequests() +{ + return _pendingRequests; +} + +const bool AuthManager::deleteToken(const QString& id) +{ + if(_authTable->deleteToken(id)) + { + //emit tokenDeleted(token); + return true; + } + return false; +} + +void AuthManager::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) +{ + if(type == settings::NETWORK) + { + const QJsonObject& obj = config.object(); + _authRequired = obj["apiAuth"].toBool(true); + _localAuthRequired = obj["localApiAuth"].toBool(false); + } +} + +void AuthManager::checkTimeout() +{ + const uint64_t now = QDateTime::currentMSecsSinceEpoch(); + + QMapIterator i(_pendingRequests); + while (i.hasNext()) + { + i.next(); + + const AuthDefinition& def = i.value(); + if(def.timeoutTime <= now) + { + emit tokenResponse(false, def.caller, QString(), def.comment, def.id); + _pendingRequests.remove(i.key()); + } + } + // abort if empty + if(_pendingRequests.isEmpty()) + _timer->stop(); +} diff --git a/libsrc/hyperion/CMakeLists.txt b/libsrc/hyperion/CMakeLists.txt index eb96a92d..e832b32f 100644 --- a/libsrc/hyperion/CMakeLists.txt +++ b/libsrc/hyperion/CMakeLists.txt @@ -25,5 +25,6 @@ target_link_libraries(hyperion bonjour boblightserver effectengine + database ${QT_LIBRARIES} ) diff --git a/libsrc/hyperion/Hyperion.cpp b/libsrc/hyperion/Hyperion.cpp index a2536000..77ba4d0c 100644 --- a/libsrc/hyperion/Hyperion.cpp +++ b/libsrc/hyperion/Hyperion.cpp @@ -71,7 +71,7 @@ Hyperion* Hyperion::getInstance() Hyperion::Hyperion(HyperionDaemon* daemon, const quint8& instance, const QString configFile, const QString rootPath) : _daemon(daemon) - , _settingsManager(new SettingsManager(this, instance, configFile)) + , _settingsManager(new SettingsManager(instance, configFile, this)) , _componentRegister(this) , _ledString(hyperion::createLedString(getSetting(settings::LEDS).array(), hyperion::createColorOrder(getSetting(settings::DEVICE).object()))) , _ledStringClone(hyperion::createLedStringClone(getSetting(settings::LEDS).array(), hyperion::createColorOrder(getSetting(settings::DEVICE).object()))) @@ -89,6 +89,9 @@ Hyperion::Hyperion(HyperionDaemon* daemon, const quint8& instance, const QString , _prevCompId(hyperion::COMP_INVALID) , _ledBuffer(_ledString.leds().size(), ColorRgb::BLACK) { + // forward settings changed to Hyperion + connect(_settingsManager, &SettingsManager::settingsChanged, this, &Hyperion::settingsChanged); + if (!_raw2ledAdjustment->verifyAdjustments()) Warning(_log, "At least one led has no color calibration, please add all leds from your led layout to an 'LED index' field!"); @@ -209,9 +212,6 @@ void Hyperion::handleSettingsUpdate(const settings::type& type, const QJsonDocum const QJsonArray leds = config.array(); -// // lock update() -// _lockUpdate = true; - // stop and cache all running effects, as effects depend heavily on ledlayout _effectEngine->cacheRunningEffects(); @@ -247,14 +247,10 @@ void Hyperion::handleSettingsUpdate(const settings::type& type, const QJsonDocum // start cached effects _effectEngine->startCachedEffects(); - -// // unlock -// _lockUpdate = false; } else if(type == settings::DEVICE) { QMutexLocker lock(&_changes); -// _lockUpdate = true; QJsonObject dev = config.object(); // handle hwLedCount update @@ -278,7 +274,6 @@ void Hyperion::handleSettingsUpdate(const settings::type& type, const QJsonDocum // do always reinit until the led devices can handle dynamic changes dev["currentLedCount"] = int(_hwLedCount); // Inject led count info _ledDeviceWrapper->createLedDevice(dev); -// _lockUpdate = false; } // update once to push single color sets / adjustments/ ledlayout resizes and update ledBuffer color update(); diff --git a/libsrc/hyperion/SettingsManager.cpp b/libsrc/hyperion/SettingsManager.cpp index a703602e..77844e6c 100644 --- a/libsrc/hyperion/SettingsManager.cpp +++ b/libsrc/hyperion/SettingsManager.cpp @@ -3,6 +3,7 @@ // util #include +#include // json schema process #include @@ -16,11 +17,11 @@ QJsonObject SettingsManager::schemaJson; -SettingsManager::SettingsManager(Hyperion* hyperion, const quint8& instance, const QString& configFile) +SettingsManager::SettingsManager(const quint8& instance, const QString& configFile, Hyperion* hyperion) : _hyperion(hyperion) , _log(Logger::getInstance("SettingsManager")) + , _sTable(new SettingsTable(instance, this)) { - connect(this, &SettingsManager::settingsChanged, _hyperion, &Hyperion::settingsChanged); // get schema if(schemaJson.isEmpty()) { @@ -34,11 +35,14 @@ SettingsManager::SettingsManager(Hyperion* hyperion, const quint8& instance, con throw std::runtime_error(error.what()); } } + // get default config QJsonObject defaultConfig; if(!JsonUtils::readFile(":/hyperion_default.config", defaultConfig, _log)) throw std::runtime_error("Failed to read default config"); +// TODO BEGIN - remove when database migration is done + Info(_log, "Selected configuration file: %s", QSTRING_CSTR(configFile)); QJsonSchemaChecker schemaCheckerT; schemaCheckerT.setSchema(schemaJson); @@ -70,76 +74,74 @@ SettingsManager::SettingsManager(Hyperion* hyperion, const quint8& instance, con throw std::runtime_error("ERROR: Can't save configuration file, aborting"); } - Debug(_log,"Settings database initialized") -} +// TODO END - remove when database migration is done -SettingsManager::SettingsManager(const quint8& instance, const QString& configFile) - : _hyperion(nullptr) - , _log(Logger::getInstance("SettingsManager")) -{ - Q_INIT_RESOURCE(resource); - // get schema - if(schemaJson.isEmpty()) + // transform json to string lists + QStringList keyList = defaultConfig.keys(); + QStringList defValueList; + for(const auto key : keyList) { - try + if(defaultConfig[key].isObject()) { - schemaJson = QJsonFactory::readSchema(":/hyperion-schema"); + defValueList << QString(QJsonDocument(defaultConfig[key].toObject()).toJson(QJsonDocument::Compact)); } - catch(const std::runtime_error& error) + else if(defaultConfig[key].isArray()) { - throw std::runtime_error(error.what()); + defValueList << QString(QJsonDocument(defaultConfig[key].toArray()).toJson(QJsonDocument::Compact)); } } - // get default config - QJsonObject defaultConfig; - if(!JsonUtils::readFile(":/hyperion_default.config", defaultConfig, _log)) - throw std::runtime_error("Failed to read default config"); - Info(_log, "Selected configuration file: %s", QSTRING_CSTR(configFile)); - QJsonSchemaChecker schemaCheckerT; - schemaCheckerT.setSchema(schemaJson); - - if(!JsonUtils::readFile(configFile, _qconfig, _log)) - throw std::runtime_error("Failed to load config!"); - - // validate config with schema and correct it if required - QPair validate = schemaCheckerT.validate(_qconfig); - - // errors in schema syntax, abort - if (!validate.second) + // fill database with default data if required + for(const auto key : keyList) { - foreach (auto & schemaError, schemaCheckerT.getMessages()) + QString val = defValueList.takeFirst(); + // prevent overwrite + if(!_sTable->recordExist(key)) + _sTable->createSettingsRecord(key,val); + } + + // need to validate all data in database constuct the entire data object + // TODO refactor schemaChecker to accept QJsonArray in validate(); QJsonDocument container? To validate them per entry... + QJsonObject dbConfig; + for(const auto key : keyList) + { + QJsonDocument doc = _sTable->getSettingsRecord(key); + if(doc.isArray()) + dbConfig[key] = doc.array(); + else + dbConfig[key] = doc.object(); + } + + // validate full dbconfig against schema, on error we need to rewrite entire table + QJsonSchemaChecker schemaChecker; + schemaChecker.setSchema(schemaJson); + QPair valid = schemaChecker.validate(dbConfig); + // check if our main schema syntax is IO + if (!valid.second) + { + foreach (auto & schemaError, schemaChecker.getMessages()) Error(_log, "Schema Syntax Error: %s", QSTRING_CSTR(schemaError)); - - throw std::runtime_error("ERROR: Hyperion schema has syntax errors!"); + throw std::runtime_error("The config schema has invalid syntax. This should never happen! Go fix it!"); } - // errors in configuration, correct it! - if (!validate.first) + if (!valid.first) { - Warning(_log,"Errors have been found in the configuration file. Automatic correction has been applied"); - _qconfig = schemaCheckerT.getAutoCorrectedConfig(_qconfig); + Info(_log,"Table upgrade required..."); + dbConfig = schemaChecker.getAutoCorrectedConfig(dbConfig); - foreach (auto & schemaError, schemaCheckerT.getMessages()) + foreach (auto & schemaError, schemaChecker.getMessages()) Warning(_log, "Config Fix: %s", QSTRING_CSTR(schemaError)); - if (!JsonUtils::write(configFile, _qconfig, _log)) - throw std::runtime_error("ERROR: Can't save configuration file, aborting"); + saveSettings(dbConfig); } + else + _qconfig = dbConfig; Debug(_log,"Settings database initialized") } -SettingsManager::~SettingsManager() -{ -} - const QJsonDocument SettingsManager::getSetting(const settings::type& type) { - QString key = settings::typeToString(type); - if(_qconfig[key].isObject()) - return QJsonDocument(_qconfig[key].toObject()); - else - return QJsonDocument(_qconfig[key].toArray()); + return _sTable->getSettingsRecord(settings::typeToString(type)); } bool SettingsManager::saveSettings(QJsonObject config, const bool& correct) @@ -168,25 +170,34 @@ bool SettingsManager::saveSettings(QJsonObject config, const bool& correct) return false; } - // compare old data with new data to emit/save changes accordingly - for(const auto key : config.keys()) - { - QString newData, oldData; - - _qconfig[key].isObject() - ? oldData = QString(QJsonDocument(_qconfig[key].toObject()).toJson(QJsonDocument::Compact)) - : oldData = QString(QJsonDocument(_qconfig[key].toArray()).toJson(QJsonDocument::Compact)); - - config[key].isObject() - ? newData = QString(QJsonDocument(config[key].toObject()).toJson(QJsonDocument::Compact)) - : newData = QString(QJsonDocument(config[key].toArray()).toJson(QJsonDocument::Compact)); - - if(oldData != newData) - emit settingsChanged(settings::stringToType(key), QJsonDocument::fromJson(newData.toLocal8Bit())); - } - - // store the current state + // store the new config _qconfig = config; + // extract keys and data + QStringList keyList = config.keys(); + QStringList newValueList; + for(const auto key : keyList) + { + if(config[key].isObject()) + { + newValueList << QString(QJsonDocument(config[key].toObject()).toJson(QJsonDocument::Compact)); + } + else if(config[key].isArray()) + { + newValueList << QString(QJsonDocument(config[key].toArray()).toJson(QJsonDocument::Compact)); + } + } + + // compare database data with new data to emit/save changes accordingly + for(const auto key : keyList) + { + QString data = newValueList.takeFirst(); + if(_sTable->getSettingsRecordString(key) != data) + { + _sTable->createSettingsRecord(key, data); + + emit settingsChanged(settings::stringToType(key), QJsonDocument::fromJson(data.toLocal8Bit())); + } + } return true; } diff --git a/libsrc/hyperion/hyperion.schema.json b/libsrc/hyperion/hyperion.schema.json index ca911742..ffd551d8 100644 --- a/libsrc/hyperion/hyperion.schema.json +++ b/libsrc/hyperion/hyperion.schema.json @@ -79,6 +79,10 @@ { "$ref": "schema-instCapture.json" }, + "network": + { + "$ref": "schema-network.json" + }, "ledConfig": { "$ref": "schema-ledConfig.json" diff --git a/libsrc/hyperion/resource.qrc b/libsrc/hyperion/resource.qrc index 575c2d48..187b8508 100644 --- a/libsrc/hyperion/resource.qrc +++ b/libsrc/hyperion/resource.qrc @@ -23,5 +23,6 @@ schema/schema-ledConfig.json schema/schema-leds.json schema/schema-instCapture.json + schema/schema-network.json diff --git a/libsrc/hyperion/schema/schema-network.json b/libsrc/hyperion/schema/schema-network.json new file mode 100644 index 00000000..9fba6d38 --- /dev/null +++ b/libsrc/hyperion/schema/schema-network.json @@ -0,0 +1,54 @@ +{ + "type" : "object", + "title" : "edt_conf_net_heading_title", + "required" : true, + "properties" : + { + "internetAccessAPI" : + { + "type" : "boolean", + "title" : "edt_conf_net_internetAccessAPI_title", + "required" : true, + "default" : false, + "propertyOrder" : 1 + }, + "ipWhitelist" : + { + "type" : "array", + "title" : "edt_conf_net_ipWhitelist_title", + "required" : true, + "items" : { + "type": "string", + "title" : "edt_conf_net_ip_itemtitle" + }, + "options": { + "dependencies": { + "internetAccessAPI": false + } + }, + "propertyOrder" : 2 + }, + "apiAuth" : + { + "type" : "boolean", + "title" : "edt_conf_net_apiAuth_title", + "required" : true, + "default" : true, + "propertyOrder" : 3 + }, + "localApiAuth" : + { + "type" : "boolean", + "title" : "edt_conf_net_localApiAuth_title", + "required" : true, + "default" : false, + "options": { + "dependencies": { + "apiAuth": true + } + }, + "propertyOrder" : 4 + } + }, + "additionalProperties" : false +} diff --git a/libsrc/jsonserver/JsonClientConnection.cpp b/libsrc/jsonserver/JsonClientConnection.cpp index 692de9a9..8ae4c372 100644 --- a/libsrc/jsonserver/JsonClientConnection.cpp +++ b/libsrc/jsonserver/JsonClientConnection.cpp @@ -6,20 +6,16 @@ #include #include -// websocket includes -#include "webserver/WebSocketClient.h" - -JsonClientConnection::JsonClientConnection(QTcpSocket *socket) +JsonClientConnection::JsonClientConnection(QTcpSocket *socket, const bool& localConnection) : QObject() , _socket(socket) - , _websocketClient(nullptr) , _receiveBuffer() , _log(Logger::getInstance("JSONCLIENTCONNECTION")) { connect(_socket, &QTcpSocket::disconnected, this, &JsonClientConnection::disconnected); connect(_socket, &QTcpSocket::readyRead, this, &JsonClientConnection::readRequest); // create a new instance of JsonAPI - _jsonAPI = new JsonAPI(socket->peerAddress().toString(), _log, this); + _jsonAPI = new JsonAPI(socket->peerAddress().toString(), _log, localConnection, this); // get the callback messages from JsonAPI and send it to the client connect(_jsonAPI,SIGNAL(callbackMessage(QJsonObject)),this,SLOT(sendMessage(QJsonObject))); } @@ -27,37 +23,21 @@ JsonClientConnection::JsonClientConnection(QTcpSocket *socket) void JsonClientConnection::readRequest() { _receiveBuffer += _socket->readAll(); - - // might be an old hyperion classic handshake request or raw socket data - if(_receiveBuffer.contains("Upgrade: websocket")) + // raw socket data, handling as usual + int bytes = _receiveBuffer.indexOf('\n') + 1; + while(bytes > 0) { - if(_websocketClient == Q_NULLPTR) - { - // disconnect this slot from socket for further requests - disconnect(_socket, &QTcpSocket::readyRead, this, &JsonClientConnection::readRequest); - int start = _receiveBuffer.indexOf("Sec-WebSocket-Key") + 19; - QByteArray header(_receiveBuffer.mid(start, _receiveBuffer.indexOf("\r\n", start) - start).data()); - _websocketClient = new WebSocketClient(header, _socket, this); - } - } - else - { - // raw socket data, handling as usual - int bytes = _receiveBuffer.indexOf('\n') + 1; - while(bytes > 0) - { - // create message string - QString message(QByteArray(_receiveBuffer.data(), bytes)); + // create message string + QString message(QByteArray(_receiveBuffer.data(), bytes)); - // remove message data from buffer - _receiveBuffer = _receiveBuffer.mid(bytes); + // remove message data from buffer + _receiveBuffer = _receiveBuffer.mid(bytes); - // handle message - _jsonAPI->handleMessage(message); + // handle message + _jsonAPI->handleMessage(message); - // try too look up '\n' again - bytes = _receiveBuffer.indexOf('\n') + 1; - } + // try too look up '\n' again + bytes = _receiveBuffer.indexOf('\n') + 1; } } diff --git a/libsrc/jsonserver/JsonClientConnection.h b/libsrc/jsonserver/JsonClientConnection.h index 0434bc5d..267e495d 100644 --- a/libsrc/jsonserver/JsonClientConnection.h +++ b/libsrc/jsonserver/JsonClientConnection.h @@ -10,7 +10,6 @@ class JsonAPI; class QTcpSocket; -class WebSocketClient; /// /// The Connection object created by \a JsonServer when a new connection is established @@ -24,7 +23,7 @@ public: /// Constructor /// @param socket The Socket object for this connection /// - JsonClientConnection(QTcpSocket * socket); + JsonClientConnection(QTcpSocket * socket, const bool& localConnection); signals: void connectionClosed(); @@ -42,7 +41,6 @@ private slots: private: QTcpSocket* _socket; - WebSocketClient* _websocketClient; /// new instance of JsonAPI JsonAPI * _jsonAPI; diff --git a/libsrc/jsonserver/JsonServer.cpp b/libsrc/jsonserver/JsonServer.cpp index 4b671d87..4fb12771 100644 --- a/libsrc/jsonserver/JsonServer.cpp +++ b/libsrc/jsonserver/JsonServer.cpp @@ -7,6 +7,7 @@ // bonjour include #include +#include // qt includes #include @@ -19,6 +20,7 @@ JsonServer::JsonServer(const QJsonDocument& config) , _server(new QTcpServer(this)) , _openConnections() , _log(Logger::getInstance("JSONSERVER")) + , _netOrigin(NetOrigin::getInstance()) { Debug(_log, "Created instance"); @@ -95,12 +97,17 @@ void JsonServer::newConnection() { if (QTcpSocket * socket = _server->nextPendingConnection()) { - Debug(_log, "New connection from: %s ",socket->localAddress().toString().toStdString().c_str()); - JsonClientConnection * connection = new JsonClientConnection(socket); - _openConnections.insert(connection); + if(_netOrigin->accessAllowed(socket->peerAddress(), socket->localAddress())) + { + Debug(_log, "New connection from: %s ",socket->localAddress().toString().toStdString().c_str()); + JsonClientConnection * connection = new JsonClientConnection(socket, _netOrigin->isLocalAddress(socket->peerAddress(), socket->localAddress())); + _openConnections.insert(connection); - // register slot for cleaning up after the connection closed - connect(connection, &JsonClientConnection::connectionClosed, this, &JsonServer::closedConnection); + // register slot for cleaning up after the connection closed + connect(connection, &JsonClientConnection::connectionClosed, this, &JsonServer::closedConnection); + } + else + socket->close(); } } } diff --git a/libsrc/leddevice/dev_net/ProviderUdp.cpp b/libsrc/leddevice/dev_net/ProviderUdp.cpp index c3eb4e12..0caf9367 100644 --- a/libsrc/leddevice/dev_net/ProviderUdp.cpp +++ b/libsrc/leddevice/dev_net/ProviderUdp.cpp @@ -53,7 +53,7 @@ bool ProviderUdp::init(const QJsonObject &deviceConfig) } _port = deviceConfig["port"].toInt(_port); - if ( (_port <= 0) || (_port > 65535) ) + if ( (_port <= 0) || (_port > MAX_PORT) ) { throw std::runtime_error("invalid target port"); } diff --git a/libsrc/leddevice/dev_net/ProviderUdp.h b/libsrc/leddevice/dev_net/ProviderUdp.h index b1532593..bc204c6d 100644 --- a/libsrc/leddevice/dev_net/ProviderUdp.h +++ b/libsrc/leddevice/dev_net/ProviderUdp.h @@ -9,6 +9,8 @@ class QUdpSocket; +#define MAX_PORT 65535 + /// /// The ProviderUdp implements an abstract base-class for LedDevices using UDP packets. /// @@ -54,6 +56,6 @@ protected: /// QUdpSocket * _udpSocket; QHostAddress _address; - quint16 _port; + ushort _port; QString _defaultHost; }; diff --git a/libsrc/protoserver/ProtoServer.cpp b/libsrc/protoserver/ProtoServer.cpp index 6c2d8152..d9f0a82d 100644 --- a/libsrc/protoserver/ProtoServer.cpp +++ b/libsrc/protoserver/ProtoServer.cpp @@ -1,6 +1,9 @@ #include #include "ProtoClientConnection.h" +// util +#include + // qt #include #include @@ -24,6 +27,7 @@ ProtoServer::~ProtoServer() void ProtoServer::initServer() { + _netOrigin = NetOrigin::getInstance(); connect(_server, &QTcpServer::newConnection, this, &ProtoServer::newConnection); // apply config @@ -58,11 +62,16 @@ void ProtoServer::newConnection() { if(QTcpSocket * socket = _server->nextPendingConnection()) { - Debug(_log, "New connection from %s", QSTRING_CSTR(socket->peerAddress().toString())); - ProtoClientConnection * client = new ProtoClientConnection(socket, _timeout, this); - // internal - connect(client, &ProtoClientConnection::clientDisconnected, this, &ProtoServer::clientDisconnected); - _openConnections.append(client); + if(_netOrigin->accessAllowed(socket->peerAddress(), socket->localAddress())) + { + Debug(_log, "New connection from %s", QSTRING_CSTR(socket->peerAddress().toString())); + ProtoClientConnection * client = new ProtoClientConnection(socket, _timeout, this); + // internal + connect(client, &ProtoClientConnection::clientDisconnected, this, &ProtoServer::clientDisconnected); + _openConnections.append(client); + } + else + socket->close(); } } } diff --git a/libsrc/udplistener/UDPListener.cpp b/libsrc/udplistener/UDPListener.cpp index 678735f4..6db31d5f 100644 --- a/libsrc/udplistener/UDPListener.cpp +++ b/libsrc/udplistener/UDPListener.cpp @@ -8,20 +8,24 @@ #include #include "HyperionConfig.h" +// utils includes +#include + // qt includes #include #include using namespace hyperion; -UDPListener::UDPListener(const QJsonDocument& config) : - QObject(), - _server(new QUdpSocket(this)), - _priority(0), - _timeout(0), - _log(Logger::getInstance("UDPLISTENER")), - _isActive(false), - _listenPort(0) +UDPListener::UDPListener(const QJsonDocument& config) + : QObject() + , _server(new QUdpSocket(this)) + , _priority(0) + , _timeout(0) + , _log(Logger::getInstance("UDPLISTENER")) + , _isActive(false) + , _listenPort(0) + , _netOrigin(NetOrigin::getInstance()) { // listen for component change connect(Hyperion::getInstance(), &Hyperion::componentStateChanged, this, &UDPListener::componentStateChanged); @@ -116,7 +120,9 @@ void UDPListener::readPendingDatagrams() quint16 senderPort; _server->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort); - processTheDatagram(&datagram, &sender); + + if(_netOrigin->accessAllowed(sender, _listenAddress)) + processTheDatagram(&datagram, &sender); } } diff --git a/libsrc/utils/NetOrigin.cpp b/libsrc/utils/NetOrigin.cpp new file mode 100644 index 00000000..1ba37ada --- /dev/null +++ b/libsrc/utils/NetOrigin.cpp @@ -0,0 +1,76 @@ +#include + +#include + +NetOrigin* NetOrigin::instance = nullptr; + +NetOrigin::NetOrigin(QObject* parent, Logger* log) + : QObject(parent) + , _log(log) + , _internetAccessAllowed(false) + , _ipWhitelist() +{ + NetOrigin::instance = this; +} + +bool NetOrigin::accessAllowed(const QHostAddress& address, const QHostAddress& local) +{ + if(_internetAccessAllowed) + return true; + + if(_ipWhitelist.contains(address)) // v4 and v6 + return true; + + if(!isLocalAddress(address, local)) + { + Warning(_log,"Client connection with IP address '%s' has been rejected! It's not whitelisted, access denied.",QSTRING_CSTR(address.toString())); + return false; + } + return true; +} + +bool NetOrigin::isLocalAddress(const QHostAddress& address, const QHostAddress& local) +{ + if(address.protocol() == QAbstractSocket::IPv4Protocol) + { + if(!address.isInSubnet(local, 24)) // 255.255.255.xxx; IPv4 0-32 + { + return false; + } + } + else if(address.protocol() == QAbstractSocket::IPv6Protocol) + { + if(!address.isInSubnet(local, 64)) // 2001:db8:abcd:0012:XXXX:XXXX:XXXX:XXXX; IPv6 0-128 + { + return false; + } + } + return true; +} + +void NetOrigin::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) +{ + if(type == settings::NETWORK) + { + const QJsonObject& obj = config.object(); + _internetAccessAllowed = obj["internetAccessAPI"].toBool(false); + + const QJsonArray& arr = obj["ipWhitelist"].toArray(); + _ipWhitelist.clear(); + + for(const auto& e : arr) + { + const QString& entry = e.toString(""); + if(entry.isEmpty()) + continue; + + QHostAddress host(entry); + if(host.isNull()) + { + Warning(_log,"The whitelisted IP address '%s' isn't valid! Skipped",QSTRING_CSTR(entry)); + continue; + } + _ipWhitelist << host; + } + } +} diff --git a/libsrc/webserver/CgiHandler.cpp b/libsrc/webserver/CgiHandler.cpp index 1824397c..f078c507 100644 --- a/libsrc/webserver/CgiHandler.cpp +++ b/libsrc/webserver/CgiHandler.cpp @@ -55,7 +55,6 @@ void CgiHandler::cmd_cfg_jsonserver() if ( _args.at(0) == "cfg_jsonserver" ) { quint16 jsonPort = 19444; - // send result as reply _reply->addHeader ("Content-Type", "text/plain" ); _reply->appendRawData (QByteArrayLiteral(":") % QString::number(jsonPort).toUtf8() ); diff --git a/libsrc/webserver/QtHttpClientWrapper.cpp b/libsrc/webserver/QtHttpClientWrapper.cpp index 04e22be3..3ce9a368 100644 --- a/libsrc/webserver/QtHttpClientWrapper.cpp +++ b/libsrc/webserver/QtHttpClientWrapper.cpp @@ -4,8 +4,8 @@ #include "QtHttpReply.h" #include "QtHttpServer.h" #include "QtHttpHeader.h" +#include "WebSocketClient.h" #include "WebJsonRpc.h" -#include "webserver/WebSocketClient.h" #include #include @@ -16,15 +16,16 @@ const QByteArray & QtHttpClientWrapper::CRLF = QByteArrayLiteral ("\r\n"); -QtHttpClientWrapper::QtHttpClientWrapper (QTcpSocket * sock, QtHttpServer * parent) - : QObject (parent) - , m_guid ("") - , m_parsingStatus (AwaitingRequest) - , m_sockClient (sock) - , m_currentRequest (Q_NULLPTR) - , m_serverHandle (parent) - , m_websocketClient(nullptr) - , m_webJsonRpc (nullptr) +QtHttpClientWrapper::QtHttpClientWrapper (QTcpSocket * sock, const bool& localConnection, QtHttpServer * parent) + : QObject (parent) + , m_guid ("") + , m_parsingStatus (AwaitingRequest) + , m_sockClient (sock) + , m_currentRequest (Q_NULLPTR) + , m_serverHandle (parent) + , m_localConnection(localConnection) + , m_websocketClient(nullptr) + , m_webJsonRpc (nullptr) { connect (m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived); } @@ -120,7 +121,7 @@ void QtHttpClientWrapper::onClientDataReceived (void) { { // disconnect this slot from socket for further requests disconnect(m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived); - m_websocketClient = new WebSocketClient(m_currentRequest->getHeader(QtHttpHeader::SecWebSocketKey), m_sockClient, this); + m_websocketClient = new WebSocketClient(m_currentRequest, m_sockClient, m_localConnection, this); } break; } @@ -149,7 +150,7 @@ void QtHttpClientWrapper::onClientDataReceived (void) { { if(m_webJsonRpc == Q_NULLPTR) { - m_webJsonRpc = new WebJsonRpc(m_currentRequest, m_serverHandle, this); + m_webJsonRpc = new WebJsonRpc(m_currentRequest, m_serverHandle, m_localConnection, this); } m_webJsonRpc->handleMessage(m_currentRequest); break; diff --git a/libsrc/webserver/QtHttpClientWrapper.h b/libsrc/webserver/QtHttpClientWrapper.h index 13a2438d..e15797d0 100644 --- a/libsrc/webserver/QtHttpClientWrapper.h +++ b/libsrc/webserver/QtHttpClientWrapper.h @@ -13,43 +13,44 @@ class WebSocketClient; class WebJsonRpc; class QtHttpClientWrapper : public QObject { - Q_OBJECT + Q_OBJECT public: - explicit QtHttpClientWrapper (QTcpSocket * sock, QtHttpServer * parent); + explicit QtHttpClientWrapper (QTcpSocket * sock, const bool& localConnection, QtHttpServer * parent); - static const char SPACE = ' '; - static const char COLON = ':'; - static const QByteArray & CRLF; + static const char SPACE = ' '; + static const char COLON = ':'; + static const QByteArray & CRLF; - enum ParsingStatus { - ParsingError = -1, - AwaitingRequest = 0, - AwaitingHeaders = 1, - AwaitingContent = 2, - RequestParsed = 3 - }; + enum ParsingStatus { + ParsingError = -1, + AwaitingRequest = 0, + AwaitingHeaders = 1, + AwaitingContent = 2, + RequestParsed = 3 + }; - QString getGuid (void); + QString getGuid (void); /// @brief Wrapper for sendReplyToClient(), handles m_parsingStatus and signal connect void sendToClientWithReply (QtHttpReply * reply); private slots: - void onClientDataReceived (void); + void onClientDataReceived (void); protected: - ParsingStatus sendReplyToClient (QtHttpReply * reply); + ParsingStatus sendReplyToClient (QtHttpReply * reply); protected slots: - void onReplySendHeadersRequested (void); - void onReplySendDataRequested (void); + void onReplySendHeadersRequested (void); + void onReplySendDataRequested (void); private: - QString m_guid; - ParsingStatus m_parsingStatus; - QTcpSocket * m_sockClient; - QtHttpRequest * m_currentRequest; - QtHttpServer * m_serverHandle; + QString m_guid; + ParsingStatus m_parsingStatus; + QTcpSocket * m_sockClient; + QtHttpRequest * m_currentRequest; + QtHttpServer * m_serverHandle; + const bool m_localConnection; WebSocketClient * m_websocketClient; WebJsonRpc * m_webJsonRpc; }; diff --git a/libsrc/webserver/QtHttpServer.cpp b/libsrc/webserver/QtHttpServer.cpp index 4a118f24..48661643 100644 --- a/libsrc/webserver/QtHttpServer.cpp +++ b/libsrc/webserver/QtHttpServer.cpp @@ -1,3 +1,4 @@ + #include "QtHttpServer.h" #include "QtHttpRequest.h" #include "QtHttpReply.h" @@ -5,158 +6,147 @@ #include +#include + const QString & QtHttpServer::HTTP_VERSION = QStringLiteral ("HTTP/1.1"); QtHttpServerWrapper::QtHttpServerWrapper (QObject * parent) - : QTcpServer (parent) - , m_useSsl (false) -{ -} + : QTcpServer (parent) + , m_useSsl (false) +{ } -QtHttpServerWrapper::~QtHttpServerWrapper (void) -{ -} +QtHttpServerWrapper::~QtHttpServerWrapper (void) { } void QtHttpServerWrapper::setUseSecure (const bool ssl) { - m_useSsl = ssl; + m_useSsl = ssl; } -void QtHttpServerWrapper::incomingConnection (qintptr handle) -{ - QTcpSocket * sock = (m_useSsl - ? new QSslSocket (this) - : new QTcpSocket (this)); - (sock->setSocketDescriptor (handle)) - ? addPendingConnection (sock) - : delete sock; +void QtHttpServerWrapper::incomingConnection (qintptr handle) { + QTcpSocket * sock = (m_useSsl + ? new QSslSocket (this) + : new QTcpSocket (this)); + if (sock->setSocketDescriptor (handle)) { + addPendingConnection (sock); + } + else { + delete sock; + } } QtHttpServer::QtHttpServer (QObject * parent) - : QObject (parent) - , m_useSsl (false) - , m_serverName (QStringLiteral ("The Qt5 HTTP Server")) + : QObject (parent) + , m_useSsl (false) + , m_serverName (QStringLiteral ("The Qt5 HTTP Server")) + , m_netOrigin (NetOrigin::getInstance()) { - m_sockServer = new QtHttpServerWrapper (this); - connect (m_sockServer, &QtHttpServerWrapper::newConnection, this, &QtHttpServer::onClientConnected); + m_sockServer = new QtHttpServerWrapper (this); + connect (m_sockServer, &QtHttpServerWrapper::newConnection, this, &QtHttpServer::onClientConnected); } -const QString & QtHttpServer::getServerName (void) const -{ - return m_serverName; +const QString & QtHttpServer::getServerName (void) const { + return m_serverName; } -quint16 QtHttpServer::getServerPort (void) const -{ - return m_sockServer->serverPort (); +quint16 QtHttpServer::getServerPort (void) const { + return m_sockServer->serverPort (); } -QString QtHttpServer::getErrorString (void) const -{ - return m_sockServer->errorString (); +QString QtHttpServer::getErrorString (void) const { + return m_sockServer->errorString (); } -void QtHttpServer::start (quint16 port) -{ +void QtHttpServer::start (quint16 port) { if(!m_sockServer->isListening()) - (m_sockServer->listen (QHostAddress::Any, port)) - ? emit started (m_sockServer->serverPort ()) - : emit error (m_sockServer->errorString ()); + { + if (m_sockServer->listen (QHostAddress::Any, port)) { + emit started (m_sockServer->serverPort ()); + } + else { + emit error (m_sockServer->errorString ()); + } + } } -void QtHttpServer::stop (void) -{ - if (m_sockServer->isListening ()) - { - m_sockServer->close (); - +void QtHttpServer::stop (void) { + if (m_sockServer->isListening ()) { + m_sockServer->close (); // disconnect clients const QList socks = m_socksClientsHash.keys(); for(auto sock : socks) { sock->close(); } - - emit stopped (); - } + emit stopped (); + } } -void QtHttpServer::setServerName (const QString & serverName) -{ - m_serverName = serverName; +void QtHttpServer::setServerName (const QString & serverName) { + m_serverName = serverName; } -void QtHttpServer::setUseSecure (const bool ssl) -{ - m_useSsl = ssl; - m_sockServer->setUseSecure (m_useSsl); +void QtHttpServer::setUseSecure (const bool ssl) { + m_useSsl = ssl; + m_sockServer->setUseSecure (m_useSsl); } -void QtHttpServer::setPrivateKey (const QSslKey & key) -{ - m_sslKey = key; +void QtHttpServer::setPrivateKey (const QSslKey & key) { + m_sslKey = key; } -void QtHttpServer::setCertificates (const QList & certs) -{ - m_sslCerts = certs; +void QtHttpServer::setCertificates (const QList & certs) { + m_sslCerts = certs; } -void QtHttpServer::onClientConnected (void) -{ - while (m_sockServer->hasPendingConnections ()) - { - if (QTcpSocket * sock = m_sockServer->nextPendingConnection ()) - { - connect (sock, &QTcpSocket::disconnected, this, &QtHttpServer::onClientDisconnected); - if (m_useSsl) +void QtHttpServer::onClientConnected (void) { + while (m_sockServer->hasPendingConnections ()) { + if (QTcpSocket * sock = m_sockServer->nextPendingConnection ()) { + if(m_netOrigin->accessAllowed(sock->peerAddress(), sock->localAddress())) { - if (QSslSocket * ssl = qobject_cast (sock)) - { - connect (ssl, SslErrorSignal (&QSslSocket::sslErrors), this, &QtHttpServer::onClientSslErrors); - connect (ssl, &QSslSocket::encrypted, this, &QtHttpServer::onClientSslEncrypted); - connect (ssl, &QSslSocket::peerVerifyError, this, &QtHttpServer::onClientSslPeerVerifyError); - connect (ssl, &QSslSocket::modeChanged, this, &QtHttpServer::onClientSslModeChanged); - ssl->setLocalCertificateChain (m_sslCerts); - ssl->setPrivateKey (m_sslKey); - ssl->setPeerVerifyMode (QSslSocket::AutoVerifyPeer); - ssl->startServerEncryption (); - } + connect (sock, &QTcpSocket::disconnected, this, &QtHttpServer::onClientDisconnected); + if (m_useSsl) { + if (QSslSocket * ssl = qobject_cast (sock)) { + connect (ssl, SslErrorSignal (&QSslSocket::sslErrors), this, &QtHttpServer::onClientSslErrors); + connect (ssl, &QSslSocket::encrypted, this, &QtHttpServer::onClientSslEncrypted); + connect (ssl, &QSslSocket::peerVerifyError, this, &QtHttpServer::onClientSslPeerVerifyError); + connect (ssl, &QSslSocket::modeChanged, this, &QtHttpServer::onClientSslModeChanged); + ssl->setLocalCertificateChain (m_sslCerts); + ssl->setPrivateKey (m_sslKey); + ssl->setPeerVerifyMode (QSslSocket::AutoVerifyPeer); + ssl->startServerEncryption (); + } + } + QtHttpClientWrapper * wrapper = new QtHttpClientWrapper (sock, m_netOrigin->isLocalAddress(sock->peerAddress(), sock->localAddress()), this); + m_socksClientsHash.insert (sock, wrapper); + emit clientConnected (wrapper->getGuid ()); } - QtHttpClientWrapper * wrapper = new QtHttpClientWrapper (sock, this); - m_socksClientsHash.insert (sock, wrapper); - emit clientConnected (wrapper->getGuid ()); - } - } + else + { + sock->close(); + } + } + } } -void QtHttpServer::onClientSslEncrypted (void) -{ +void QtHttpServer::onClientSslEncrypted (void) { } + +void QtHttpServer::onClientSslPeerVerifyError (const QSslError & err) { + Q_UNUSED (err) } -void QtHttpServer::onClientSslPeerVerifyError (const QSslError & err) -{ - Q_UNUSED (err) +void QtHttpServer::onClientSslErrors (const QList & errors) { + Q_UNUSED (errors) } -void QtHttpServer::onClientSslErrors (const QList & errors) -{ - Q_UNUSED (errors) +void QtHttpServer::onClientSslModeChanged (QSslSocket::SslMode mode) { + Q_UNUSED (mode) } -void QtHttpServer::onClientSslModeChanged (QSslSocket::SslMode mode) -{ - Q_UNUSED (mode) -} - -void QtHttpServer::onClientDisconnected (void) -{ - if (QTcpSocket * sockClient = qobject_cast (sender ())) - { - if (QtHttpClientWrapper * wrapper = m_socksClientsHash.value (sockClient, Q_NULLPTR)) - { - emit clientDisconnected (wrapper->getGuid ()); - wrapper->deleteLater (); - m_socksClientsHash.remove (sockClient); - } - } +void QtHttpServer::onClientDisconnected (void) { + if (QTcpSocket * sockClient = qobject_cast (sender ())) { + if (QtHttpClientWrapper * wrapper = m_socksClientsHash.value (sockClient, Q_NULLPTR)) { + emit clientDisconnected (wrapper->getGuid ()); + wrapper->deleteLater (); + m_socksClientsHash.remove (sockClient); + } + } } diff --git a/libsrc/webserver/QtHttpServer.h b/libsrc/webserver/QtHttpServer.h index 7cd383cf..965eb49e 100644 --- a/libsrc/webserver/QtHttpServer.h +++ b/libsrc/webserver/QtHttpServer.h @@ -17,72 +17,72 @@ class QTcpServer; class QtHttpRequest; class QtHttpReply; class QtHttpClientWrapper; +class NetOrigin; class QtHttpServerWrapper : public QTcpServer { - Q_OBJECT + Q_OBJECT public: - explicit QtHttpServerWrapper (QObject * parent = Q_NULLPTR); - virtual ~QtHttpServerWrapper (void); + explicit QtHttpServerWrapper (QObject * parent = Q_NULLPTR); + virtual ~QtHttpServerWrapper (void); - void setUseSecure (const bool ssl = true); + void setUseSecure (const bool ssl = true); protected: - void incomingConnection (qintptr handle) Q_DECL_OVERRIDE; + void incomingConnection (qintptr handle) Q_DECL_OVERRIDE; private: - bool m_useSsl; + bool m_useSsl; }; class QtHttpServer : public QObject { - Q_OBJECT + Q_OBJECT public: - explicit QtHttpServer (QObject * parent = Q_NULLPTR); + explicit QtHttpServer (QObject * parent = Q_NULLPTR); - static const QString & HTTP_VERSION; + static const QString & HTTP_VERSION; - typedef void (QSslSocket::* SslErrorSignal) (const QList &); + typedef void (QSslSocket::* SslErrorSignal) (const QList &); - const QString & getServerName (void) const; + const QString & getServerName (void) const; - quint16 getServerPort (void) const; - QString getErrorString (void) const; - - bool isListening(void) { return m_sockServer->isListening(); }; + quint16 getServerPort (void) const; + QString getErrorString (void) const; + const bool isListening() { return m_sockServer->isListening(); }; public slots: - void start (quint16 port = 0); - void stop (void); - void setServerName (const QString & serverName); - void setUseSecure (const bool ssl = true); - void setPrivateKey (const QSslKey & key); - void setCertificates (const QList & certs); + void start (quint16 port = 0); + void stop (void); + void setServerName (const QString & serverName); + void setUseSecure (const bool ssl = true); + void setPrivateKey (const QSslKey & key); + void setCertificates (const QList & certs); signals: - void started (quint16 port); - void stopped (void); - void error (const QString & msg); - void clientConnected (const QString & guid); - void clientDisconnected (const QString & guid); - void requestNeedsReply (QtHttpRequest * request, QtHttpReply * reply); + void started (quint16 port); + void stopped (void); + void error (const QString & msg); + void clientConnected (const QString & guid); + void clientDisconnected (const QString & guid); + void requestNeedsReply (QtHttpRequest * request, QtHttpReply * reply); private slots: - void onClientConnected (void); - void onClientDisconnected (void); - void onClientSslEncrypted (void); - void onClientSslPeerVerifyError (const QSslError & err); - void onClientSslErrors (const QList & errors); - void onClientSslModeChanged (QSslSocket::SslMode mode); + void onClientConnected (void); + void onClientDisconnected (void); + void onClientSslEncrypted (void); + void onClientSslPeerVerifyError (const QSslError & err); + void onClientSslErrors (const QList & errors); + void onClientSslModeChanged (QSslSocket::SslMode mode); private: - bool m_useSsl; - QSslKey m_sslKey; - QList m_sslCerts; - QString m_serverName; - QtHttpServerWrapper * m_sockServer; - QHash m_socksClientsHash; + bool m_useSsl; + QSslKey m_sslKey; + QList m_sslCerts; + QString m_serverName; + NetOrigin* m_netOrigin; + QtHttpServerWrapper * m_sockServer; + QHash m_socksClientsHash; }; #endif // QTHTTPSERVER_H - diff --git a/libsrc/webserver/StaticFileServing.cpp b/libsrc/webserver/StaticFileServing.cpp index 7b0141f9..bb0b45ae 100644 --- a/libsrc/webserver/StaticFileServing.cpp +++ b/libsrc/webserver/StaticFileServing.cpp @@ -23,7 +23,7 @@ StaticFileServing::StaticFileServing (QObject * parent) StaticFileServing::~StaticFileServing () { - delete _mimeDb; + } void StaticFileServing::setBaseUrl(const QString& url) diff --git a/libsrc/webserver/WebJsonRpc.cpp b/libsrc/webserver/WebJsonRpc.cpp index 85e16fa0..49c5932c 100644 --- a/libsrc/webserver/WebJsonRpc.cpp +++ b/libsrc/webserver/WebJsonRpc.cpp @@ -6,22 +6,23 @@ #include -WebJsonRpc::WebJsonRpc(QtHttpRequest* request, QtHttpServer* server, QtHttpClientWrapper* parent) +WebJsonRpc::WebJsonRpc(QtHttpRequest* request, QtHttpServer* server, const bool& localConnection, QtHttpClientWrapper* parent) : QObject(parent) , _server(server) , _wrapper(parent) , _log(Logger::getInstance("HTTPJSONRPC")) { const QString client = request->getClientInfo().clientAddress.toString(); - _jsonAPI = new JsonAPI(client, _log, this, true); + _jsonAPI = new JsonAPI(client, _log, localConnection, this, true); connect(_jsonAPI, &JsonAPI::callbackMessage, this, &WebJsonRpc::handleCallback); } void WebJsonRpc::handleMessage(QtHttpRequest* request) { + QByteArray header = request->getHeader("Authorization"); QByteArray data = request->getRawData(); _unlocked = true; - _jsonAPI->handleMessage(data); + _jsonAPI->handleMessage(data,header); } void WebJsonRpc::handleCallback(QJsonObject obj) diff --git a/libsrc/webserver/WebJsonRpc.h b/libsrc/webserver/WebJsonRpc.h index 06adb33b..867c3bfa 100644 --- a/libsrc/webserver/WebJsonRpc.h +++ b/libsrc/webserver/WebJsonRpc.h @@ -1,9 +1,7 @@ #pragma once -// utils includes #include -// qt includes #include class QtHttpServer; @@ -14,7 +12,7 @@ class JsonAPI; class WebJsonRpc : public QObject { Q_OBJECT public: - WebJsonRpc(QtHttpRequest* request, QtHttpServer* server, QtHttpClientWrapper* parent); + WebJsonRpc(QtHttpRequest* request, QtHttpServer* server, const bool& localConnection, QtHttpClientWrapper* parent); void handleMessage(QtHttpRequest* request); diff --git a/libsrc/webserver/WebServer.cpp b/libsrc/webserver/WebServer.cpp index c570736a..876a025c 100644 --- a/libsrc/webserver/WebServer.cpp +++ b/libsrc/webserver/WebServer.cpp @@ -13,11 +13,12 @@ WebServer::WebServer(const QJsonDocument& config, QObject * parent) - : QObject(parent) + : QObject(parent) , _config(config) , _log(Logger::getInstance("WEBSERVER")) , _server() { + } WebServer::~WebServer() diff --git a/libsrc/webserver/WebSocketClient.cpp b/libsrc/webserver/WebSocketClient.cpp index ebbfda52..d7ed480b 100644 --- a/libsrc/webserver/WebSocketClient.cpp +++ b/libsrc/webserver/WebSocketClient.cpp @@ -1,39 +1,37 @@ -#include "webserver/WebSocketClient.h" +#include "WebSocketClient.h" +#include "QtHttpRequest.h" +#include "QtHttpHeader.h" -// hyperion includes #include - -// JsonAPI includes #include -// qt includes #include #include #include #include -#include - -WebSocketClient::WebSocketClient(QByteArray socketKey, QTcpSocket* sock, QObject* parent) +WebSocketClient::WebSocketClient(QtHttpRequest* request, QTcpSocket* sock, const bool& localConnection, QObject* parent) : QObject(parent) , _socket(sock) - , _secWebSocketKey(socketKey) , _log(Logger::getInstance("WEBSOCKET")) +// , _hyperion(Hyperion::getInstance()) { // connect socket; disconnect handled from QtHttpServer connect(_socket, &QTcpSocket::readyRead , this, &WebSocketClient::handleWebSocketFrame); - const QString client = sock->peerAddress().toString(); + // QtHttpRequest contains all headers for handshake + QByteArray secWebSocketKey = request->getHeader(QtHttpHeader::SecWebSocketKey); + const QString client = request->getClientInfo().clientAddress.toString(); // Json processor - _jsonAPI = new JsonAPI(client, _log, this); + _jsonAPI = new JsonAPI(client, _log, localConnection, this); connect(_jsonAPI, &JsonAPI::callbackMessage, this, &WebSocketClient::sendMessage); Debug(_log, "New connection from %s", QSTRING_CSTR(client)); // do handshake - _secWebSocketKey += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - QByteArray hash = QCryptographicHash::hash(_secWebSocketKey, QCryptographicHash::Sha1).toBase64(); + secWebSocketKey += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + QByteArray hash = QCryptographicHash::hash(secWebSocketKey, QCryptographicHash::Sha1).toBase64(); QString data = QString("HTTP/1.1 101 Switching Protocols\r\n") @@ -113,14 +111,14 @@ void WebSocketClient::handleWebSocketFrame(void) if (_wsh.fin) { _onContinuation = false; -// if (_wsh.opCode == OPCODE::TEXT) -// { + if (_wsh.opCode == OPCODE::TEXT) + { _jsonAPI->handleMessage(QString(_wsReceiveBuffer)); -// } -// else -// { -// handleBinaryMessage(_wsReceiveBuffer); -// } + } + else + { + handleBinaryMessage(_wsReceiveBuffer); + } _wsReceiveBuffer.clear(); } } @@ -223,7 +221,6 @@ void WebSocketClient::sendClose(int status, QString reason) _socket->close(); } -/* void WebSocketClient::handleBinaryMessage(QByteArray &data) { //uint8_t priority = data.at(0); @@ -242,10 +239,9 @@ void WebSocketClient::handleBinaryMessage(QByteArray &data) image.resize(width, height); memcpy(image.memptr(), data.data()+4, imgSize); - _hyperion->registerInput(); - _hyperion->setInputImage(priority, image, duration_s*1000); + //_hyperion->registerInput(); + //_hyperion->setInputImage(priority, image, duration_s*1000); } -*/ qint64 WebSocketClient::sendMessage(QJsonObject obj) { diff --git a/include/webserver/WebSocketClient.h b/libsrc/webserver/WebSocketClient.h similarity index 87% rename from include/webserver/WebSocketClient.h rename to libsrc/webserver/WebSocketClient.h index b7a273d9..80e1491b 100644 --- a/include/webserver/WebSocketClient.h +++ b/libsrc/webserver/WebSocketClient.h @@ -1,16 +1,18 @@ #pragma once #include -#include "webserver/WebSocketUtils.h" -#include +#include "WebSocketUtils.h" class QTcpSocket; + +class QtHttpRequest; +class Hyperion; class JsonAPI; class WebSocketClient : public QObject { Q_OBJECT public: - WebSocketClient(QByteArray socketKey, QTcpSocket* sock, QObject* parent); + WebSocketClient(QtHttpRequest* request, QTcpSocket* sock, const bool& localConnection, QObject* parent); struct WebSocketHeader { @@ -23,13 +25,13 @@ public: private: QTcpSocket* _socket; - QByteArray _secWebSocketKey; Logger* _log; + Hyperion* _hyperion; JsonAPI* _jsonAPI; void getWsFrameHeader(WebSocketHeader* header); void sendClose(int status, QString reason = ""); -// void handleBinaryMessage(QByteArray &data); + void handleBinaryMessage(QByteArray &data); qint64 sendMessage_Raw(const char* data, quint64 size); qint64 sendMessage_Raw(QByteArray &data); QByteArray makeFrameHeader(quint8 opCode, quint64 payloadLength, bool lastFrame); diff --git a/include/webserver/WebSocketUtils.h b/libsrc/webserver/WebSocketUtils.h similarity index 100% rename from include/webserver/WebSocketUtils.h rename to libsrc/webserver/WebSocketUtils.h diff --git a/src/hyperion-remote/JsonConnection.cpp b/src/hyperion-remote/JsonConnection.cpp index 0d0abdfd..9a77a7c7 100644 --- a/src/hyperion-remote/JsonConnection.cpp +++ b/src/hyperion-remote/JsonConnection.cpp @@ -543,6 +543,25 @@ void JsonConnection::setVideoMode(QString videoMode) parseReply(reply); } +void JsonConnection::setToken(const QString &token) +{ + // create command + QJsonObject command; + command["command"] = QString("authorize"); + command["subcommand"] = QString("login"); + + if (token.size() < 36) + throw std::runtime_error("The given token length is too short."); + + command["token"] = token; + + // send command message + QJsonObject reply = sendMessage(command); + + // parse reply message + parseReply(reply); +} + QJsonObject JsonConnection::sendMessage(const QJsonObject & message) { // serialize message diff --git a/src/hyperion-remote/JsonConnection.h b/src/hyperion-remote/JsonConnection.h index 596936af..ca48e56f 100644 --- a/src/hyperion-remote/JsonConnection.h +++ b/src/hyperion-remote/JsonConnection.h @@ -171,6 +171,9 @@ public: // sets video mode 3D/2D void setVideoMode(QString videoMode); + // set the specified authorization token + void setToken(const QString &token); + private: /// diff --git a/src/hyperion-remote/hyperion-remote.cpp b/src/hyperion-remote/hyperion-remote.cpp index 898bce05..5514ad54 100644 --- a/src/hyperion-remote/hyperion-remote.cpp +++ b/src/hyperion-remote/hyperion-remote.cpp @@ -59,47 +59,51 @@ int main(int argc, char * argv[]) // create the option parser and initialize all parameters Parser parser("Application to send a command to hyperion using the Json interface"); - Option & argAddress = parser.add