Feat: Add Admin API (#617)

* Push progress

TODO: rework RESET, probably to main.cpp again

* resetPassword rework

* enable administration restriction

* add short cmd for userdata

* Js apis

* Refactor JsonCB class

* Add userToken Auth

* Feat: Close connection if ext clients when def pw is set

* Feat: Protect db against pw/token tests

* WebUi PW Support (#9)

* Initial WebUi Password Support

* Small changes

* Initial WebUi Password Support

* Small changes

* Basic WebUi Token support

* added "removeStorage", added uiLock, updated login page

* Small improvments

* Small change

* Fix: prevent downgrade of authorization

* Add translation for localAdminAuth

* Feat: Show always save button in led layout

* Revert "Feat: Show always save button in led layout"

This reverts commit caad1dfcde.

* Feat: Password change link in notification

* Fix: body padding modal overlap

* Feat: Add instance index to response on switch

* prevent schema error

Signed-off-by: Paulchen-Panther <Paulchen-Panter@protonmail.com>

* Feat: add pw save

* Feat: callout settings/pw replaced with notification
This commit is contained in:
brindosch 2019-09-17 21:33:46 +02:00 committed by GitHub
parent 04c3bc8cc9
commit 5e559627be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 8047 additions and 137 deletions

View File

@ -0,0 +1,35 @@
<div class="container" style="margin:20px auto;max-width:500px;">
<center>
<div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">Login</h3>
</div>
<div class="panel-body">
<form>
<div class="form-group">
<input name="password" class="form-control" type="password" id="password" placeholder="Password" autocomplete="off"/>
<input name="show_pw" type="checkbox" id="show_pw"/><label for="show_pw">Show/Hide Password</label>
</div>
<div class="form-group">
<button type="submit" class="btn btn-sm btn-success" id="btn_password" onclick="requestAuthorization(document.getElementById('password').value); return false;" disabled><i class="fa fa-fw fa-unlock"></i>Login</button>
</div>
</form>
</div>
</div>
</div>
</center>
</div>
<script>
removeOverlay();
$('#password').off().on('input',function(e) {
if(e.currentTarget.value.length >= 8)
$('#btn_password').removeAttr('disabled');
});
$('#show_pw').off().on('change',function(e) {
(e.currentTarget.checked ? $('#password').attr('type', 'text') : $('#password').attr('type', 'password'))
});
</script>

File diff suppressed because it is too large Load Diff

View File

@ -76,6 +76,8 @@
"dashboard_alert_message_confsave_success" : "Deine Hyperion Konfiguration wurde erfolgreich gespeichert. Deine Änderungen sind somit übernommen.",
"dashboard_message_global_setting_t": "Instanzunabhängige Einstellung",
"dashboard_message_global_setting": "Die Einstellungen auf dieser Seite sind instanzunabhängig. Änderungen werden global übernommen.",
"dashboard_message_default_password_t": "WebUi Standardpasswort gesetzt",
"dashboard_message_default_password": "Das Standardpasswort der WebUi ist gesetzt. Wir empfehlen dringend, dieses zu ändern.",
"dashboard_active_instance": "Ausgewählte Instanz",
"main_menu_dashboard_token": "Dashboard",
"main_menu_configuration_token": "Konfiguration",
@ -312,6 +314,8 @@
"infoDialog_effconf_created_text": "Der Effekt \"$1\" wurde erfolgreich erstellt!",
"InfoDialog_lang_title": "Spracheinstellung",
"InfoDialog_lang_text": "Sollte dir die Vorauswahl der automatischen Spracherkennung nicht gefallen, kannst du die Sprache hier manuell festlegen.",
"InfoDialog_changePassword_title" : "Ändere Passwort",
"InfoDialog_changePassword_success" : "Passwort erfolgreich gespeichert!",
"InfoDialog_access_title": "Einstellungsstufe",
"InfoDialog_access_text": "Je höher die Stufe je mehr Einstellungen und Funktionen stehen zur Verfügung. Empfohlen ist \"Standard\".",
"InfoDialog_nowrite_title": "Fehler beim Schreibzugriff!",
@ -612,6 +616,8 @@
"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_net_localAdminAuth_title":"Lokale Admin Authentifizierung",
"edt_conf_net_localAdminAuth_expl":"Wenn aktiviert, muss der Administrationszugriff aus dem Heimnetzwerk mit einem Passwort authentifiziert werden.",
"edt_conf_net_restirctedInternetAccessAPI_title" : "Auf IP's beschränken",
"edt_conf_net_restirctedInternetAccessAPI_expl": "Den Zugriff auf die API durch das Internet auf bestimmte IP's beschränken",
"edt_conf_js_heading_title": "JSON Server",

View File

@ -75,6 +75,8 @@
"dashboard_alert_message_confsave_success" : "Your Hyperion configuration has been saved successfully. Your changes are now active.",
"dashboard_message_global_setting_t": "Instance independent setting",
"dashboard_message_global_setting": "The settings on this page are not depending on a specific instance. Changes will be stored globally for all instances.",
"dashboard_message_default_password_t": "WebUi default password is set",
"dashboard_message_default_password": "The default password for the WebUi is set. We strongly recommend to change this.",
"dashboard_active_instance": "Selected instance",
"main_menu_dashboard_token" : "Dashboard",
"main_menu_configuration_token" : "Configuration",
@ -312,6 +314,8 @@
"InfoDialog_lang_title" : "Language setting",
"InfoDialog_lang_text" : "If you don't like the result of the automatic language detection you could overwrite it here.",
"InfoDialog_access_title" : "Settings level",
"InfoDialog_changePassword_title" : "Change Password",
"InfoDialog_changePassword_success" : "Password successfully saved!",
"InfoDialog_access_text" : "Depending on settings level you could adjust more options or get access to more features. Recommended is the \"Default\" level.",
"InfoDialog_nowrite_title" : "write permission error!",
"InfoDialog_nowrite_text" : "Hyperion can't write to your current loaded configuration file. Please repair the file permissions to proceed.",
@ -610,7 +614,9 @@
"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 (Exception see \"Local API Authentication\"). 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_net_localApiAuth_expl" : "When enabled, connections from your home network needs to authenticate themself against Hyperion with a token.",
"edt_conf_net_localAdminAuth_title":"Local Admin API Authentication",
"edt_conf_net_localAdminAuth_expl":"When enabled, administration access from your home network needs a password.",
"edt_conf_net_restirctedInternetAccessAPI_title" : "Restrict to IP's",
"edt_conf_net_restirctedInternetAccessAPI_expl": "You can restrict the access to the API through the internet to certain IP's.",
"edt_conf_js_heading_title" : "JSON Server",

View File

@ -92,7 +92,7 @@
</div>
<!-- /.navbar-header -->
<ul class="nav navbar-top-links navbar-right">
<ul class="nav navbar-top-links navbar-right" id="top-navbar">
<!-- Browser built in capture stream - streamer.js -->
<li class="dropdown" id="btn_streamer" style="display:none">
<!-- Hidden helpers -->
@ -170,8 +170,23 @@
</div>
</a>
</li>
<li class="divider"></li>
<li id="btn_changePassword">
<a>
<div>
<i class="fa fa-key fa-fw"></i>
<span data-i18n="InfoDialog_changePassword_title"></span>
</div>
</a>
</li>
</ul>
</li>
<!-- /.lock-ui -->
<li class="dropdown" id="btn_lock_ui" style="display:none">
<a>
<i class="fa fa-lock fa-fw"></i>
</a>
</li>
</ul>
<!-- /.navbar-top-left -->
@ -216,12 +231,6 @@
<h4 id="dashboard_active_instance_friendly_name"></h4>
</div>
</div>
<div id="hyperion_config_write_success_notify" style="display:none;padding:0 10px;margin:0">
<div class="bs-callout bs-callout-success">
<h4 data-i18n="dashboard_alert_message_confsave_success_t"></h4>
<span data-i18n="dashboard_alert_message_confsave_success"></span>
</div>
</div>
<div id="hyperion_global_setting_notify" style="display:none;padding:0 10px;margin:0">
<div class="bs-callout bs-callout-warning">
<h4 data-i18n="dashboard_message_global_setting_t"></h4>

View File

@ -72,23 +72,88 @@ $(document).ready( function() {
window.showOptHelp = window.serverConfig.general.showOptHelp;
});
$(window.hyperion).on("cmd-config-setconfig", function(event) {
if (event.response.success === true) {
$('#hyperion_config_write_success_notify').fadeIn().delay(5000).fadeOut();
showNotification('success', $.i18n('dashboard_alert_message_confsave_success'), $.i18n('dashboard_alert_message_confsave_success_t'))
}
});
$(window.hyperion).one("cmd-authorize-login", function(event) {
$("#main-nav").removeAttr('style')
$("#top-navbar").removeAttr('style')
if(window.defaultPasswordIsSet === true)
showNotification('warning', $.i18n('dashboard_message_default_password'), $.i18n('dashboard_message_default_password_t'), '<a style="cursor:pointer" onClick="changePassword()"> '+$.i18n('InfoDialog_changePassword_title')+'</a>')
else
//if logged on and pw != default show option to lock ui
$("#btn_lock_ui").removeAttr('style')
if (event.response.hasOwnProperty('info'))
setStorage("loginToken", event.response.info.token, true);
requestServerConfigSchema();
});
$(window.hyperion).on("cmd-authorize-newPassword", function(event) {
if (event.response.success === true){
showInfoDialog("success",$.i18n('InfoDialog_changePassword_success'));
// not necessarily true, but better than nothing
window.defaultPasswordIsSet = false;
}
});
$(window.hyperion).one("cmd-authorize-newPasswordRequired", function(event) {
var loginToken = getStorage("loginToken", true)
if (event.response.info.newPasswordRequired == true)
{
window.defaultPasswordIsSet = true;
if(loginToken)
requestTokenAuthorization(loginToken)
else
requestAuthorization('hyperion');
}
else
{
$("#main-nav").attr('style', 'display:none')
$("#top-navbar").attr('style', 'display:none')
if(loginToken)
requestTokenAuthorization(loginToken)
else
loadContentTo("#page-content", "login")
}
});
$(window.hyperion).one("cmd-authorize-adminRequired", function(event) {
//Check if a admin login is required.
//If yes: check if default pw is set. If no: go ahead to get server config and render page
if (event.response.info.adminRequired === true)
requestRequiresDefaultPasswortChange();
else
requestServerConfigSchema();
});
$(window.hyperion).on("error",function(event){
showInfoDialog("error","Error", event.reason);
//If we are getting an error "No Authorization" back with a set loginToken we will forward to new Login (Token is expired.
//e.g.: hyperiond was started new in the meantime)
if (event.reason == "No Authorization" && getStorage("loginToken", true))
{
removeStorage("loginToken", true);
requestRequiresAdminAuth();
}
else
{
showInfoDialog("error","Error", event.reason);
}
});
$(window.hyperion).on("open",function(event){
requestAuthorization();
requestRequiresAdminAuth();
});
$(window.hyperion).one("ready", function(event) {
@ -190,3 +255,8 @@ $(function(){
$(this).toggleClass('active inactive');
});
});
// hotfix body padding when bs modals overlap
$(document.body).on('hide.bs.modal,hidden.bs.modal', function () {
$('body').css('padding-right','0');
});

View File

@ -28,6 +28,7 @@ window.wSess = [];
window.currentHyperionInstance = 0;
window.currentHyperionInstanceName = "?";
window.comps = [];
window.defaultPasswordIsSet = null;
tokenList = {};
function initRestart()
@ -165,9 +166,30 @@ function sendToHyperion(command, subcommand, msg)
// -----------------------------------------------------------
// wrapped server commands
function requestAuthorization()
// Test if admin requires authentication
function requestRequiresAdminAuth()
{
sendToHyperion("authorize","login",'"username": "Hyperion", "password": "hyperion"');
sendToHyperion("authorize","adminRequired");
}
// Test if the default password needs to be changed
function requestRequiresDefaultPasswortChange()
{
sendToHyperion("authorize","newPasswordRequired");
}
// Change password
function requestChangePassword(oldPw, newPw)
{
sendToHyperion("authorize","newPassword",'"password": "'+oldPw+'", "newPassword":"'+newPw+'"');
}
function requestAuthorization(password)
{
sendToHyperion("authorize","login",'"password": "' + password + '"');
}
function requestTokenAuthorization(token)
{
sendToHyperion("authorize","login",'"token": "' + token + '"');
}
function requestToken(comment)

View File

@ -4,6 +4,26 @@ var availLang = ['en','de','es','it','cs'];
var availAccess = ['default','advanced','expert'];
//$.i18n.debug = true;
//Change Password
function changePassword(){
showInfoDialog('changePassword', $.i18n('InfoDialog_changePassword_title'));
// fill default pw if default is set
if(window.defaultPasswordIsSet)
$('#oldPw').val('hyperion')
$('#id_btn_ok').off().on('click',function() {
var oldPw = $('#oldPw').val();
var newPw = $('#newPw').val();
requestChangePassword(oldPw, newPw)
});
$('#newPw, #oldPw').off().on('input',function(e) {
($('#oldPw').val().length >= 8 && $('#newPw').val().length >= 8) ? $('#id_btn_ok').attr('disabled', false) : $('#id_btn_ok').attr('disabled', true);
});
}
$(document).ready( function() {
//i18n
@ -112,6 +132,17 @@ $(document).ready( function() {
$('#id_select').trigger('change');
});
// change pw btn
$('#btn_changePassword').off().on('click',function() {
changePassword();
});
//Lock Ui
$('#btn_lock_ui').off().on('click',function() {
removeStorage('loginToken', true);
location.replace('/');
});
//hide menu elements
if (storedAccess != 'expert')
$('#load_webconfig').toggle(false);

View File

@ -297,6 +297,15 @@ function showInfoDialog(type,header,message)
$('#id_footer_rename').html('<button type="button" id="id_btn_ok" class="btn btn-success" data-dismiss-modal="#modal_dialog_rename" disabled><i class="fa fa-fw fa-save"></i>'+$.i18n('general_btn_ok')+'</button>');
$('#id_footer_rename').append('<button type="button" class="btn btn-danger" data-dismiss="modal"><i class="fa fa-fw fa-close"></i>'+$.i18n('general_btn_cancel')+'</button>');
}
else if (type == "changePassword")
{
$('#id_body_rename').html('<i style="margin-bottom:20px" class="fa fa-key modal-icon-edit"><br>');
$('#id_body_rename').append('<h4>'+header+'</h4>');
$('#id_body_rename').append('<input class="form-control" id="oldPw" placeholder="Old" type="text"> <br />');
$('#id_body_rename').append('<input class="form-control" id="newPw" placeholder="New" type="text">');
$('#id_footer_rename').html('<button type="button" id="id_btn_ok" class="btn btn-success" data-dismiss-modal="#modal_dialog_rename" disabled><i class="fa fa-fw fa-save"></i>'+$.i18n('general_btn_ok')+'</button>');
$('#id_footer_rename').append('<button type="button" class="btn btn-danger" data-dismiss="modal"><i class="fa fa-fw fa-close"></i>'+$.i18n('general_btn_cancel')+'</button>');
}
else if (type == "checklist")
{
$('#id_body').html('<img style="margin-bottom:20px" src="img/hyperion/hyperionlogo.png" alt="Redefine ambient light!">');
@ -326,7 +335,7 @@ function showInfoDialog(type,header,message)
$('#id_body').append('<select id="id_select" class="form-control" style="margin-top:10px;width:auto;"></select>');
$(type == "renInst" ? "#modal_dialog_rename" : "#modal_dialog").modal({
$(type == "renInst" || type == "changePassword" ? "#modal_dialog_rename" : "#modal_dialog").modal({
backdrop : "static",
keyboard: false,
show: true
@ -534,8 +543,9 @@ function hexToRgb(hex) {
@param type Valid types are "info","success","warning","danger"
@param message The message to show
@param title A title (optional)
@param addhtml Add custom html to the notification end
*/
function showNotification(type, message, title="")
function showNotification(type, message, title="", addhtml="")
{
if(title == "")
{
@ -564,15 +574,19 @@ function showNotification(type, message, title="")
// settings
type: type,
animate: {
enter: 'animated fadeInRight',
exit: 'animated fadeOutRight'
enter: 'animated fadeInDown',
exit: 'animated fadeOutUp'
},
placement:{
align:'center'
},
mouse_over : 'pause',
template: '<div data-notify="container" class="bg-w col-xs-11 col-sm-3 bs-callout bs-callout-{0}" role="alert">' +
template: '<div data-notify="container" class="bg-w col-md-6 bs-callout bs-callout-{0}" role="alert">' +
'<button type="button" aria-hidden="true" class="close" data-notify="dismiss">×</button>' +
'<span data-notify="icon"></span> ' +
'<h4 data-notify="title">{1}</h4> ' +
'<span data-notify="message">{2}</span>' +
addhtml+
'<div class="progress" data-notify="progressbar">' +
'<div class="progress-bar progress-bar-{0}" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"></div>' +
'</div>' +

View File

@ -306,13 +306,21 @@
"v4lPriority" : 240
},
/// The configuration of the network security restrictions, contains the following items:
/// * internetAccessAPI : When true allows connection from internet to the API. When false it blocks all outside connections
/// * restirctedInternetAccessAPI : webui voodoo only - ignore it
/// * ipWhitelist : Whitelist ip addresses from the internet to allow access to the API
/// * apiAuth : When true the API requires authentication through tokens to use the API. Read also "localApiAuth"
/// * localApiAuth : When false connections from the local network don't require an API authentification.
/// * localAdminApiAuth : When false connections from the local network don't require an authentification for administration access.
"network" :
{
"internetAccessAPI" : false,
"restirctedInternetAccessAPI" : false,
"ipWhitelist" : [],
"apiAuth" : true,
"localApiAuth" : false
"localApiAuth" : false,
"localAdminAuth": true
},
/// Recreate and save led layouts made with web config. These values are just helpers for ui, not for Hyperion.

View File

@ -181,7 +181,8 @@
"restirctedInternetAccessAPI" : false,
"ipWhitelist" : [],
"apiAuth" : true,
"localApiAuth" : false
"localApiAuth" : false,
"localAdminAuth": true
},
"ledConfig" :

View File

@ -37,6 +37,11 @@ public:
///
void handleMessage(const QString & message, const QString& httpAuthHeader = "");
///
/// @brief Initialization steps
///
void initialize(void);
public slots:
///
/// @brief Is called whenever the current Hyperion instance pushes new led raw values (if enabled)
@ -92,6 +97,11 @@ signals:
///
void forwardJsonMessage(QJsonObject);
///
/// @brief The API might decide to block connections for security reasons, this emitter should close the socket
///
void forceClose();
private:
/// Auth management pointer
AuthManager* _authManager;
@ -112,6 +122,9 @@ private:
/// Log instance
Logger* _log;
/// Is this a local connection
bool _localConnection;
/// Hyperion instance manager
HyperionIManager* _instanceManager;
@ -323,4 +336,9 @@ private:
/// @param error String describing the error
///
void sendErrorReply(const QString & error, const QString &command="", const int tan=0);
///
/// @brief Kill all signal/slot connections to stop possible data emitter
///
void stopDataConnections(void);
};

View File

@ -23,25 +23,38 @@ class JsonCB : public QObject
Q_OBJECT
public:
JsonCB(Hyperion* hyperion, QObject* parent);
JsonCB(QObject* parent);
///
/// @brief Subscribe to future data updates given by cmd
/// @param cmd The cmd which will be subscribed for
/// @param cmd The cmd which will be subscribed for
/// @param unsubscribe Revert subscription
/// @return True on success, false if not found
///
bool subscribeFor(const QString& cmd);
bool subscribeFor(const QString& cmd, const bool & unsubscribe = false);
///
/// @brief Get all possible commands to subscribe for
/// @return The list of commands
///
QStringList getCommands() { return _availableCommands; };
///
/// @brief Get all subscribed commands
/// @return The list of commands
///
QStringList getSubscribedCommands() { return _subscribedCommands; };
///
/// @brief Reset subscriptions, disconnect all signals
///
void resetSubscriptions(void);
///
/// @brief Re-apply all current subs to a new Hyperion instance, the connections to the old instance will be dropped
///
void setSubscriptionsTo(Hyperion* hyperion);
signals:
///
/// @brief Emits whenever a new json mesage callback is ready to send

View File

@ -16,9 +16,14 @@ class AuthTable : public DBManager
public:
/// construct wrapper with auth table
AuthTable(QObject* parent = nullptr)
AuthTable(const QString& rootPath = "", QObject* parent = nullptr)
: DBManager(parent)
{
if(!rootPath.isEmpty()){
// Init Hyperion database usage
setRootPath(rootPath);
setDatabaseName("hyperion");
}
// init Auth table
setTable("auth");
// create table columns
@ -75,6 +80,82 @@ public:
return false;
}
///
/// @brief Test if a user token is authorized for access.
/// @param usr The user name
/// @param token The token
/// @return True on success else false
///
inline bool isUserTokenAuthorized(const QString& usr, const QString& token)
{
if(getUserToken(usr) == token.toUtf8())
{
updateUserUsed(usr);
return true;
}
return false;
}
///
/// @brief Update token of a user. It's an alternate login path which is replaced on startup. This token is NOT hashed(!)
/// @param user The user name
/// @return True on success else false
///
inline bool setUserToken(const QString& user)
{
QVariantMap map;
map["token"] = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex();
VectorPair cond;
cond.append(CPair("user", user));
return updateRecord(cond, map);
}
///
/// @brief Get token of a user. This token is NOT hashed(!)
/// @param user The user name
/// @return The token
///
inline const QByteArray getUserToken(const QString& user)
{
QVariantMap results;
VectorPair cond;
cond.append(CPair("user", user));
getRecord(cond, results, QStringList()<<"token");
return results["token"].toByteArray();
}
///
/// @brief update password of given user. The user should be tested (isUserAuthorized) to verify this change
/// @param user The user name
/// @param newPw The new password to set
/// @return True on success else false
///
inline bool updateUserPassword(const QString& user, const QString& newPw)
{
QVariantMap map;
map["password"] = calcPasswordHashOfUser(user, newPw);
VectorPair cond;
cond.append(CPair("user", user));
return updateRecord(cond, map);
}
///
/// @brief Reset password of Hyperion user !DANGER! Used in Hyperion main.cpp
/// @return True on success else false
///
inline bool resetHyperionUser()
{
QVariantMap map;
map["password"] = calcPasswordHashOfUser("Hyperion", "hyperion");
VectorPair cond;
cond.append(CPair("user", "Hyperion"));
return updateRecord(cond, map);
}
///
/// @brief Update 'last_use' column entry for the corresponding user
/// @param[in] user The user to search for

View File

@ -47,13 +47,38 @@ public:
/// @brief Check authorization is required according to the user setting
/// @return True if authorization required else false
///
bool & isAuthRequired();
const bool & isAuthRequired() { return _authRequired; };
///
/// @brief Check if authorization is required for local network connections
/// @return True if authorization required else false
///
bool & isLocalAuthRequired();
const bool & isLocalAuthRequired() { return _localAuthRequired; };
///
/// @brief Check if authorization is required for local network connections for admin access
/// @return True if authorization required else false
///
const bool & isLocalAdminAuthRequired() { return _localAdminAuthRequired; };
///
/// @brief Check if Hyperion user has default password
/// @return True if so, else false
///
const bool hasHyperionDefaultPw() { return isUserAuthorized("Hyperion","hyperion"); };
///
/// @brief Get the current valid token for user. Make sure this call is allowed!
/// @param For the defined user
/// @return The token
///
const QString getUserToken(const QString & usr = "Hyperion");
///
/// @brief Reset Hyperion user
/// @return True on success else false
///
bool resetHyperionUser();
///
/// @brief Create a new token and skip the usual chain
@ -77,6 +102,35 @@ public:
///
bool isTokenAuthorized(const QString& token);
///
/// @brief Check if token is authorized
/// @param usr The username
/// @param token The token
/// @return True if authorized else false
///
bool isUserTokenAuthorized(const QString& usr, const QString& token);
///
/// @brief Check if user auth is temporary blocked due to failed attempts
/// @return True on blocked and no further Auth requests will be accepted
///
bool isUserAuthBlocked(){ return (_userAuthAttempts.length() >= 10); };
///
/// @brief Check if token auth is temporary blocked due to failed attempts
/// @return True on blocked and no further Auth requests will be accepted
///
bool isTokenAuthBlocked(){ return (_tokenAuthAttempts.length() >= 25); };
///
/// @brief Change password of user
/// @param user The username
/// @param pw The CURRENT password
/// @param newPw The new password
/// @return True on success else false
///
bool updateUserPassword(const QString& user, const QString& pw, const QString& newPw);
///
/// @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
@ -144,6 +198,12 @@ signals:
void tokenResponse(const bool& success, QObject* caller, const QString& token, const QString& comment, const QString& id);
private:
///
/// @brief Increment counter for token/user auth
/// @param user If true we increment USER auth instead of token
///
void setAuthBlock(const bool& user = false);
/// Database interface for auth table
AuthTable* _authTable;
@ -162,12 +222,29 @@ private:
/// Reflect state of local auth
bool _localAuthRequired;
/// Reflect state of local admin auth
bool _localAdminAuthRequired;
/// Timer for counting against pendingRequest timeouts
QTimer* _timer;
// Timer which cleans up the block counter
QTimer* _authBlockTimer;
// Contains timestamps of failed user login attempts
QVector<uint64_t> _userAuthAttempts;
// Contains timestamps of failed token login attempts
QVector<uint64_t> _tokenAuthAttempts;
private slots:
///
/// @brief Check timeout of pending requests
///
void checkTimeout();
///
/// @brief Check if there are timeouts for failed login attempts
///
void checkAuthBlockTimeout();
};

View File

@ -10,19 +10,19 @@
"subcommand" : {
"type" : "string",
"required" : true,
"enum" : ["requestToken","createToken","deleteToken","getTokenList","logout","login","required","answerRequest","getPendingRequests"]
"enum" : ["requestToken","createToken","deleteToken","getTokenList","logout","login","required","adminRequired","newPasswordRequired","newPassword","answerRequest","getPendingRequests"]
},
"tan" : {
"type" : "integer"
},
"username": {
"type": "string",
"minLength" : 3
},
"password": {
"type": "string",
"minLength" : 8
},
"newPassword": {
"type": "string",
"minLength" : 8
},
"token": {
"type": "string",
"minLength" : 36

View File

@ -48,19 +48,37 @@ JsonAPI::JsonAPI(QString peerAddress, Logger* log, const bool& localConnection,
, _noListener(noListener)
, _peerAddress(peerAddress)
, _log(log)
, _localConnection(localConnection)
, _instanceManager(HyperionIManager::getInstance())
, _hyperion(nullptr)
, _jsonCB(nullptr)
, _jsonCB(new JsonCB(this))
, _streaming_logging_activated(false)
, _imageStreamTimer(new QTimer(this))
, _ledStreamTimer(new QTimer(this))
{
Q_INIT_RESOURCE(JSONRPC_schemas);
}
void JsonAPI::initialize(void)
{
// For security we block external connections if default PW is set
if(!_localConnection && _authManager->hasHyperionDefaultPw())
{
emit forceClose();
}
// if this is localConnection and network allows unauth locals, set authorized flag
if(_apiAuthRequired && localConnection)
if(_apiAuthRequired && _localConnection)
_authorized = !_authManager->isLocalAuthRequired();
// admin access is allowed, when the connection is local and the option for local admin isn't set. Con: All local connections get full access
if(_localConnection)
{
_userAuthorized = !_authManager->isLocalAdminAuthRequired();
// just in positive direction
if(_userAuthorized)
_authorized = true;
}
// setup auth interface
connect(_authManager, &AuthManager::newPendingTokenRequest, this, &JsonAPI::handlePendingTokenRequest);
connect(_authManager, &AuthManager::tokenResponse, this, &JsonAPI::handleTokenResponse);
@ -68,6 +86,9 @@ JsonAPI::JsonAPI(QString peerAddress, Logger* log, const bool& localConnection,
// listen for killed instances
connect(_instanceManager, &HyperionIManager::instanceStateChanged, this, &JsonAPI::handleInstanceStateChange);
// pipe callbacks from subscriptions to parent
connect(_jsonCB, &JsonCB::newCallback, this, &JsonAPI::callbackMessage);
// init Hyperion pointer
handleInstanceSwitch(0);
@ -91,30 +112,8 @@ bool JsonAPI::handleInstanceSwitch(const quint8& inst, const bool& forced)
// get new Hyperion pointer
_hyperion = _instanceManager->getHyperionInstance(inst);
// the JsonCB creates json messages you can subscribe to e.g. data change events; forward them to the parent client
QStringList cbCmds;
if(_jsonCB != nullptr)
{
cbCmds = _jsonCB->getSubscribedCommands();
delete _jsonCB;
}
_jsonCB = new JsonCB(_hyperion, this);
connect(_jsonCB, &JsonCB::newCallback, this, &JsonAPI::callbackMessage);
// read subs
for(const auto & entry : cbCmds)
{
_jsonCB->subscribeFor(entry);
}
// // imageStream last state
// if(_ledcolorsImageActive)
// connect(_hyperion, &Hyperion::currentImage, this, &JsonAPI::setImage, Qt::UniqueConnection);
//
// //ledColor stream last state
// if(_ledcolorsLedsActive)
// connect(_hyperion, &Hyperion::rawLedColors, this, &JsonAPI::streamLedcolorsUpdate, Qt::UniqueConnection);
// the JsonCB creates json messages you can subscribe to e.g. data change events
_jsonCB->setSubscriptionsTo(_hyperion);
return true;
}
@ -727,7 +726,7 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject& message, const QString&
for(const auto & entry : subsArr)
{
// config callbacks just if auth is set
if(entry == "settings-update" && !_authorized)
if(entry == "settings-update" && !_userAuthorized)
continue;
if(!_jsonCB->subscribeFor(entry.toString()))
@ -887,17 +886,28 @@ void JsonAPI::handleConfigCommand(const QJsonObject& message, const QString& com
}
else if (subcommand == "setconfig")
{
handleConfigSetCommand(message, full_command, tan);
if(_userAuthorized)
handleConfigSetCommand(message, full_command, tan);
else
sendErrorReply("No Authorization",command, tan);
}
else if (subcommand == "getconfig")
{
sendSuccessDataReply(QJsonDocument(_hyperion->getQJsonConfig()), full_command, tan);
if(_userAuthorized)
sendSuccessDataReply(QJsonDocument(_hyperion->getQJsonConfig()), full_command, tan);
else
sendErrorReply("No Authorization",command, tan);
}
else if (subcommand == "reload")
{
_hyperion->freeObjects(true);
Process::restartHyperion();
sendErrorReply("failed to restart hyperion", full_command, tan);
if(_userAuthorized)
{
_hyperion->freeObjects(true);
Process::restartHyperion();
sendErrorReply("failed to restart hyperion", full_command, tan);
}
else
sendErrorReply("No Authorization",command, tan);
}
else
{
@ -1139,11 +1149,33 @@ void JsonAPI::handleVideoModeCommand(const QJsonObject& message, const QString &
void JsonAPI::handleAuthorizeCommand(const QJsonObject & message, const QString &command, const int tan)
{
const QString& subc = message["subcommand"].toString().trimmed();
const QString& id = message["id"].toString().trimmed();
const QString& password = message["password"].toString().trimmed();
const QString& newPassword = message["newPassword"].toString().trimmed();
// catch test if auth is required
if(subc == "required")
{
QJsonObject req;
req["required"] = _apiAuthRequired;
req["required"] = !_authorized;
sendSuccessDataReply(QJsonDocument(req), command+"-"+subc, tan);
return;
}
// catch test if admin auth is required
if(subc == "adminRequired")
{
QJsonObject req;
req["adminRequired"] = !_userAuthorized;
sendSuccessDataReply(QJsonDocument(req), command+"-"+subc, tan);
return;
}
// default hyperion password is a security risk, replace it asap
if(subc == "newPasswordRequired")
{
QJsonObject req;
req["newPasswordRequired"] = _authManager->hasHyperionDefaultPw();
sendSuccessDataReply(QJsonDocument(req), command+"-"+subc, tan);
return;
}
@ -1153,10 +1185,30 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject & message, const QString
{
_authorized = false;
_userAuthorized = false;
// disconnect all kind of data callbacks
stopDataConnections();
sendSuccessReply(command+"-"+subc, tan);
return;
}
// change password
if(subc == "newPassword")
{
// use password, newPassword
if(_userAuthorized)
{
if(_authManager->updateUserPassword("Hyperion", password, newPassword))
{
sendSuccessReply(command+"-"+subc, tan);
return;
}
sendErrorReply("Failed to update user password",command+"-"+subc, tan);
return;
}
sendErrorReply("No Authorization",command+"-"+subc, tan);
return;
}
// token created from ui
if(subc == "createToken")
{
@ -1180,11 +1232,11 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject & message, const QString
// delete token
if(subc == "deleteToken")
{
const QString& did = message["id"].toString().trimmed();
// use id
// for user authorized sessions
if(_userAuthorized)
{
_authManager->deleteToken(did);
_authManager->deleteToken(id);
sendSuccessReply(command+"-"+subc, tan);
return;
}
@ -1195,8 +1247,8 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject & message, const QString
// catch token request
if(subc == "requestToken")
{
// use id
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;
@ -1228,7 +1280,7 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject & message, const QString
// accept/deny token request
if(subc == "answerRequest")
{
const QString& id = message["id"].toString().trimmed();
// use id
const bool& accept = message["accept"].toBool(false);
if(_userAuthorized)
{
@ -1245,7 +1297,7 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject & message, const QString
// deny token request
if(subc == "acceptRequest")
{
const QString& id = message["id"].toString().trimmed();
// use id
if(_userAuthorized)
{
_authManager->acceptTokenRequest(id);
@ -1256,7 +1308,7 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject & message, const QString
return;
}
// cath get token list
// catch get token list
if(subc == "getTokenList")
{
if(_userAuthorized)
@ -1283,12 +1335,27 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject & message, const QString
// login
if(subc == "login")
{
// catch token auth
const QString& token = message["token"].toString().trimmed();
// catch token
if(!token.isEmpty())
{
if(token.count() >= 36)
// userToken is longer
if(token.count() > 36)
{
if(_authManager->isUserTokenAuthorized("Hyperion",token))
{
_authorized = true;
_userAuthorized = true;
sendSuccessReply(command+"-"+subc, tan);
}
else
sendErrorReply("No Authorization", command+"-"+subc, tan);
return;
}
// usual app token is 36
if(token.count() == 36)
{
if(_authManager->isTokenAuthorized(token))
{
@ -1304,23 +1371,25 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject & message, const QString
return;
}
// user & password
const QString& user = message["username"].toString().trimmed();
const QString& password = message["password"].toString().trimmed();
// password
// use password
if(user.count() >= 3 && password.count() >= 8)
if(password.count() >= 8)
{
if(_authManager->isUserAuthorized(user, password))
if(_authManager->isUserAuthorized("Hyperion", password))
{
_authorized = true;
_userAuthorized = true;
sendSuccessReply(command+"-"+subc, tan);
// Return the current valid Hyperion user token
QJsonObject obj;
obj["token"] = _authManager->getUserToken();
sendSuccessDataReply(QJsonDocument(obj),command+"-"+subc, tan);
}
else
sendErrorReply("No Authorization", command+"-"+subc, tan);
}
else
sendErrorReply("User or password string too short", command+"-"+subc, tan);
sendErrorReply("Password string too short", command+"-"+subc, tan);
}
}
@ -1343,8 +1412,11 @@ void JsonAPI::handleInstanceCommand(const QJsonObject & message, const QString &
if(subc == "switchTo")
{
if(handleInstanceSwitch(inst))
sendSuccessReply(command+"-"+subc, tan);
if(handleInstanceSwitch(inst)){
QJsonObject obj;
obj["instance"] = inst;
sendSuccessDataReply(QJsonDocument(obj),command+"-"+subc, tan);
}
else
sendErrorReply("Selected Hyperion instance isn't running",command+"-"+subc, tan);
return;
@ -1576,3 +1648,13 @@ void JsonAPI::handleInstanceStateChange(const instanceState& state, const quint8
break;
}
}
void JsonAPI::stopDataConnections(void)
{
LoggerManager::getInstance()->disconnect();
_streaming_logging_activated = false;
_jsonCB->resetSubscriptions();
_imageStreamTimer->stop();
_ledStreamTimer->stop();
}

View File

@ -27,87 +27,138 @@
using namespace hyperion;
JsonCB::JsonCB(Hyperion* hyperion, QObject* parent)
JsonCB::JsonCB(QObject* parent)
: QObject(parent)
, _hyperion(hyperion)
, _componentRegister(& _hyperion->getComponentRegister())
, _hyperion(nullptr)
, _componentRegister(nullptr)
, _bonjour(BonjourBrowserWrapper::getInstance())
, _prioMuxer(_hyperion->getMuxerInstance())
, _prioMuxer(nullptr)
{
_availableCommands << "components-update" << "sessions-update" << "priorities-update" << "imageToLedMapping-update"
<< "adjustment-update" << "videomode-update" << "effects-update" << "settings-update" << "leds-update" << "instance-update";
}
bool JsonCB::subscribeFor(const QString& type)
bool JsonCB::subscribeFor(const QString& type, const bool & unsubscribe)
{
if(!_availableCommands.contains(type))
return false;
if(unsubscribe)
_subscribedCommands.removeAll(type);
else
_subscribedCommands << type;
if(type == "components-update")
{
_subscribedCommands << type;
connect(_componentRegister, &ComponentRegister::updatedComponentState, this, &JsonCB::handleComponentState, Qt::UniqueConnection);
if(unsubscribe)
disconnect(_componentRegister, &ComponentRegister::updatedComponentState, this, &JsonCB::handleComponentState);
else
connect(_componentRegister, &ComponentRegister::updatedComponentState, this, &JsonCB::handleComponentState, Qt::UniqueConnection);
}
if(type == "sessions-update")
{
_subscribedCommands << type;
connect(_bonjour, &BonjourBrowserWrapper::browserChange, this, &JsonCB::handleBonjourChange, Qt::UniqueConnection);
if(unsubscribe)
disconnect(_bonjour, &BonjourBrowserWrapper::browserChange, this, &JsonCB::handleBonjourChange);
else
connect(_bonjour, &BonjourBrowserWrapper::browserChange, this, &JsonCB::handleBonjourChange, Qt::UniqueConnection);
}
if(type == "priorities-update")
{
_subscribedCommands << type;
connect(_prioMuxer, &PriorityMuxer::prioritiesChanged, this, &JsonCB::handlePriorityUpdate, Qt::UniqueConnection);
connect(_prioMuxer, &PriorityMuxer::autoSelectChanged, this, &JsonCB::handlePriorityUpdate, Qt::UniqueConnection);
if(unsubscribe){
disconnect(_prioMuxer,0 ,0 ,0);
} else {
connect(_prioMuxer, &PriorityMuxer::prioritiesChanged, this, &JsonCB::handlePriorityUpdate, Qt::UniqueConnection);
connect(_prioMuxer, &PriorityMuxer::autoSelectChanged, this, &JsonCB::handlePriorityUpdate, Qt::UniqueConnection);
}
}
if(type == "imageToLedMapping-update")
{
_subscribedCommands << type;
connect(_hyperion, &Hyperion::imageToLedsMappingChanged, this, &JsonCB::handleImageToLedsMappingChange, Qt::UniqueConnection);
if(unsubscribe)
disconnect(_hyperion, &Hyperion::imageToLedsMappingChanged, this, &JsonCB::handleImageToLedsMappingChange);
else
connect(_hyperion, &Hyperion::imageToLedsMappingChanged, this, &JsonCB::handleImageToLedsMappingChange, Qt::UniqueConnection);
}
if(type == "adjustment-update")
{
_subscribedCommands << type;
connect(_hyperion, &Hyperion::adjustmentChanged, this, &JsonCB::handleAdjustmentChange, Qt::UniqueConnection);
if(unsubscribe)
disconnect(_hyperion, &Hyperion::adjustmentChanged, this, &JsonCB::handleAdjustmentChange);
else
connect(_hyperion, &Hyperion::adjustmentChanged, this, &JsonCB::handleAdjustmentChange, Qt::UniqueConnection);
}
if(type == "videomode-update")
{
_subscribedCommands << type;
connect(_hyperion, &Hyperion::newVideoMode, this, &JsonCB::handleVideoModeChange, Qt::UniqueConnection);
if(unsubscribe)
disconnect(_hyperion, &Hyperion::newVideoMode, this, &JsonCB::handleVideoModeChange);
else
connect(_hyperion, &Hyperion::newVideoMode, this, &JsonCB::handleVideoModeChange, Qt::UniqueConnection);
}
if(type == "effects-update")
{
_subscribedCommands << type;
connect(_hyperion, &Hyperion::effectListUpdated, this, &JsonCB::handleEffectListChange, Qt::UniqueConnection);
if(unsubscribe)
disconnect(_hyperion, &Hyperion::effectListUpdated, this, &JsonCB::handleEffectListChange);
else
connect(_hyperion, &Hyperion::effectListUpdated, this, &JsonCB::handleEffectListChange, Qt::UniqueConnection);
}
if(type == "settings-update")
{
_subscribedCommands << type;
connect(_hyperion, &Hyperion::settingsChanged, this, &JsonCB::handleSettingsChange, Qt::UniqueConnection);
if(unsubscribe)
disconnect(_hyperion, &Hyperion::settingsChanged, this, &JsonCB::handleSettingsChange);
else
connect(_hyperion, &Hyperion::settingsChanged, this, &JsonCB::handleSettingsChange, Qt::UniqueConnection);
}
if(type == "leds-update")
{
_subscribedCommands << type;
connect(_hyperion, &Hyperion::settingsChanged, this, &JsonCB::handleLedsConfigChange, Qt::UniqueConnection);
if(unsubscribe)
disconnect(_hyperion, &Hyperion::settingsChanged, this, &JsonCB::handleLedsConfigChange);
else
connect(_hyperion, &Hyperion::settingsChanged, this, &JsonCB::handleLedsConfigChange, Qt::UniqueConnection);
}
if(type == "instance-update")
{
_subscribedCommands << type;
connect(HyperionIManager::getInstance(), &HyperionIManager::change, this, &JsonCB::handleInstanceChange, Qt::UniqueConnection);
if(unsubscribe)
disconnect(HyperionIManager::getInstance(), &HyperionIManager::change, this, &JsonCB::handleInstanceChange);
else
connect(HyperionIManager::getInstance(), &HyperionIManager::change, this, &JsonCB::handleInstanceChange, Qt::UniqueConnection);
}
return true;
}
void JsonCB::resetSubscriptions(void){
for(const auto & entry : getSubscribedCommands()){
subscribeFor(entry, true);
}
}
void JsonCB::setSubscriptionsTo(Hyperion* hyperion){
// get current subs
QStringList currSubs(getSubscribedCommands());
// stop subs
resetSubscriptions();
// update pointer
_hyperion = hyperion;
_componentRegister = &_hyperion->getComponentRegister();
_prioMuxer = _hyperion->getMuxerInstance();
// re-apply subs
for(const auto & entry : currSubs)
{
subscribeFor(entry);
}
}
void JsonCB::doCallback(const QString& cmd, const QVariant& data)
{
QJsonObject obj;

View File

@ -12,14 +12,16 @@ AuthManager* AuthManager::manager = nullptr;
AuthManager::AuthManager(QObject* parent)
: QObject(parent)
, _authTable(new AuthTable(this))
, _authTable(new AuthTable("",this))
, _metaTable(new MetaTable(this))
, _pendingRequests()
, _authRequired(true)
, _timer(new QTimer(this))
, _authBlockTimer(new QTimer(this))
{
AuthManager::manager = this;
// get uuid
_uuid = _metaTable->getUUID();
@ -27,21 +29,18 @@ AuthManager::AuthManager(QObject* parent)
_timer->setInterval(1000);
connect(_timer, &QTimer::timeout, this, &AuthManager::checkTimeout);
// setup authBlockTimer
_authBlockTimer->setInterval(60000);
connect(_authBlockTimer, &QTimer::timeout, this, &AuthManager::checkAuthBlockTimeout);
// init with default user and password
if(!_authTable->userExist("Hyperion"))
{
_authTable->createUser("Hyperion","hyperion");
}
}
bool & AuthManager::isAuthRequired()
{
return _authRequired;
}
bool & AuthManager::isLocalAuthRequired()
{
return _localAuthRequired;
// update Hyperion user token on startup
_authTable->setUserToken("Hyperion");
}
const AuthManager::AuthDefinition AuthManager::createToken(const QString& comment)
@ -77,14 +76,69 @@ const QVector<AuthManager::AuthDefinition> AuthManager::getTokenList()
return finalVec;
}
const QString AuthManager::getUserToken(const QString & usr)
{
return QString(_authTable->getUserToken(usr));
}
void AuthManager::setAuthBlock(const bool& user)
{
// current timestamp +10 minutes
if(user)
_userAuthAttempts.append(QDateTime::currentMSecsSinceEpoch()+600000);
else
_tokenAuthAttempts.append(QDateTime::currentMSecsSinceEpoch()+600000);
QMetaObject::invokeMethod(_authBlockTimer, "start", Qt::QueuedConnection);
}
bool AuthManager::isUserAuthorized(const QString& user, const QString& pw)
{
return _authTable->isUserAuthorized(user, pw);
if(isUserAuthBlocked())
return false;
if(!_authTable->isUserAuthorized(user, pw)){
setAuthBlock(true);
return false;
}
return true;
}
bool AuthManager::isTokenAuthorized(const QString& token)
{
return _authTable->tokenExist(token);
if(isTokenAuthBlocked())
return false;
if(!_authTable->tokenExist(token)){
setAuthBlock();
return false;
}
return true;
}
bool AuthManager::isUserTokenAuthorized(const QString& usr, const QString& token)
{
if(isUserAuthBlocked())
return false;
if(!_authTable->isUserTokenAuthorized(usr, token)){
setAuthBlock(true);
return false;
}
return true;
}
bool AuthManager::updateUserPassword(const QString& user, const QString& pw, const QString& newPw)
{
if(isUserAuthorized(user, pw))
return _authTable->updateUserPassword(user, newPw);
return false;
}
bool AuthManager::resetHyperionUser()
{
return _authTable->resetHyperionUser();
}
void AuthManager::setNewTokenRequest(QObject* caller, const QString& comment, const QString& id)
@ -144,6 +198,7 @@ void AuthManager::handleSettingsUpdate(const settings::type& type, const QJsonDo
const QJsonObject& obj = config.object();
_authRequired = obj["apiAuth"].toBool(true);
_localAuthRequired = obj["localApiAuth"].toBool(false);
_localAdminAuthRequired = obj["localAdminAuth"].toBool(false);
}
}
@ -167,3 +222,25 @@ void AuthManager::checkTimeout()
if(_pendingRequests.isEmpty())
_timer->stop();
}
void AuthManager::checkAuthBlockTimeout(){
// handle user auth block
for (auto it = _userAuthAttempts.begin(); it != _userAuthAttempts.end(); it++) {
// after 10 minutes, we remove the entry
if (*it < (uint64_t)QDateTime::currentMSecsSinceEpoch()) {
_userAuthAttempts.erase(it--);
}
}
// handle token auth block
for (auto it = _tokenAuthAttempts.begin(); it != _tokenAuthAttempts.end(); it++) {
// after 10 minutes, we remove the entry
if (*it < (uint64_t)QDateTime::currentMSecsSinceEpoch()) {
_tokenAuthAttempts.erase(it--);
}
}
// if the lists are empty we stop
if(_userAuthAttempts.empty() && _tokenAuthAttempts.empty())
_authBlockTimer->stop();
}

View File

@ -66,6 +66,14 @@
}
},
"propertyOrder" : 5
},
"localAdminAuth" :
{
"type" : "boolean",
"title" : "edt_conf_net_localAdminAuth_title",
"required" : true,
"default" : true,
"propertyOrder" : 5
}
},
"additionalProperties" : false

View File

@ -17,7 +17,10 @@ JsonClientConnection::JsonClientConnection(QTcpSocket *socket, const bool& local
// create a new instance of JsonAPI
_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)));
connect(_jsonAPI, &JsonAPI::callbackMessage, this , &JsonClientConnection::sendMessage);
connect(_jsonAPI, &JsonAPI::forceClose, this , [&](){ _socket->close(); } );
_jsonAPI->initialize();
}
void JsonClientConnection::readRequest()

View File

@ -300,8 +300,8 @@ void QtHttpClientWrapper::onReplySendDataRequested (void)
void QtHttpClientWrapper::sendToClientWithReply(QtHttpReply * reply)
{
connect (reply, &QtHttpReply::requestSendHeaders, this, &QtHttpClientWrapper::onReplySendHeadersRequested);
connect (reply, &QtHttpReply::requestSendData, this, &QtHttpClientWrapper::onReplySendDataRequested);
connect (reply, &QtHttpReply::requestSendHeaders, this, &QtHttpClientWrapper::onReplySendHeadersRequested, Qt::UniqueConnection);
connect (reply, &QtHttpReply::requestSendData, this, &QtHttpClientWrapper::onReplySendDataRequested, Qt::UniqueConnection);
m_parsingStatus = sendReplyToClient (reply);
}
@ -340,3 +340,19 @@ QtHttpClientWrapper::ParsingStatus QtHttpClientWrapper::sendReplyToClient (QtHtt
return AwaitingRequest;
}
void QtHttpClientWrapper::closeConnection()
{
// probably filter for request to follow http spec
if(m_currentRequest != Q_NULLPTR)
{
QtHttpReply reply(m_serverHandle);
reply.setStatusCode(QtHttpReply::StatusCode::Forbidden);
connect (&reply, &QtHttpReply::requestSendHeaders, this, &QtHttpClientWrapper::onReplySendHeadersRequested, Qt::UniqueConnection);
connect (&reply, &QtHttpReply::requestSendData, this, &QtHttpClientWrapper::onReplySendDataRequested, Qt::UniqueConnection);
m_parsingStatus = sendReplyToClient(&reply);
}
m_sockClient->close ();
}

View File

@ -34,6 +34,11 @@ public:
/// @brief Wrapper for sendReplyToClient(), handles m_parsingStatus and signal connect
void sendToClientWithReply (QtHttpReply * reply);
///
/// @brief close a connection with FORBIDDEN header (used from JsonAPI over HTTP)
///
void closeConnection();
private slots:
void onClientDataReceived (void);

View File

@ -15,14 +15,20 @@ WebJsonRpc::WebJsonRpc(QtHttpRequest* request, QtHttpServer* server, const bool&
const QString client = request->getClientInfo().clientAddress.toString();
_jsonAPI = new JsonAPI(client, _log, localConnection, this, true);
connect(_jsonAPI, &JsonAPI::callbackMessage, this, &WebJsonRpc::handleCallback);
connect(_jsonAPI, &JsonAPI::forceClose, [&]() { _wrapper->closeConnection(); _stopHandle = true; });
_jsonAPI->initialize();
}
void WebJsonRpc::handleMessage(QtHttpRequest* request)
{
QByteArray header = request->getHeader("Authorization");
QByteArray data = request->getRawData();
_unlocked = true;
_jsonAPI->handleMessage(data,header);
// TODO better solution. If jsonAPI emits forceClose the request is deleted and the following call to this method results in segfault
if(!_stopHandle)
{
QByteArray header = request->getHeader("Authorization");
QByteArray data = request->getRawData();
_unlocked = true;
_jsonAPI->handleMessage(data,header);
}
}
void WebJsonRpc::handleCallback(QJsonObject obj)

View File

@ -22,6 +22,7 @@ private:
Logger* _log;
JsonAPI* _jsonAPI;
bool _stopHandle = false;
bool _unlocked = false;
private slots:

View File

@ -25,6 +25,7 @@ WebSocketClient::WebSocketClient(QtHttpRequest* request, QTcpSocket* sock, const
// Json processor
_jsonAPI = new JsonAPI(client, _log, localConnection, this);
connect(_jsonAPI, &JsonAPI::callbackMessage, this, &WebSocketClient::sendMessage);
connect(_jsonAPI, &JsonAPI::forceClose, this,[this]() { this->sendClose(CLOSECODE::NORMAL); });
Debug(_log, "New connection from %s", QSTRING_CSTR(client));
@ -40,6 +41,9 @@ WebSocketClient::WebSocketClient(QtHttpRequest* request, QTcpSocket* sock, const
_socket->write(QSTRING_CSTR(data), data.size());
_socket->flush();
// Init JsonAPI
_jsonAPI->initialize();
}
void WebSocketClient::handleWebSocketFrame(void)
@ -123,7 +127,7 @@ void WebSocketClient::handleWebSocketFrame(void)
handleBinaryMessage(_wsReceiveBuffer);
}
_wsReceiveBuffer.clear();
}
}
break;

View File

@ -72,7 +72,7 @@ class HyperionDaemon : public QObject
friend SysTray;
public:
HyperionDaemon(QString rootPath, QObject *parent, const bool& logLvlOverwrite );
HyperionDaemon(QString rootPath, QObject *parent, const bool& logLvlOverwrite);
~HyperionDaemon();
///

View File

@ -28,6 +28,7 @@
#include <utils/FileUtils.h>
#include <commandline/Parser.h>
#include <commandline/IntOption.h>
#include <../../include/db/AuthTable.h>
#ifdef ENABLE_X11
#include <X11/Xlib.h>
@ -239,14 +240,15 @@ int main(int argc, char** argv)
Parser parser("Hyperion Daemon");
parser.addHelpOption();
BooleanOption & versionOption = parser.add<BooleanOption>(0x0, "version", "Show version information");
Option & userDataOption = parser.add<Option> (0x0, "userdata", "Overwrite user data path, defaults to home directory of current user (%1)", QDir::homePath() + "/.hyperion");
BooleanOption & silentOption = parser.add<BooleanOption>('s', "silent", "do not print any outputs");
BooleanOption & verboseOption = parser.add<BooleanOption>('v', "verbose", "Increase verbosity");
BooleanOption & debugOption = parser.add<BooleanOption>('d', "debug", "Show debug messages");
parser.add<BooleanOption>(0x0, "desktop", "show systray on desktop");
parser.add<BooleanOption>(0x0, "service", "force hyperion to start as console service");
Option & exportEfxOption = parser.add<Option> (0x0, "export-effects", "export effects to given path");
BooleanOption & versionOption = parser.add<BooleanOption> (0x0, "version", "Show version information");
Option & userDataOption = parser.add<Option> ('u', "userdata", "Overwrite user data path, defaults to home directory of current user (%1)", QDir::homePath() + "/.hyperion");
BooleanOption & resetPassword = parser.add<BooleanOption> (0x0, "resetPassword", "Lost your password? Reset it with this option back to 'hyperion'");
BooleanOption & silentOption = parser.add<BooleanOption> ('s', "silent", "do not print any outputs");
BooleanOption & verboseOption = parser.add<BooleanOption> ('v', "verbose", "Increase verbosity");
BooleanOption & debugOption = parser.add<BooleanOption> ('d', "debug", "Show debug messages");
parser.add<BooleanOption> (0x0, "desktop", "show systray on desktop");
parser.add<BooleanOption> (0x0, "service", "force hyperion to start as console service");
Option & exportEfxOption = parser.add<Option> (0x0, "export-effects", "export effects to given path");
parser.process(*qApp);
@ -334,6 +336,21 @@ int main(int argc, char** argv)
Info(log, "Set user data path to '%s'", QSTRING_CSTR(mDir.absolutePath()));
// reset Password without spawning daemon
if(parser.isSet(resetPassword))
{
AuthTable* table = new AuthTable(userDataPath);
if(table->resetHyperionUser()){
Info(log,"Password reset successfull");
delete table;
exit(0);
} else {
Error(log,"Failed to reset password!");
delete table;
exit(1);
}
}
HyperionDaemon* hyperiond = nullptr;
try
{