diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index db995f3296..e18e863d41 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,7 +1,7 @@ [ { "name": "metaverse", - "label": "Metaverse Registration", + "label": "Metaverse / Networking", "settings": [ { "name": "access_token", @@ -44,6 +44,29 @@ } ] }, + { + "label": "Paths", + "settings": [ + { + "name": "paths", + "label": "", + "help": "Clients can enter a path to reach an exact viewpoint in your domain.
Add rows to the table below to map a path to a viewpoint.
The index path ( / ) is where clients will enter if they do not enter an explicit path.", + "type": "table", + "key": { + "name": "path", + "label": "Path", + "placeholder": "/" + }, + "columns": [ + { + "name": "viewpoint", + "label": "Viewpoint", + "placeholder": "/512,512,512" + } + ] + } + ] + }, { "name": "security", "label": "Security", diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index 4fac753959..f956d4a374 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -21,34 +21,34 @@ var Settings = { }; var viewHelpers = { - getFormGroup: function(groupName, setting, values, isAdvanced, isLocked) { - setting_name = groupName + "." + setting.name - - form_group = "
" - - if (_.has(values, groupName) && _.has(values[groupName], setting.name)) { - setting_value = values[groupName][setting.name] - } else if (_.has(setting, 'default')) { - setting_value = setting.default - } else { - setting_value = "" + getFormGroup: function(keypath, setting, values, isAdvanced, isLocked) { + form_group = "
"; + + setting_value = _(values).valueForKeyPath(keypath); + + if (!setting_value) { + if (_.has(setting, 'default')) { + setting_value = setting.default; + } else { + setting_value = ""; + } } - - label_class = 'control-label' + + label_class = 'control-label'; if (isLocked) { - label_class += ' locked' + label_class += ' locked'; } - + common_attrs = " class='" + (setting.type !== 'checkbox' ? 'form-control' : '') - + " " + Settings.TRIGGER_CHANGE_CLASS + "' data-short-name='" + setting.name + "' name='" + setting_name + "' " - + "id='" + setting_name + "'" - + + " " + Settings.TRIGGER_CHANGE_CLASS + "' data-short-name='" + setting.name + "' name='" + keypath + "' " + + "id='" + keypath + "'"; + if (setting.type === 'checkbox') { if (setting.label) { form_group += "" } form_group += "
" - form_group += "
" @@ -56,52 +56,52 @@ var viewHelpers = { input_type = _.has(setting, 'type') ? setting.type : "text" if (setting.label) { - form_group += ""; + form_group += ""; } - + if (input_type === 'table') { - form_group += makeTable(setting, setting_name, setting_value, isLocked) + form_group += makeTable(setting, keypath, setting_value, isLocked) } else { if (input_type === 'select') { - form_group += "'" + _.each(setting.options, function(option) { - form_group += "" }) - + form_group += "" - + form_group += "" } else { - + if (input_type == 'integer') { input_type = "text" } - + form_group += "" } - + form_group += "" + setting.help + "" - } + } } - + form_group += "
" return form_group } } -$(document).ready(function(){ +$(document).ready(function(){ /* - * Clamped-width. + * Clamped-width. * Usage: *
This long content will force clamped width
* * Author: LV */ - + $('[data-clampedwidth]').each(function () { var elem = $(this); var parentPanel = elem.data('clampedwidth'); @@ -113,52 +113,52 @@ $(document).ready(function(){ resizeFn(); $(window).resize(resizeFn); }) - + $('#settings-form').on('click', '.' + Settings.ADD_ROW_BUTTON_CLASS, function(){ addTableRow(this); }) - + $('#settings-form').on('click', '.' + Settings.DEL_ROW_BUTTON_CLASS, function(){ deleteTableRow(this); }) - + $('#settings-form').on('click', '.' + Settings.MOVE_UP_BUTTON_CLASS, function(){ moveTableRow(this, true); }) - + $('#settings-form').on('click', '.' + Settings.MOVE_DOWN_BUTTON_CLASS, function(){ moveTableRow(this, false); }) - + $('#settings-form').on('keypress', 'table input', function(e){ if (e.keyCode == 13) { // capture enter in table input // if we have a sibling next to us that has an input, jump to it, otherwise check if we have a glyphicon for add to click sibling = $(this).parent('td').next(); - + if (sibling.hasClass(Settings.DATA_COL_CLASS)) { // set focus to next input sibling.find('input').focus() } else if (sibling.hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click() - + // set focus to the first input in the new row $(this).closest('table').find('tr.inputs input:first').focus() - } + } } }); - + $('#settings-form').on('change', '.' + Settings.TRIGGER_CHANGE_CLASS , function(){ // this input was changed, add the changed data attribute to it $(this).attr('data-changed', true) - + badgeSidebarForDifferences($(this)) }) - + $('#advanced-toggle-button').click(function(){ Settings.showAdvanced = !Settings.showAdvanced var advancedSelector = $('.' + Settings.ADVANCED_CLASS) - + if (Settings.showAdvanced) { advancedSelector.show() $(this).html("Hide advanced") @@ -166,48 +166,48 @@ $(document).ready(function(){ advancedSelector.hide() $(this).html("Show advanced") } - + $(this).blur() }) - + $('#settings-form').on('click', '#choose-domain-btn', function(){ chooseFromHighFidelityDomains($(this)) }) - + $('#settings-form').on('change', 'select', function(){ $("input[name='" + $(this).attr('data-hidden-input') + "']").val($(this).val()).change() }) var panelsSource = $('#panels-template').html() Settings.panelsTemplate = _.template(panelsSource) - + var sidebarTemplate = $('#list-group-template').html() Settings.sidebarTemplate = _.template(sidebarTemplate) - + // $('body').scrollspy({ target: '#setup-sidebar'}) - - reloadSettings() + + reloadSettings(); }) function reloadSettings() { $.getJSON('/settings.json', function(data){ _.extend(data, viewHelpers) - + $('.nav-stacked').html(Settings.sidebarTemplate(data)) $('#panels').html(Settings.panelsTemplate(data)) - + Settings.initialValues = form2js('settings-form', ".", false, cleanupFormValues, true); - + // add tooltip to locked settings $('label.locked').tooltip({ placement: 'right', title: 'This setting is in the master config file and cannot be changed' }) - + if (!_.has(data["locked"], "metaverse") && !_.has(data["locked"]["metaverse"], "id")) { // append the domain selection modal, as long as it's not locked appendDomainSelectionModal() - } + } }); } @@ -224,20 +224,20 @@ $('body').on('click', '.save-button', function(e){ $("input:not([data-changed])").each(function(){ $(this).prop('disabled', true); }); - + // grab a JSON representation of the form via form2js var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true); - + console.log(formJSON); - + // re-enable all inputs $("input").each(function(){ $(this).prop('disabled', false); }); - + // remove focus from the button $(this).blur() - + // POST the form JSON to the domain-server settings.json endpoint so the settings are saved $.ajax('/settings.json', { data: JSON.stringify(formJSON), @@ -254,36 +254,41 @@ $('body').on('click', '.save-button', function(e){ showErrorMessage("Error", SETTINGS_ERROR_MESSAGE) reloadSettings(); }); - + return false; }); -function makeTable(setting, setting_name, setting_value, isLocked) { +function makeTable(setting, keypath, setting_value, isLocked) { var isArray = !_.has(setting, 'key') - + if (!isArray && setting.can_order) { setting.can_order = false; } - - var html = "" + setting.help + "" - html += "" - + + var html = ""; + + if (setting.help) { + html += "" + setting.help + "" + } + + html += "
" + // Column names html += "" - + if (setting.numbered === true) { html += "" // Row number } - + if (setting.key) { html += "" // Key } - + _.each(setting.columns, function(col) { html += "" // Data }) - + if (!isLocked) { if (setting.can_order) { html += "" } - + // populate rows in the table from existing values var row_num = 1 - + _.each(setting_value, function(row, indexOrName) { - html += "" - + html += "" + if (setting.numbered === true) { html += "" } - + if (setting.key) { html += "" } - + _.each(setting.columns, function(col) { html += "" }) - + if (!isLocked) { if (setting.can_order) { html += "" } - + html += "" - + row_num++ }) - + // populate inputs in the table for new values if (!isLocked) { html += makeTableInputs(setting) } html += "
#" + setting.key.label + "" + col.label + "
" + row_num + "" + indexOrName + "" - + if (isArray) { rowIsObject = setting.columns.length > 1 colValue = rowIsObject ? row[col.name] : row html += colValue - + // for arrays we add a hidden input to this td so that values can be posted appropriately - html += "" } else if (row.hasOwnProperty(col.name)) { - html += row[col.name] + html += row[col.name] } - + html += "
" - + return html; } function makeTableInputs(setting) { var html = "" - + if (setting.numbered === true) { html += "" } - + if (setting.key) { html += "\ \ " } - + _.each(setting.columns, function(col) { html += "\ \ " }) - + if (setting.can_order) { html += "" } html += "" html += "" - + return html } function badgeSidebarForDifferences(changedElement) { // figure out which group this input is in var panelParentID = changedElement.closest('.panel').attr('id') - - // get a JSON representation of that section - var panelJSON = form2js(panelParentID, ".", false, cleanupFormValues, true)[panelParentID] - var initialPanelJSON = Settings.initialValues[panelParentID] - + + // if the panel contains non-grouped settings, the initial value is Settings.initialValues + var isGrouped = $(panelParentID).hasClass('grouped'); + + if (isGrouped) { + var initialPanelJSON = Settings.initialValues[panelParentID]; + + // get a JSON representation of that section + var panelJSON = form2js(panelParentID, ".", false, cleanupFormValues, true)[panelParentID]; + } else { + var initialPanelJSON = Settings.initialValues; + + // get a JSON representation of that section + var panelJSON = form2js(panelParentID, ".", false, cleanupFormValues, true); + } + var badgeValue = 0 - + // badge for any settings we have that are not the same or are not present in initialValues for (var setting in panelJSON) { - if ((!_.has(initialPanelJSON, setting) && panelJSON[setting] !== "") || - (!_.isEqual(panelJSON[setting], initialPanelJSON[setting]) + if ((!_.has(initialPanelJSON, setting) && panelJSON[setting] !== "") || + (!_.isEqual(panelJSON[setting], initialPanelJSON[setting]) && (panelJSON[setting] !== "" || _.has(initialPanelJSON, setting)))) { badgeValue += 1 } } - + // update the list-group-item badge to have the new value if (badgeValue == 0) { badgeValue = "" } - + $("a[href='#" + panelParentID + "'] .badge").html(badgeValue); } function addTableRow(add_glyphicon) { var row = $(add_glyphicon).closest('tr') - + var table = row.parents('table') var isArray = table.data('setting-type') === 'array' - + var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS) - + if (!isArray) { // Check key spaces var key = row.children(".key").children("input").val() @@ -433,7 +449,7 @@ function addTableRow(add_glyphicon) { return } } - + // Check empty fields var empty = false; _.each(row.children('.' + Settings.DATA_COL_CLASS + ' input'), function(element) { @@ -442,23 +458,23 @@ function addTableRow(add_glyphicon) { return } }) - + if (empty) { showErrorMessage("Error", "Empty field(s)") return } - + var input_clone = row.clone() - + // Change input row to data row var table = row.parents("table") - var setting_name = table.attr("name") + var setting_name = table.attr("name") var full_name = setting_name + "." + key row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS) row.removeClass("inputs") - + _.each(row.children(), function(element) { - if ($(element).hasClass("numbered")) { + if ($(element).hasClass("numbered")) { // Index row var numbers = columns.children(".numbered") if (numbers.length > 0) { @@ -478,88 +494,88 @@ function addTableRow(add_glyphicon) { var input = $(element).children("input") $(element).html(input.val()) input.remove() - } else if ($(element).hasClass(Settings.DATA_COL_CLASS)) { + } else if ($(element).hasClass(Settings.DATA_COL_CLASS)) { // Hide inputs var input = $(element).children("input") input.attr("type", "hidden") - + if (isArray) { var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length var key = $(element).attr('name') - + // are there multiple columns or just one? // with multiple we have an array of Objects, with one we have an array of whatever the value type is var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length - input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) + input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) } else { input.attr("name", full_name + "." + $(element).attr("name")) } - + input.attr("data-changed", "true") - + $(element).append(input.val()) } else { console.log("Unknown table element") } }) - + input_clone.find('input').each(function(){ $(this).val($(this).attr('data-default')); }); - + if (isArray) { updateDataChangedForSiblingRows(row, true) - + // the addition of any table row should remove the empty-array-row row.siblings('.empty-array-row').remove() } - + badgeSidebarForDifferences($(table)) - + row.parent().append(input_clone) } function deleteTableRow(delete_glyphicon) { var row = $(delete_glyphicon).closest('tr') - + var table = $(row).closest('table') var isArray = table.data('setting-type') === 'array' - + row.empty(); - + if (!isArray) { - row.html(""); } else { if (table.find('.' + Settings.DATA_ROW_CLASS).length > 1) { updateDataChangedForSiblingRows(row) - + // this isn't the last row - we can just remove it row.remove() } else { // this is the last row, we can't remove it completely since we need to post an empty array - + row.removeClass(Settings.DATA_ROW_CLASS).removeClass(Settings.NEW_ROW_CLASS) row.addClass('empty-array-row') - - row.html(""); } } - + // we need to fire a change event on one of the remaining inputs so that the sidebar badge is updated badgeSidebarForDifferences($(table)) } function moveTableRow(move_glyphicon, move_up) { var row = $(move_glyphicon).closest('tr') - + var table = $(row).closest('table') var isArray = table.data('setting-type') === 'array' if (!isArray) { return; } - + if (move_up) { var prev_row = row.prev() if (prev_row.hasClass(Settings.DATA_ROW_CLASS)) { @@ -571,7 +587,7 @@ function moveTableRow(move_glyphicon, move_up) { next_row.after(row) } } - + // we need to fire a change event on one of the remaining inputs so that the sidebar badge is updated badgeSidebarForDifferences($(table)) } @@ -579,23 +595,23 @@ function moveTableRow(move_glyphicon, move_up) { function updateDataChangedForSiblingRows(row, forceTrue) { // anytime a new row is added to an array we need to set data-changed for all sibling row inputs to true // unless it matches the inital set of values - + if (!forceTrue) { // figure out which group this row is in var panelParentID = row.closest('.panel').attr('id') // get the short name for the setting from the table var tableShortName = row.closest('table').data('short-name') - + // get a JSON representation of that section var panelSettingJSON = form2js(panelParentID, ".", false, cleanupFormValues, true)[panelParentID][tableShortName] var initialPanelSettingJSON = Settings.initialValues[panelParentID][tableShortName] - + // if they are equal, we don't need data-changed isTrue = !_.isEqual(panelSettingJSON, initialPanelSettingJSON) } else { isTrue = true } - + row.siblings('.' + Settings.DATA_ROW_CLASS).each(function(){ var hiddenInput = $(this).find('td.' + Settings.DATA_COL_CLASS + ' input') if (isTrue) { @@ -611,19 +627,19 @@ function showRestartModal() { backdrop: 'static', keyboard: false }); - + var secondsElapsed = 0; var numberOfSecondsToWait = 3; - + var refreshSpan = $('span#refresh-time') refreshSpan.html(numberOfSecondsToWait + " seconds"); - + // call ourselves every 1 second to countdown var refreshCountdown = setInterval(function(){ secondsElapsed++; secondsLeft = numberOfSecondsToWait - secondsElapsed refreshSpan.html(secondsLeft + (secondsLeft == 1 ? " second" : " seconds")) - + if (secondsElapsed == numberOfSecondsToWait) { location.reload(true); clearInterval(refreshCountdown); @@ -646,22 +662,22 @@ function showErrorMessage(title, message) { function chooseFromHighFidelityDomains(clickedButton) { // setup the modal to help user pick their domain if (Settings.initialValues.metaverse.access_token) { - + // add a spinner to the choose button clickedButton.html("Loading domains...") clickedButton.attr('disabled', 'disabled') - + // get a list of user domains from data-web data_web_domains_url = "https://metaverse.highfidelity.com/api/v1/domains?access_token=" $.getJSON(data_web_domains_url + Settings.initialValues.metaverse.access_token, function(data){ - + modal_buttons = { cancel: { label: 'Cancel', className: 'btn-default' } } - + if (data.data.domains.length) { // setup a select box for the returned domains modal_body = "

Choose the High Fidelity domain you want this domain-server to represent.
This will set your domain ID on the settings page.

" @@ -685,25 +701,25 @@ function chooseFromHighFidelityDomains(clickedButton) { window.open("https://metaverse.highfidelity.com/user/domains", '_blank'); } } - modal_body = "

You do not have any domains in your High Fidelity account." + + modal_body = "

You do not have any domains in your High Fidelity account." + "

Go to your domains page to create a new one. Once your domain is created re-open this dialog to select it.

" } - - + + bootbox.dialog({ title: "Choose matching domain", message: modal_body, buttons: modal_buttons }) - + // remove the spinner from the choose button clickedButton.html("Choose from my domains") clickedButton.removeAttr('disabled') }) - + } else { bootbox.alert({ - message: "You must have an access token to query your High Fidelity domains.

" + + message: "You must have an access token to query your High Fidelity domains.

" + "Please follow the instructions on the settings page to add an access token.", title: "Access token required" }) diff --git a/domain-server/resources/web/stats/js/underscore-keypath.min.js b/domain-server/resources/web/js/underscore-keypath.min.js similarity index 100% rename from domain-server/resources/web/stats/js/underscore-keypath.min.js rename to domain-server/resources/web/js/underscore-keypath.min.js diff --git a/domain-server/resources/web/settings/index.shtml b/domain-server/resources/web/settings/index.shtml index 598f137285..9bbf913b1a 100644 --- a/domain-server/resources/web/settings/index.shtml +++ b/domain-server/resources/web/settings/index.shtml @@ -5,31 +5,32 @@
- +
- +
- + - +
- +
- + @@ -83,8 +92,9 @@ + - \ No newline at end of file + diff --git a/domain-server/resources/web/stats/index.shtml b/domain-server/resources/web/stats/index.shtml index dc7fe9678f..13967d4e36 100644 --- a/domain-server/resources/web/stats/index.shtml +++ b/domain-server/resources/web/stats/index.shtml @@ -9,6 +9,6 @@ - + diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index b20b100789..0b42f78fe2 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -75,35 +75,35 @@ DomainServer::DomainServer(int argc, char* argv[]) : setOrganizationDomain("highfidelity.io"); setApplicationName("domain-server"); QSettings::setDefaultFormat(QSettings::IniFormat); - + // make sure we have a fresh AccountManager instance // (need this since domain-server can restart itself and maintain static variables) AccountManager::getInstance(true); - + _settingsManager.setupConfigMap(arguments()); - + // setup a shutdown event listener to handle SIGTERM or WM_CLOSE for us #ifdef _WIN32 installNativeEventFilter(&ShutdownEventListener::getInstance()); #else ShutdownEventListener::getInstance(); #endif - + qRegisterMetaType("DomainServerWebSessionData"); qRegisterMetaTypeStreamOperators("DomainServerWebSessionData"); - + if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth() && optionallySetupAssignmentPayment()) { // we either read a certificate and private key or were not passed one // and completed login or did not need to qDebug() << "Setting up LimitedNodeList and assignments."; setupNodeListAndAssignments(); - + loadExistingSessionsFromSettings(); - + // setup automatic networking settings with data server setupAutomaticNetworking(); - + // preload some user public keys so they can connect on first request preloadAllowedUserPublicKeys(); } @@ -111,7 +111,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : void DomainServer::restart() { qDebug() << "domain-server is restarting."; - + exit(DomainServer::EXIT_CODE_REBOOT); } @@ -182,16 +182,16 @@ bool DomainServer::optionallySetupOAuth() { const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); _oauthProviderURL = QUrl(settingsMap.value(OAUTH_PROVIDER_URL_OPTION).toString()); - + // if we don't have an oauth provider URL then we default to the default node auth url if (_oauthProviderURL.isEmpty()) { _oauthProviderURL = NetworkingConstants::METAVERSE_SERVER_URL; } - + AccountManager& accountManager = AccountManager::getInstance(); accountManager.disableSettingsFilePersistence(); accountManager.setAuthURL(_oauthProviderURL); - + _oauthClientID = settingsMap.value(OAUTH_CLIENT_ID_OPTION).toString(); _oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV); _hostname = settingsMap.value(REDIRECT_HOSTNAME_OPTION).toString(); @@ -226,7 +226,7 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { QVariant localPortValue = _settingsManager.valueOrDefaultValueForKeyPath(CUSTOM_LOCAL_PORT_OPTION); unsigned short domainServerPort = (unsigned short) localPortValue.toUInt(); - + QVariantMap& settingsMap = _settingsManager.getSettingsMap(); unsigned short domainServerDTLSPort = 0; @@ -245,12 +245,12 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { parseAssignmentConfigs(parsedTypes); populateDefaultStaticAssignmentsExcludingTypes(parsedTypes); - + // check for scripts the user wants to persist from their domain-server config populateStaticScriptedAssignmentsFromSettings(); auto nodeList = DependencyManager::set(domainServerPort, domainServerDTLSPort); - + // no matter the local port, save it to shared mem so that local assignment clients can ask what it is nodeList->putLocalPortIntoSharedMemory(DOMAIN_SERVER_LOCAL_PORT_SMEM_KEY, this, nodeList->getNodeSocket().localPort()); @@ -260,7 +260,7 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { quint16 localHttpsPort = DOMAIN_SERVER_HTTPS_PORT; nodeList->putLocalPortIntoSharedMemory(DOMAIN_SERVER_LOCAL_HTTPS_PORT_SMEM_KEY, this, localHttpsPort); - + // set our LimitedNodeList UUID to match the UUID from our config // nodes will currently use this to add resources to data-web that relate to our domain const QString METAVERSE_DOMAIN_ID_KEY_PATH = "metaverse.id"; @@ -280,22 +280,22 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { bool DomainServer::didSetupAccountManagerWithAccessToken() { AccountManager& accountManager = AccountManager::getInstance(); - + if (accountManager.hasValidAccessToken()) { // we already gave the account manager a valid access token return true; } - + if (!_oauthProviderURL.isEmpty()) { // check for an access-token in our settings, can optionally be overidden by env value const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token"; const QString ENV_ACCESS_TOKEN_KEY = "DOMAIN_SERVER_ACCESS_TOKEN"; - + QString accessToken = QProcessEnvironment::systemEnvironment().value(ENV_ACCESS_TOKEN_KEY); - + if (accessToken.isEmpty()) { const QVariant* accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); - + if (accessTokenVariant && accessTokenVariant->canConvert(QMetaType::QString)) { accessToken = accessTokenVariant->toString(); } else { @@ -308,17 +308,17 @@ bool DomainServer::didSetupAccountManagerWithAccessToken() { qDebug() << "Using access token from DOMAIN_SERVER_ACCESS_TOKEN in env. This overrides any access token present" << " in the user or master config."; } - + // give this access token to the AccountManager accountManager.setAccessTokenForCurrentAuthURL(accessToken); - + return true; } else { qDebug() << "Missing OAuth provider URL, but a domain-server feature was required that requires authentication." << "domain-server will now quit."; QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); - + return false; } } @@ -326,24 +326,24 @@ bool DomainServer::didSetupAccountManagerWithAccessToken() { bool DomainServer::optionallySetupAssignmentPayment() { const QString PAY_FOR_ASSIGNMENTS_OPTION = "pay-for-assignments"; const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); - + if (settingsMap.contains(PAY_FOR_ASSIGNMENTS_OPTION) && settingsMap.value(PAY_FOR_ASSIGNMENTS_OPTION).toBool() && didSetupAccountManagerWithAccessToken()) { - + qDebug() << "Assignments will be paid for via" << qPrintable(_oauthProviderURL.toString()); - + // assume that the fact we are authing against HF data server means we will pay for assignments // setup a timer to send transactions to pay assigned nodes every 30 seconds QTimer* creditSetupTimer = new QTimer(this); connect(creditSetupTimer, &QTimer::timeout, this, &DomainServer::setupPendingAssignmentCredits); - + const qint64 CREDIT_CHECK_INTERVAL_MSECS = 5 * 1000; creditSetupTimer->start(CREDIT_CHECK_INTERVAL_MSECS); - + QTimer* nodePaymentTimer = new QTimer(this); connect(nodePaymentTimer, &QTimer::timeout, this, &DomainServer::sendPendingTransactionsToServer); - + const qint64 TRANSACTION_SEND_INTERVAL_MSECS = 30 * 1000; nodePaymentTimer->start(TRANSACTION_SEND_INTERVAL_MSECS); } @@ -353,63 +353,63 @@ bool DomainServer::optionallySetupAssignmentPayment() { void DomainServer::setupAutomaticNetworking() { auto nodeList = DependencyManager::get(); - + const int STUN_REFLEXIVE_KEEPALIVE_INTERVAL_MSECS = 10 * 1000; const int STUN_IP_ADDRESS_CHECK_INTERVAL_MSECS = 30 * 1000; // setup our timer to check our IP via stun every X seconds QTimer* dynamicIPTimer = new QTimer(this); connect(dynamicIPTimer, &QTimer::timeout, this, &DomainServer::requestCurrentPublicSocketViaSTUN); - + _automaticNetworkingSetting = _settingsManager.valueOrDefaultValueForKeyPath(METAVERSE_AUTOMATIC_NETWORKING_KEY_PATH).toString(); - + if (_automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) { dynamicIPTimer->start(STUN_REFLEXIVE_KEEPALIVE_INTERVAL_MSECS); - + // setup a timer to heartbeat with the ice-server every so often QTimer* iceHeartbeatTimer = new QTimer(this); connect(iceHeartbeatTimer, &QTimer::timeout, this, &DomainServer::performICEUpdates); iceHeartbeatTimer->start(ICE_HEARBEAT_INTERVAL_MSECS); - + // call our sendHeartbeatToIceServer immediately anytime a local or public socket changes connect(nodeList.data(), &LimitedNodeList::localSockAddrChanged, this, &DomainServer::sendHeartbeatToIceServer); connect(nodeList.data(), &LimitedNodeList::publicSockAddrChanged, this, &DomainServer::sendHeartbeatToIceServer); - + // attempt to update our public socket now, this will send a heartbeat once we get public socket requestCurrentPublicSocketViaSTUN(); - + // in case the STUN lookup is still happening we should re-request a public socket once we get that address connect(&nodeList->getSTUNSockAddr(), &HifiSockAddr::lookupCompleted, this, &DomainServer::requestCurrentPublicSocketViaSTUN); - + } - + if (!didSetupAccountManagerWithAccessToken()) { qDebug() << "Cannot send heartbeat to data server without an access token."; qDebug() << "Add an access token to your config file or via the web interface."; - + return; } - + if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE || _automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) { - + const QUuid& domainID = nodeList->getSessionUUID(); - + if (!domainID.isNull()) { qDebug() << "domain-server" << _automaticNetworkingSetting << "automatic networking enabled for ID" << uuidStringWithoutCurlyBraces(domainID) << "via" << _oauthProviderURL.toString(); - + if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE) { dynamicIPTimer->start(STUN_IP_ADDRESS_CHECK_INTERVAL_MSECS); - + // send public socket changes to the data server so nodes can find us at our new IP connect(nodeList.data(), &LimitedNodeList::publicSockAddrChanged, this, &DomainServer::performIPAddressUpdate); - + // attempt to update our sockets now requestCurrentPublicSocketViaSTUN(); } else { @@ -419,18 +419,18 @@ void DomainServer::setupAutomaticNetworking() { } else { qDebug() << "Cannot enable domain-server automatic networking without a domain ID." << "Please add an ID to your config file or via the web interface."; - + return; } } else { sendHeartbeatToDataServer(); } - + qDebug() << "Updating automatic networking setting in domain-server to" << _automaticNetworkingSetting; - + // no matter the auto networking settings we should heartbeat to the data-server every 15s const int DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS = 15 * 1000; - + QTimer* dataHeartbeatTimer = new QTimer(this); connect(dataHeartbeatTimer, SIGNAL(timeout()), this, SLOT(sendHeartbeatToDataServer())); dataHeartbeatTimer->start(DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS); @@ -445,7 +445,7 @@ void DomainServer::parseAssignmentConfigs(QSet& excludedTypes) // check for configs from the command line, these take precedence const QString ASSIGNMENT_CONFIG_REGEX_STRING = "config-([\\d]+)"; QRegExp assignmentConfigRegex(ASSIGNMENT_CONFIG_REGEX_STRING); - + const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); // scan for assignment config keys @@ -480,26 +480,26 @@ void DomainServer::addStaticAssignmentToAssignmentHash(Assignment* newAssignment void DomainServer::populateStaticScriptedAssignmentsFromSettings() { const QString PERSISTENT_SCRIPTS_KEY_PATH = "scripts.persistent_scripts"; const QVariant* persistentScriptsVariant = valueForKeyPath(_settingsManager.getSettingsMap(), PERSISTENT_SCRIPTS_KEY_PATH); - + if (persistentScriptsVariant) { QVariantList persistentScriptsList = persistentScriptsVariant->toList(); foreach(const QVariant& persistentScriptVariant, persistentScriptsList) { QVariantMap persistentScript = persistentScriptVariant.toMap(); - + const QString PERSISTENT_SCRIPT_URL_KEY = "url"; const QString PERSISTENT_SCRIPT_NUM_INSTANCES_KEY = "num_instances"; const QString PERSISTENT_SCRIPT_POOL_KEY = "pool"; - + if (persistentScript.contains(PERSISTENT_SCRIPT_URL_KEY)) { // check how many instances of this script to add - + int numInstances = persistentScript[PERSISTENT_SCRIPT_NUM_INSTANCES_KEY].toInt(); QString scriptURL = persistentScript[PERSISTENT_SCRIPT_URL_KEY].toString(); - + QString scriptPool = persistentScript.value(PERSISTENT_SCRIPT_POOL_KEY).toString(); - + qDebug() << "Adding" << numInstances << "of persistent script at URL" << scriptURL << "- pool" << scriptPool; - + for (int i = 0; i < numInstances; ++i) { // add a scripted assignment to the queue for this instance Assignment* scriptAssignment = new Assignment(Assignment::CreateCommand, @@ -544,7 +544,7 @@ void DomainServer::createStaticAssignmentsForType(Assignment::Type type, const Q QString dashes = payloadKey.size() == 1 ? "-" : "--"; payloadStringList << QString("%1%2 %3").arg(dashes).arg(payloadKey).arg(configMap[payloadKey].toString()); } - + configAssignment->setPayload(payloadStringList.join(' ').toUtf8()); addStaticAssignmentToAssignmentHash(configAssignment); @@ -557,7 +557,7 @@ void DomainServer::populateDefaultStaticAssignmentsExcludingTypes(const QSet(static_cast(defaultedType) + 1)) { - if (!excludedTypes.contains(defaultedType) + if (!excludedTypes.contains(defaultedType) && defaultedType != Assignment::UNUSED_0 && defaultedType != Assignment::UNUSED_1 && defaultedType != Assignment::UNUSED_2 @@ -577,7 +577,7 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock NodeType_t nodeType; HifiSockAddr publicSockAddr, localSockAddr; - + QDataStream packetStream(packet); packetStream.skipRawData(numBytesForPacketHeader(packet)); @@ -613,11 +613,11 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock } } - + QList nodeInterestList; QString username; QByteArray usernameSignature; - + packetStream >> nodeInterestList >> username >> usernameSignature; auto limitedNodeList = DependencyManager::get(); @@ -630,14 +630,14 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock out << reason; // tell client it has been refused. DependencyManager::get()->writeUnverifiedDatagram(connectionDeniedByteArray, senderSockAddr); - + return; } if ((!isAssignment && !STATICALLY_ASSIGNED_NODES.contains(nodeType)) || (isAssignment && matchingQueuedAssignment)) { // this was either not a static assignment or it was and we had a matching one in the queue - + QUuid nodeUUID; if (_connectingICEPeers.contains(packetUUID) || _connectedICEPeers.contains(packetUUID)) { @@ -656,7 +656,7 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock const QVariant* editorsAreRezzersVariant = valueForKeyPath(_settingsManager.getSettingsMap(), EDITORS_ARE_REZZERS_KEYPATH); - + bool onlyEditorsAreRezzers = false; if (editorsAreRezzersVariant) { onlyEditorsAreRezzers = editorsAreRezzersVariant->toBool(); @@ -691,10 +691,10 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock nodeData->setUsername(username); // also add an interpolation to JSONBreakableMarshal so that servers can get username in stats - JSONBreakableMarshal::addInterpolationForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, + JSONBreakableMarshal::addInterpolationForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, uuidStringWithoutCurlyBraces(nodeUUID), username); } - + nodeData->setSendingSockAddr(senderSockAddr); // reply back to the user with a PacketTypeDomainList @@ -719,16 +719,16 @@ bool DomainServer::verifyUsersKey (const QString& username, const QByteArray& usernameSignature, QString& reasonReturn) { // it's possible this user can be allowed to connect, but we need to check their username signature - + QByteArray publicKeyArray = _userPublicKeys.value(username); if (!publicKeyArray.isEmpty()) { // if we do have a public key for the user, check for a signature match - + const unsigned char* publicKeyData = reinterpret_cast(publicKeyArray.constData()); - + // first load up the public key into an RSA struct RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, publicKeyArray.size()); - + if (rsaPublicKey) { QByteArray decryptedArray(RSA_size(rsaPublicKey), 0); int decryptResult = @@ -736,14 +736,14 @@ bool DomainServer::verifyUsersKey (const QString& username, reinterpret_cast(usernameSignature.constData()), reinterpret_cast(decryptedArray.data()), rsaPublicKey, RSA_PKCS1_PADDING); - + if (decryptResult != -1) { if (username.toLower() == decryptedArray) { qDebug() << "Username signature matches for" << username << "- allowing connection."; - + // free up the public key before we return RSA_free(rsaPublicKey); - + return true; } else { qDebug() << "Username signature did not match for" << username << "- denying connection."; @@ -753,7 +753,7 @@ bool DomainServer::verifyUsersKey (const QString& username, qDebug() << "Couldn't decrypt user signature for" << username << "- denying connection."; reasonReturn = "Couldn't decrypt user signature."; } - + // free up the public key, we don't need it anymore RSA_free(rsaPublicKey); } else { @@ -776,13 +776,13 @@ bool DomainServer::shouldAllowConnectionFromNode(const QString& username, const QVariant* allowedUsersVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ALLOWED_USERS_SETTINGS_KEYPATH); QStringList allowedUsers = allowedUsersVariant ? allowedUsersVariant->toStringList() : QStringList(); - + // we always let in a user who is sending a packet from our local socket or from the localhost address if (senderSockAddr.getAddress() == DependencyManager::get()->getLocalSockAddr().getAddress() || senderSockAddr.getAddress() == QHostAddress::LocalHost) { return true; } - + if (allowedUsers.count() > 0) { if (allowedUsers.contains(username, Qt::CaseInsensitive)) { if (verifyUsersKey(username, usernameSignature, reasonReturn)) { @@ -827,7 +827,7 @@ bool DomainServer::shouldAllowConnectionFromNode(const QString& username, void DomainServer::preloadAllowedUserPublicKeys() { const QVariant* allowedUsersVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ALLOWED_USERS_SETTINGS_KEYPATH); QStringList allowedUsers = allowedUsersVariant ? allowedUsersVariant->toStringList() : QStringList(); - + if (allowedUsers.size() > 0) { // in the future we may need to limit how many requests here - for now assume that lists of allowed users are not // going to create > 100 requests @@ -842,11 +842,11 @@ void DomainServer::requestUserPublicKey(const QString& username) { JSONCallbackParameters callbackParams; callbackParams.jsonCallbackReceiver = this; callbackParams.jsonCallbackMethod = "publicKeyJSONCallback"; - + const QString USER_PUBLIC_KEY_PATH = "api/v1/users/%1/public_key"; - + qDebug() << "Requesting public key for user" << username; - + AccountManager::getInstance().sendRequest(USER_PUBLIC_KEY_PATH.arg(username), AccountManagerAuth::None, QNetworkAccessManager::GetOperation, callbackParams); @@ -940,7 +940,7 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif int numBroadcastPacketLeadBytes = broadcastDataStream.device()->pos(); DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); - + // if we've established a connection via ICE with this peer, use that socket // otherwise just try to reply back to them on their sending socket (although that may not work) HifiSockAddr destinationSockAddr = _connectedICEPeers.value(node->getUUID()); @@ -959,40 +959,40 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif // reset our nodeByteArray and nodeDataStream QByteArray nodeByteArray; QDataStream nodeDataStream(&nodeByteArray, QIODevice::Append); - + if (otherNode->getUUID() != node->getUUID() && nodeInterestList.contains(otherNode->getType())) { - + // don't send avatar nodes to other avatars, that will come from avatar mixer nodeDataStream << *otherNode.data(); - + // pack the secret that these two nodes will use to communicate with each other QUuid secretUUID = nodeData->getSessionSecretHash().value(otherNode->getUUID()); if (secretUUID.isNull()) { // generate a new secret UUID these two nodes can use secretUUID = QUuid::createUuid(); - + // set that on the current Node's sessionSecretHash nodeData->getSessionSecretHash().insert(otherNode->getUUID(), secretUUID); - + // set it on the other Node's sessionSecretHash reinterpret_cast(otherNode->getLinkedData()) ->getSessionSecretHash().insert(node->getUUID(), secretUUID); - + } - + nodeDataStream << secretUUID; - + if (broadcastPacket.size() + nodeByteArray.size() > dataMTU) { // we need to break here and start a new packet // so send the current one - + limitedNodeList->writeUnverifiedDatagram(broadcastPacket, node, senderSockAddr); - + // reset the broadcastPacket structure broadcastPacket.resize(numBroadcastPacketLeadBytes); broadcastDataStream.device()->seek(numBroadcastPacketLeadBytes); } - + // append the nodeByteArray to the current state of broadcastDataStream broadcastPacket.append(nodeByteArray); } @@ -1099,12 +1099,12 @@ void DomainServer::setupPendingAssignmentCredits() { // enumerate the NodeList to find the assigned nodes DependencyManager::get()->eachNode([&](const SharedNodePointer& node){ DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); - + if (!nodeData->getAssignmentUUID().isNull() && !nodeData->getWalletUUID().isNull()) { // check if we have a non-finalized transaction for this node to add this amount to TransactionHash::iterator i = _pendingAssignmentCredits.find(nodeData->getWalletUUID()); WalletTransaction* existingTransaction = NULL; - + while (i != _pendingAssignmentCredits.end() && i.key() == nodeData->getWalletUUID()) { if (!i.value()->isFinalized()) { existingTransaction = i.value(); @@ -1113,16 +1113,16 @@ void DomainServer::setupPendingAssignmentCredits() { ++i; } } - + qint64 elapsedMsecsSinceLastPayment = nodeData->getPaymentIntervalTimer().elapsed(); nodeData->getPaymentIntervalTimer().restart(); - + const float CREDITS_PER_HOUR = 0.10f; const float CREDITS_PER_MSEC = CREDITS_PER_HOUR / (60 * 60 * 1000); const int SATOSHIS_PER_MSEC = CREDITS_PER_MSEC * SATOSHIS_PER_CREDIT; - + float pendingCredits = elapsedMsecsSinceLastPayment * SATOSHIS_PER_MSEC; - + if (existingTransaction) { existingTransaction->incrementAmount(pendingCredits); } else { @@ -1164,22 +1164,22 @@ void DomainServer::sendPendingTransactionsToServer() { void DomainServer::publicKeyJSONCallback(QNetworkReply& requestReply) { QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); - + if (jsonObject["status"].toString() == "success") { // figure out which user this is for - + const QString PUBLIC_KEY_URL_REGEX_STRING = "api\\/v1\\/users\\/([A-Za-z0-9_\\.]+)\\/public_key"; QRegExp usernameRegex(PUBLIC_KEY_URL_REGEX_STRING); - + if (usernameRegex.indexIn(requestReply.url().toString()) != -1) { QString username = usernameRegex.cap(1); - + qDebug() << "Storing a public key for user" << username; - + // pull the public key as a QByteArray from this response const QString JSON_DATA_KEY = "data"; const QString JSON_PUBLIC_KEY_KEY = "public_key"; - + _userPublicKeys[username] = QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8()); } @@ -1219,11 +1219,11 @@ void DomainServer::requestCurrentPublicSocketViaSTUN() { QJsonObject jsonForDomainSocketUpdate(const HifiSockAddr& socket) { const QString SOCKET_NETWORK_ADDRESS_KEY = "network_address"; const QString SOCKET_PORT_KEY = "port"; - + QJsonObject socketObject; socketObject[SOCKET_NETWORK_ADDRESS_KEY] = socket.getAddress().toString(); socketObject[SOCKET_PORT_KEY] = socket.getPort(); - + return socketObject; } @@ -1237,44 +1237,44 @@ void DomainServer::sendHeartbeatToDataServer(const QString& networkAddress) { const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; auto nodeList = DependencyManager::get(); const QUuid& domainID = nodeList->getSessionUUID(); - + // setup the domain object to send to the data server const QString PUBLIC_NETWORK_ADDRESS_KEY = "network_address"; const QString AUTOMATIC_NETWORKING_KEY = "automatic_networking"; - + QJsonObject domainObject; if (!networkAddress.isEmpty()) { domainObject[PUBLIC_NETWORK_ADDRESS_KEY] = networkAddress; } - + domainObject[AUTOMATIC_NETWORKING_KEY] = _automaticNetworkingSetting; - + // add a flag to indicate if this domain uses restricted access - for now that will exclude it from listings const QString RESTRICTED_ACCESS_FLAG = "restricted"; - + const QVariant* allowedUsersVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ALLOWED_USERS_SETTINGS_KEYPATH); QStringList allowedUsers = allowedUsersVariant ? allowedUsersVariant->toStringList() : QStringList(); domainObject[RESTRICTED_ACCESS_FLAG] = (allowedUsers.size() > 0); - + // add the number of currently connected agent users int numConnectedAuthedUsers = 0; - + nodeList->eachNode([&numConnectedAuthedUsers](const SharedNodePointer& node){ if (node->getLinkedData() && !static_cast(node->getLinkedData())->getUsername().isEmpty()) { ++numConnectedAuthedUsers; } }); - + const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; const QString HEARTBEAT_NUM_USERS_KEY = "num_users"; - + QJsonObject heartbeatObject; heartbeatObject[HEARTBEAT_NUM_USERS_KEY] = numConnectedAuthedUsers; domainObject[DOMAIN_HEARTBEAT_KEY] = heartbeatObject; - + QString domainUpdateJSON = QString("{\"domain\": %1 }").arg(QString(QJsonDocument(domainObject).toJson())); - + AccountManager::getInstance().sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), AccountManagerAuth::Required, QNetworkAccessManager::PutOperation, @@ -1295,11 +1295,11 @@ void DomainServer::sendHeartbeatToIceServer() { void DomainServer::sendICEPingPackets() { auto nodeList = DependencyManager::get(); - + QHash::iterator peer = _connectingICEPeers.begin(); - + while (peer != _connectingICEPeers.end()) { - + if (peer->getConnectionAttempts() >= MAX_ICE_CONNECTION_ATTEMPTS) { // we've already tried to connect to this peer enough times // remove it from our list - if it wants to re-connect it'll come back through ice-server @@ -1308,16 +1308,16 @@ void DomainServer::sendICEPingPackets() { // send ping packets to this peer's interfaces qDebug() << "Sending ping packets to establish connectivity with ICE peer with ID" << peer->getUUID(); - + // send the ping packet to the local and public sockets for this node QByteArray localPingPacket = nodeList->constructPingPacket(PingType::Local, false); nodeList->writeUnverifiedDatagram(localPingPacket, peer->getLocalSocket()); - + QByteArray publicPingPacket = nodeList->constructPingPacket(PingType::Public, false); nodeList->writeUnverifiedDatagram(publicPingPacket, peer->getPublicSocket()); - + peer->incrementConnectionAttempts(); - + // go to next peer in hash ++peer; } @@ -1329,17 +1329,17 @@ void DomainServer::processICEHeartbeatResponse(const QByteArray& packet) { // any peer we don't have we add to the hash, otherwise we update QDataStream iceResponseStream(packet); iceResponseStream.skipRawData(numBytesForPacketHeader(packet)); - + NetworkPeer receivedPeer; - + while (!iceResponseStream.atEnd()) { iceResponseStream >> receivedPeer; - + if (!_connectedICEPeers.contains(receivedPeer.getUUID())) { if (!_connectingICEPeers.contains(receivedPeer.getUUID())) { qDebug() << "New peer requesting connection being added to hash -" << receivedPeer; } - + _connectingICEPeers[receivedPeer.getUUID()] = receivedPeer; } } @@ -1348,7 +1348,7 @@ void DomainServer::processICEHeartbeatResponse(const QByteArray& packet) { void DomainServer::processICEPingReply(const QByteArray& packet, const HifiSockAddr& senderSockAddr) { QUuid nodeUUID = uuidFromPacketHeader(packet); NetworkPeer sendingPeer = _connectingICEPeers.take(nodeUUID); - + if (!sendingPeer.isNull()) { // we had this NetworkPeer in our connecting list - add the right sock addr to our connected list if (senderSockAddr == sendingPeer.getLocalSocket()) { @@ -1372,31 +1372,36 @@ void DomainServer::processDatagram(const QByteArray& receivedPacket, const HifiS break; case PacketTypeDomainListRequest: { QUuid nodeUUID = uuidFromPacketHeader(receivedPacket); - + if (!nodeUUID.isNull() && nodeList->nodeWithUUID(nodeUUID)) { NodeType_t throwawayNodeType; HifiSockAddr nodePublicAddress, nodeLocalAddress; - + QDataStream packetStream(receivedPacket); packetStream.skipRawData(numBytesForPacketHeader(receivedPacket)); - + parseNodeDataFromByteArray(packetStream, throwawayNodeType, nodePublicAddress, nodeLocalAddress, senderSockAddr); - + SharedNodePointer checkInNode = nodeList->nodeWithUUID(nodeUUID); checkInNode->setPublicSocket(nodePublicAddress); checkInNode->setLocalSocket(nodeLocalAddress); - + // update last receive to now quint64 timeNow = usecTimestampNow(); checkInNode->setLastHeardMicrostamp(timeNow); - + QList nodeInterestList; packetStream >> nodeInterestList; - + sendDomainListToNode(checkInNode, senderSockAddr, nodeInterestList.toSet()); } - + + break; + } + case PacketTypeDomainServerPathQuery: { + // have our private method attempt to respond to this path query + respondToPathQuery(receivedPacket, senderSockAddr); break; } case PacketTypeNodeJsonStats: { @@ -1404,7 +1409,7 @@ void DomainServer::processDatagram(const QByteArray& receivedPacket, const HifiS if (matchingNode) { reinterpret_cast(matchingNode->getLinkedData())->parseJSONStatsPacket(receivedPacket); } - + break; } case PacketTypeStunResponse: @@ -1413,7 +1418,7 @@ void DomainServer::processDatagram(const QByteArray& receivedPacket, const HifiS case PacketTypeUnverifiedPing: { QByteArray pingReplyPacket = nodeList->constructPingReplyPacket(receivedPacket); nodeList->writeUnverifiedDatagram(pingReplyPacket, senderSockAddr); - + break; } case PacketTypeUnverifiedPingReply: { @@ -1470,10 +1475,10 @@ QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) { // if the node has pool information, add it DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); - + // add the node username, if it exists nodeJson[JSON_KEY_USERNAME] = nodeData->getUsername(); - + SharedAssignmentPointer matchingAssignment = _allAssignments.value(nodeData->getAssignmentUUID()); if (matchingAssignment) { nodeJson[JSON_KEY_POOL] = matchingAssignment->getPool(); @@ -1512,64 +1517,64 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url const QString URI_NODES = "/nodes"; const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; - + auto nodeList = DependencyManager::get(); - + // allow sub-handlers to handle requests that do not require authentication if (_settingsManager.handlePublicHTTPRequest(connection, url)) { return true; } - + // check if this is a request for a scripted assignment (with a temp unique UUID) const QString ASSIGNMENT_REGEX_STRING = QString("\\%1\\/(%2)\\/?$").arg(URI_ASSIGNMENT).arg(UUID_REGEX_STRING); QRegExp assignmentRegex(ASSIGNMENT_REGEX_STRING); - + if (connection->requestOperation() == QNetworkAccessManager::GetOperation && assignmentRegex.indexIn(url.path()) != -1) { QUuid matchingUUID = QUuid(assignmentRegex.cap(1)); - + SharedAssignmentPointer matchingAssignment = _allAssignments.value(matchingUUID); if (!matchingAssignment) { // check if we have a pending assignment that matches this temp UUID, and it is a scripted assignment PendingAssignedNodeData* pendingData = _pendingAssignedNodes.value(matchingUUID); if (pendingData) { matchingAssignment = _allAssignments.value(pendingData->getAssignmentUUID()); - + if (matchingAssignment && matchingAssignment->getType() == Assignment::AgentType) { // we have a matching assignment and it is for the right type, have the HTTP manager handle it // via correct URL for the script so the client can download - + QUrl scriptURL = url; scriptURL.setPath(URI_ASSIGNMENT + "/scripts/" + uuidStringWithoutCurlyBraces(pendingData->getAssignmentUUID())); - + // have the HTTPManager serve the appropriate script file return _httpManager.handleHTTPRequest(connection, scriptURL, true); } } } - + // request not handled return false; } - + // check if this is a request for our domain ID const QString URI_ID = "/id"; if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == URI_ID) { QUuid domainID = nodeList->getSessionUUID(); - + connection->respond(HTTPConnection::StatusCode200, uuidStringWithoutCurlyBraces(domainID).toLocal8Bit()); return true; } - + // all requests below require a cookie to prove authentication so check that first if (!isAuthenticatedRequest(connection, url)) { // this is not an authenticated request // return true from the handler since it was handled with a 401 or re-direct to auth return true; } - + if (connection->requestOperation() == QNetworkAccessManager::GetOperation) { if (url.path() == "/assignments.json") { // user is asking for json list of assignments @@ -1581,7 +1586,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url // enumerate the NodeList to find the assigned nodes nodeList->eachNode([this, &assignedNodesJSON](const SharedNodePointer& node){ DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); - + if (!nodeData->getAssignmentUUID().isNull()) { // add the node using the UUID as the key QString uuidString = uuidStringWithoutCurlyBraces(nodeData->getAssignmentUUID()); @@ -1725,10 +1730,10 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url QFile scriptFile(newPath); if (scriptFile.open(QIODevice::WriteOnly)) { scriptFile.write(formData[0].second); - + qDebug() << qPrintable(QString("Saved a script for assignment at %1%2") .arg(newPath).arg(assignmentPool == emptyPool ? "" : " - pool is " + assignmentPool)); - + // add the script assigment to the assignment queue SharedAssignmentPointer sharedScriptedAssignment(scriptAssignment); _unfulfilledAssignments.enqueue(sharedScriptedAssignment); @@ -1814,40 +1819,40 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u QNetworkRequest tokenRequest(tokenRequestUrl); tokenRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - + QNetworkReply* tokenReply = NetworkAccessManager::getInstance().post(tokenRequest, tokenPostBody.toLocal8Bit()); - + if (_webAuthenticationStateSet.remove(stateUUID)) { // this is a web user who wants to auth to access web interface // we hold the response back to them until we get their profile information // and can decide if they are let in or not - + QEventLoop loop; connect(tokenReply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - + // start the loop for the token request loop.exec(); - + QNetworkReply* profileReply = profileRequestGivenTokenReply(tokenReply); - + // stop the loop once the profileReply is complete connect(profileReply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - + // restart the loop for the profile request loop.exec(); - + // call helper method to get cookieHeaders Headers cookieHeaders = setupCookieHeadersFromProfileReply(profileReply); - + connection->respond(HTTPConnection::StatusCode302, QByteArray(), HTTPConnection::DefaultContentType, cookieHeaders); - + delete tokenReply; delete profileReply; - + // we've redirected the user back to our homepage return true; - + } } @@ -1861,47 +1866,47 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u } bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl& url) { - + const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie"; const QString ADMIN_USERS_CONFIG_KEY = "admin-users"; const QString ADMIN_ROLES_CONFIG_KEY = "admin-roles"; const QString BASIC_AUTH_USERNAME_KEY_PATH = "security.http_username"; const QString BASIC_AUTH_PASSWORD_KEY_PATH = "security.http_password"; - + const QByteArray UNAUTHENTICATED_BODY = "You do not have permission to access this domain-server."; - + QVariantMap& settingsMap = _settingsManager.getSettingsMap(); - + if (!_oauthProviderURL.isEmpty() && (settingsMap.contains(ADMIN_USERS_CONFIG_KEY) || settingsMap.contains(ADMIN_ROLES_CONFIG_KEY))) { QString cookieString = connection->requestHeaders().value(HTTP_COOKIE_HEADER_KEY); - + const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)"; QRegExp cookieUUIDRegex(COOKIE_UUID_REGEX_STRING); - + QUuid cookieUUID; if (cookieString.indexOf(cookieUUIDRegex) != -1) { cookieUUID = cookieUUIDRegex.cap(1); } - + if (valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)) { qDebug() << "Config file contains web admin settings for OAuth and basic HTTP authentication." << "These cannot be combined - using OAuth for authentication."; } - + if (!cookieUUID.isNull() && _cookieSessionHash.contains(cookieUUID)) { // pull the QJSONObject for the user with this cookie UUID DomainServerWebSessionData sessionData = _cookieSessionHash.value(cookieUUID); QString profileUsername = sessionData.getUsername(); - + if (settingsMap.value(ADMIN_USERS_CONFIG_KEY).toStringList().contains(profileUsername)) { // this is an authenticated user return true; } - + // loop the roles of this user and see if they are in the admin-roles array QStringList adminRolesArray = settingsMap.value(ADMIN_ROLES_CONFIG_KEY).toStringList(); - + if (!adminRolesArray.isEmpty()) { foreach(const QString& userRole, sessionData.getRoles()) { if (adminRolesArray.contains(userRole)) { @@ -1910,79 +1915,79 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl } } } - + connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY); - + // the user does not have allowed username or role, return 401 return false; } else { // re-direct this user to OAuth page - + // generate a random state UUID to use QUuid stateUUID = QUuid::createUuid(); - + // add it to the set so we can handle the callback from the OAuth provider _webAuthenticationStateSet.insert(stateUUID); - + QUrl oauthRedirectURL = oauthAuthorizationURL(stateUUID); - + Headers redirectHeaders; redirectHeaders.insert("Location", oauthRedirectURL.toEncoded()); - + connection->respond(HTTPConnection::StatusCode302, QByteArray(), HTTPConnection::DefaultContentType, redirectHeaders); - + // we don't know about this user yet, so they are not yet authenticated return false; } } else if (valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)) { // config file contains username and password combinations for basic auth const QByteArray BASIC_AUTH_HEADER_KEY = "Authorization"; - + // check if a username and password have been provided with the request QString basicAuthString = connection->requestHeaders().value(BASIC_AUTH_HEADER_KEY); - + if (!basicAuthString.isEmpty()) { QStringList splitAuthString = basicAuthString.split(' '); QString base64String = splitAuthString.size() == 2 ? splitAuthString[1] : ""; QString credentialString = QByteArray::fromBase64(base64String.toLocal8Bit()); - + if (!credentialString.isEmpty()) { QStringList credentialList = credentialString.split(':'); if (credentialList.size() == 2) { QString headerUsername = credentialList[0]; QString headerPassword = credentialList[1]; - + // we've pulled a username and password - now check if there is a match in our basic auth hash QString settingsUsername = valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)->toString(); const QVariant* settingsPasswordVariant = valueForKeyPath(settingsMap, BASIC_AUTH_PASSWORD_KEY_PATH); QString settingsPassword = settingsPasswordVariant ? settingsPasswordVariant->toString() : ""; - + if (settingsUsername == headerUsername && headerPassword == settingsPassword) { return true; } } } } - + // basic HTTP auth being used but no username and password are present // or the username and password are not correct // send back a 401 and ask for basic auth - + const QByteArray HTTP_AUTH_REQUEST_HEADER_KEY = "WWW-Authenticate"; static QString HTTP_AUTH_REALM_STRING = QString("Basic realm='%1 %2'") .arg(_hostname.isEmpty() ? "localhost" : _hostname) .arg("domain-server"); - + Headers basicAuthHeader; basicAuthHeader.insert(HTTP_AUTH_REQUEST_HEADER_KEY, HTTP_AUTH_REALM_STRING.toUtf8()); - + connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY, HTTPConnection::DefaultContentType, basicAuthHeader); - + // not authenticated, bubble up false return false; - + } else { // we don't have an OAuth URL + admin roles/usernames, so all users are authenticated return true; @@ -1996,12 +2001,12 @@ QNetworkReply* DomainServer::profileRequestGivenTokenReply(QNetworkReply* tokenR // pull the access token from the returned JSON and store it with the matching session UUID QJsonDocument returnedJSON = QJsonDocument::fromJson(tokenReply->readAll()); QString accessToken = returnedJSON.object()[OAUTH_JSON_ACCESS_TOKEN_KEY].toString(); - + // fire off a request to get this user's identity so we can see if we will let them in QUrl profileURL = _oauthProviderURL; profileURL.setPath("/api/v1/user/profile"); profileURL.setQuery(QString("%1=%2").arg(OAUTH_JSON_ACCESS_TOKEN_KEY, accessToken)); - + QNetworkRequest profileRequest(profileURL); profileRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); return NetworkAccessManager::getInstance().get(profileRequest); @@ -2011,34 +2016,34 @@ const QString DS_SETTINGS_SESSIONS_GROUP = "web-sessions"; Headers DomainServer::setupCookieHeadersFromProfileReply(QNetworkReply* profileReply) { Headers cookieHeaders; - + // create a UUID for this cookie QUuid cookieUUID = QUuid::createUuid(); - + QJsonDocument profileDocument = QJsonDocument::fromJson(profileReply->readAll()); QJsonObject userObject = profileDocument.object()["data"].toObject()["user"].toObject(); - + // add the profile to our in-memory data structure so we know who the user is when they send us their cookie DomainServerWebSessionData sessionData(userObject); _cookieSessionHash.insert(cookieUUID, sessionData); - + // persist the cookie to settings file so we can get it back on DS relaunch QStringList path = QStringList() << DS_SETTINGS_SESSIONS_GROUP << cookieUUID.toString(); Setting::Handle(path).set(QVariant::fromValue(sessionData)); - + // setup expiry for cookie to 1 month from today QDateTime cookieExpiry = QDateTime::currentDateTimeUtc().addMonths(1); - + QString cookieString = HIFI_SESSION_COOKIE_KEY + "=" + uuidStringWithoutCurlyBraces(cookieUUID.toString()); cookieString += "; expires=" + cookieExpiry.toString("ddd, dd MMM yyyy HH:mm:ss") + " GMT"; cookieString += "; domain=" + _hostname + "; path=/"; - + cookieHeaders.insert("Set-Cookie", cookieString.toUtf8()); - + // redirect the user back to the homepage so they can present their cookie and be authenticated QString redirectString = "http://" + _hostname + ":" + QString::number(_httpManager.serverPort()); cookieHeaders.insert("Location", redirectString.toUtf8()); - + return cookieHeaders; } @@ -2046,7 +2051,7 @@ void DomainServer::loadExistingSessionsFromSettings() { // read data for existing web sessions into memory so existing sessions can be leveraged Settings domainServerSettings; domainServerSettings.beginGroup(DS_SETTINGS_SESSIONS_GROUP); - + foreach(const QString& uuidKey, domainServerSettings.childKeys()) { _cookieSessionHash.insert(QUuid(uuidKey), domainServerSettings.value(uuidKey).value()); @@ -2077,7 +2082,7 @@ void DomainServer::nodeAdded(SharedNodePointer node) { } void DomainServer::nodeKilled(SharedNodePointer node) { - + // remove this node from the connecting / connected ICE lists (if they exist) _connectingICEPeers.remove(node->getUUID()); _connectedICEPeers.remove(node->getUUID()); @@ -2095,7 +2100,7 @@ void DomainServer::nodeKilled(SharedNodePointer node) { } // If this node was an Agent ask JSONBreakableMarshal to potentially remove the interpolation we stored - JSONBreakableMarshal::removeInterpolationForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, + JSONBreakableMarshal::removeInterpolationForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, uuidStringWithoutCurlyBraces(node->getUUID())); // cleanup the connection secrets that we set up for this node (on the other nodes) @@ -2181,7 +2186,7 @@ void DomainServer::addStaticAssignmentsToQueue() { QHash::iterator staticAssignment = staticHashCopy.begin(); while (staticAssignment != staticHashCopy.end()) { // add any of the un-matched static assignments to the queue - + // enumerate the nodes and check if there is one with an attached assignment with matching UUID if (!DependencyManager::get()->nodeWithUUID(staticAssignment->data()->getUUID())) { // this assignment has not been fulfilled - reset the UUID and add it to the assignment queue @@ -2191,3 +2196,75 @@ void DomainServer::addStaticAssignmentsToQueue() { ++staticAssignment; } } + +void DomainServer::respondToPathQuery(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr) { + // this is a query for the viewpoint resulting from a path + // first pull the query path from the packet + + int numHeaderBytes = numBytesForPacketHeaderGivenPacketType(PacketTypeDomainServerPathQuery); + const char* packetDataStart = receivedPacket.data() + numHeaderBytes; + + // figure out how many bytes the sender said this path is + quint16 numPathBytes = *packetDataStart; + + if (numPathBytes <= receivedPacket.size() - numHeaderBytes - sizeof(numPathBytes)) { + // the number of path bytes makes sense for the sent packet - pull out the path + QString pathQuery = QString::fromUtf8(packetDataStart + sizeof(numPathBytes), numPathBytes); + + // our settings contain paths that start with a leading slash, so make sure this query has that + if (!pathQuery.startsWith("/")) { + pathQuery.prepend("/"); + } + + const QString PATHS_SETTINGS_KEYPATH_FORMAT = "%1.%2"; + const QString PATH_VIEWPOINT_KEY = "viewpoint"; + + // check out paths in the _configMap to see if we have a match + const QVariant* pathMatch = valueForKeyPath(_settingsManager.getSettingsMap(), + QString(PATHS_SETTINGS_KEYPATH_FORMAT).arg(SETTINGS_PATHS_KEY) + .arg(pathQuery)); + if (pathMatch) { + // we got a match, respond with the resulting viewpoint + auto nodeList = DependencyManager::get(); + + QString responseViewpoint = pathMatch->toMap()[PATH_VIEWPOINT_KEY].toString(); + + if (!responseViewpoint.isEmpty()) { + QByteArray viewpointUTF8 = responseViewpoint.toUtf8(); + + // prepare a packet for the response + QByteArray pathResponsePacket = nodeList->byteArrayWithPopulatedHeader(PacketTypeDomainServerPathResponse); + + // check the number of bytes the viewpoint is + quint16 numViewpointBytes = responseViewpoint.toUtf8().size(); + + // are we going to be able to fit this response viewpoint in a packet? + if (numPathBytes + numViewpointBytes + pathResponsePacket.size() + sizeof(numViewpointBytes) + < MAX_PACKET_SIZE) { + // append the number of bytes this path is + pathResponsePacket.append(reinterpret_cast(&numPathBytes), sizeof(numPathBytes)); + + // append the path itself + pathResponsePacket.append(pathQuery.toUtf8()); + + // append the number of bytes the resulting viewpoint is + pathResponsePacket.append(reinterpret_cast(&numViewpointBytes), sizeof(numViewpointBytes)); + + // append the viewpoint itself + pathResponsePacket.append(viewpointUTF8); + + qDebug() << "Sending a viewpoint response for path query" << pathQuery << "-" << viewpointUTF8; + + // send off the packet - see if we can associate this outbound data to a particular node + // TODO: does this senderSockAddr always work for a punched DS client? + nodeList->writeUnverifiedDatagram(pathResponsePacket, senderSockAddr); + } + } + + } else { + // we don't respond if there is no match - this may need to change once this packet + // query/response is made reliable + qDebug() << "No match for path query" << pathQuery << "- refusing to respond."; + } + } +} diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 79303ef098..3863084191 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -39,29 +39,29 @@ class DomainServer : public QCoreApplication, public HTTPSRequestHandler { Q_OBJECT public: DomainServer(int argc, char* argv[]); - + static int const EXIT_CODE_REBOOT; - + bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false); bool handleHTTPSRequest(HTTPSConnection* connection, const QUrl& url, bool skipSubHandler = false); - + public slots: /// Called by NodeList to inform us a node has been added void nodeAdded(SharedNodePointer node); /// Called by NodeList to inform us a node has been killed void nodeKilled(SharedNodePointer node); - + void publicKeyJSONCallback(QNetworkReply& requestReply); void transactionJSONCallback(const QJsonObject& data); - + void restart(); - + private slots: void loginFailed(); void readAvailableDatagrams(); void setupPendingAssignmentCredits(); void sendPendingTransactionsToServer(); - + void requestCurrentPublicSocketViaSTUN(); void performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr); void performICEUpdates(); @@ -74,23 +74,23 @@ private: bool optionallyReadX509KeyAndCertificate(); bool didSetupAccountManagerWithAccessToken(); bool optionallySetupAssignmentPayment(); - + void setupAutomaticNetworking(); void sendHeartbeatToDataServer(const QString& networkAddress); void processICEPingReply(const QByteArray& packet, const HifiSockAddr& senderSockAddr); void processICEHeartbeatResponse(const QByteArray& packet); - + void processDatagram(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr); - + void handleConnectRequest(const QByteArray& packet, const HifiSockAddr& senderSockAddr); unsigned int countConnectedUsers(); bool verifyUsersKey (const QString& username, const QByteArray& usernameSignature, QString& reasonReturn); bool shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr, QString& reasonReturn); - + void preloadAllowedUserPublicKeys(); void requestUserPublicKey(const QString& username); - + int parseNodeDataFromByteArray(QDataStream& packetStream, NodeType_t& nodeType, HifiSockAddr& publicSockAddr, @@ -99,61 +99,63 @@ private: NodeSet nodeInterestListFromPacket(const QByteArray& packet, int numPreceedingBytes); void sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr& senderSockAddr, const NodeSet& nodeInterestList); - + void parseAssignmentConfigs(QSet& excludedTypes); void addStaticAssignmentToAssignmentHash(Assignment* newAssignment); void createStaticAssignmentsForType(Assignment::Type type, const QVariantList& configList); void populateDefaultStaticAssignmentsExcludingTypes(const QSet& excludedTypes); void populateStaticScriptedAssignmentsFromSettings(); - + SharedAssignmentPointer matchingQueuedAssignmentForCheckIn(const QUuid& checkInUUID, NodeType_t nodeType); SharedAssignmentPointer deployableAssignmentForRequest(const Assignment& requestAssignment); void removeMatchingAssignmentFromQueue(const SharedAssignmentPointer& removableAssignment); void refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer& assignment); void addStaticAssignmentsToQueue(); - + + void respondToPathQuery(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr); + QUrl oauthRedirectURL(); QUrl oauthAuthorizationURL(const QUuid& stateUUID = QUuid::createUuid()); - + bool isAuthenticatedRequest(HTTPConnection* connection, const QUrl& url); void handleTokenRequestFinished(); QNetworkReply* profileRequestGivenTokenReply(QNetworkReply* tokenReply); void handleProfileRequestFinished(); Headers setupCookieHeadersFromProfileReply(QNetworkReply* profileReply); - + void loadExistingSessionsFromSettings(); - + QJsonObject jsonForSocket(const HifiSockAddr& socket); QJsonObject jsonObjectForNode(const SharedNodePointer& node); - + HTTPManager _httpManager; HTTPSManager* _httpsManager; - + QHash _allAssignments; QQueue _unfulfilledAssignments; QHash _pendingAssignedNodes; TransactionHash _pendingAssignmentCredits; - + bool _isUsingDTLS; - + QUrl _oauthProviderURL; QString _oauthClientID; QString _oauthClientSecret; QString _hostname; - + QSet _webAuthenticationStateSet; QHash _cookieSessionHash; - + QHash _userPublicKeys; - + QHash _connectingICEPeers; QHash _connectedICEPeers; - + QString _automaticNetworkingSetting; - + DomainServerSettingsManager _settingsManager; - + HifiSockAddr _iceServerSocket; }; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index f29b1f48ae..5539b6b564 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -32,6 +32,8 @@ const QString DESCRIPTION_NAME_KEY = "name"; const QString SETTING_DESCRIPTION_TYPE_KEY = "type"; const QString DESCRIPTION_COLUMNS_KEY = "columns"; +const QString SETTINGS_VIEWPOINT_KEY = "viewpoint"; + DomainServerSettingsManager::DomainServerSettingsManager() : _descriptionArray(), _configMap() @@ -39,31 +41,31 @@ DomainServerSettingsManager::DomainServerSettingsManager() : // load the description object from the settings description QFile descriptionFile(QCoreApplication::applicationDirPath() + SETTINGS_DESCRIPTION_RELATIVE_PATH); descriptionFile.open(QIODevice::ReadOnly); - + _descriptionArray = QJsonDocument::fromJson(descriptionFile.readAll()).array(); } void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList) { _configMap.loadMasterAndUserConfig(argumentList); - + // for now we perform a temporary transition from http-username and http-password to http_username and http_password const QVariant* oldUsername = valueForKeyPath(_configMap.getUserConfig(), "security.http-username"); const QVariant* oldPassword = valueForKeyPath(_configMap.getUserConfig(), "security.http-password"); - + if (oldUsername || oldPassword) { QVariantMap& settingsMap = *reinterpret_cast(_configMap.getUserConfig()["security"].data()); - + // remove old keys, move to new format if (oldUsername) { settingsMap["http_username"] = oldUsername->toString(); settingsMap.remove("http-username"); } - + if (oldPassword) { settingsMap["http_password"] = oldPassword->toString(); settingsMap.remove("http-password"); } - + // save the updated settings persistToFile(); } @@ -71,18 +73,18 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString &keyPath) { const QVariant* foundValue = valueForKeyPath(_configMap.getMergedConfig(), keyPath); - + if (foundValue) { return *foundValue; } else { int dotIndex = keyPath.indexOf('.'); - + QString groupKey = keyPath.mid(0, dotIndex); QString settingKey = keyPath.mid(dotIndex + 1); - + foreach(const QVariant& group, _descriptionArray.toVariantList()) { QVariantMap groupMap = group.toMap(); - + if (groupMap[DESCRIPTION_NAME_KEY].toString() == groupKey) { foreach(const QVariant& setting, groupMap[DESCRIPTION_SETTINGS_KEY].toList()) { QVariantMap settingMap = setting.toMap(); @@ -90,12 +92,12 @@ QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QStrin return settingMap[SETTING_DEFAULT_KEY]; } } - + return QVariant(); } } } - + return QVariant(); } @@ -104,23 +106,23 @@ const QString SETTINGS_PATH = "/settings.json"; bool DomainServerSettingsManager::handlePublicHTTPRequest(HTTPConnection* connection, const QUrl &url) { if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH) { // this is a GET operation for our settings - + // check if there is a query parameter for settings affecting a particular type of assignment const QString SETTINGS_TYPE_QUERY_KEY = "type"; QUrlQuery settingsQuery(url); QString typeValue = settingsQuery.queryItemValue(SETTINGS_TYPE_QUERY_KEY); - + if (!typeValue.isEmpty()) { QJsonObject responseObject = responseObjectForType(typeValue); - + connection->respond(HTTPConnection::StatusCode200, QJsonDocument(responseObject).toJson(), "application/json"); - + return true; } else { return false; } } - + return false; } @@ -129,130 +131,125 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection // this is a POST operation to change one or more settings QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent()); QJsonObject postedObject = postedDocument.object(); - - qDebug() << "The postedObject is" << postedObject; + + qDebug() << "DomainServerSettingsManager postedObject -" << postedObject; // we recurse one level deep below each group for the appropriate setting - recurseJSONObjectAndOverwriteSettings(postedObject, _configMap.getUserConfig(), _descriptionArray); - + recurseJSONObjectAndOverwriteSettings(postedObject, _configMap.getUserConfig()); + // store whatever the current _settingsMap is to file persistToFile(); - + // return success to the caller QString jsonSuccess = "{\"status\": \"success\"}"; connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json"); - + // defer a restart to the domain-server, this gives our HTTPConnection enough time to respond const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000; QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart())); - + return true; } else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH) { // setup a JSON Object with descriptions and non-omitted settings const QString SETTINGS_RESPONSE_DESCRIPTION_KEY = "descriptions"; const QString SETTINGS_RESPONSE_VALUE_KEY = "values"; const QString SETTINGS_RESPONSE_LOCKED_VALUES_KEY = "locked"; - + QJsonObject rootObject; rootObject[SETTINGS_RESPONSE_DESCRIPTION_KEY] = _descriptionArray; rootObject[SETTINGS_RESPONSE_VALUE_KEY] = responseObjectForType("", true); rootObject[SETTINGS_RESPONSE_LOCKED_VALUES_KEY] = QJsonDocument::fromVariant(_configMap.getMasterConfig()).object(); - - + + connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json"); } - + return false; } QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& typeValue, bool isAuthenticated) { QJsonObject responseObject; - + if (!typeValue.isEmpty() || isAuthenticated) { // convert the string type value to a QJsonValue QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt()); - + const QString AFFECTED_TYPES_JSON_KEY = "assignment-types"; - + // enumerate the groups in the description object to find which settings to pass foreach(const QJsonValue& groupValue, _descriptionArray) { QJsonObject groupObject = groupValue.toObject(); QString groupKey = groupObject[DESCRIPTION_NAME_KEY].toString(); QJsonArray groupSettingsArray = groupObject[DESCRIPTION_SETTINGS_KEY].toArray(); - + QJsonObject groupResponseObject; - + foreach(const QJsonValue& settingValue, groupSettingsArray) { const QString VALUE_HIDDEN_FLAG_KEY = "value-hidden"; - + QJsonObject settingObject = settingValue.toObject(); - + if (!settingObject[VALUE_HIDDEN_FLAG_KEY].toBool()) { QJsonArray affectedTypesArray = settingObject[AFFECTED_TYPES_JSON_KEY].toArray(); if (affectedTypesArray.isEmpty()) { affectedTypesArray = groupObject[AFFECTED_TYPES_JSON_KEY].toArray(); } - + if (affectedTypesArray.contains(queryType) || (queryType.isNull() && isAuthenticated)) { // this is a setting we should include in the responseObject - + QString settingName = settingObject[DESCRIPTION_NAME_KEY].toString(); - + // we need to check if the settings map has a value for this setting QVariant variantValue; - QVariant settingsMapGroupValue = _configMap.getMergedConfig() - .value(groupObject[DESCRIPTION_NAME_KEY].toString()); - - if (!settingsMapGroupValue.isNull()) { - variantValue = settingsMapGroupValue.toMap().value(settingName); + + if (!groupKey.isEmpty()) { + QVariant settingsMapGroupValue = _configMap.getMergedConfig().value(groupKey); + + if (!settingsMapGroupValue.isNull()) { + variantValue = settingsMapGroupValue.toMap().value(settingName); + } + } else { + variantValue = _configMap.getMergedConfig().value(settingName); } - + + QJsonValue result; + if (variantValue.isNull()) { // no value for this setting, pass the default if (settingObject.contains(SETTING_DEFAULT_KEY)) { - groupResponseObject[settingName] = settingObject[SETTING_DEFAULT_KEY]; + result = settingObject[SETTING_DEFAULT_KEY]; } else { // users are allowed not to provide a default for string values // if so we set to the empty string - groupResponseObject[settingName] = QString(""); + result = QString(""); } - + } else { - groupResponseObject[settingName] = QJsonValue::fromVariant(variantValue); + result = QJsonValue::fromVariant(variantValue); + } + + if (!groupKey.isEmpty()) { + // this belongs in the group object + groupResponseObject[settingName] = result; + } else { + // this is a value that should be at the root + responseObject[settingName] = result; } } } } - - if (!groupResponseObject.isEmpty()) { + + if (!groupKey.isEmpty() && !groupResponseObject.isEmpty()) { // set this group's object to the constructed object responseObject[groupKey] = groupResponseObject; } } } - - - return responseObject; -} -bool DomainServerSettingsManager::settingExists(const QString& groupName, const QString& settingName, - const QJsonArray& descriptionArray, QJsonObject& settingDescription) { - foreach(const QJsonValue& groupValue, descriptionArray) { - QJsonObject groupObject = groupValue.toObject(); - if (groupObject[DESCRIPTION_NAME_KEY].toString() == groupName) { - - foreach(const QJsonValue& settingValue, groupObject[DESCRIPTION_SETTINGS_KEY].toArray()) { - QJsonObject settingObject = settingValue.toObject(); - if (settingObject[DESCRIPTION_NAME_KEY].toString() == settingName) { - settingDescription = settingObject; - return true; - } - } - } - } - settingDescription = QJsonObject(); - return false; + + return responseObject; } void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap, @@ -266,13 +263,21 @@ void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonV QString settingType = settingDescription[SETTING_DESCRIPTION_TYPE_KEY].toString(); const QString INPUT_DOUBLE_TYPE = "double"; const QString INPUT_INTEGER_TYPE = "int"; - + if (settingType == INPUT_DOUBLE_TYPE) { settingMap[key] = newValue.toString().toDouble(); } else if (settingType == INPUT_INTEGER_TYPE) { settingMap[key] = newValue.toString().toInt(); } else { - settingMap[key] = newValue.toString(); + QString sanitizedValue = newValue.toString(); + + // we perform special handling for viewpoints here + // we do not want them to be prepended with a slash + if (key == SETTINGS_VIEWPOINT_KEY && !sanitizedValue.startsWith('/')) { + sanitizedValue.prepend('/'); + } + + settingMap[key] = sanitizedValue; } } } else if (newValue.isBool()) { @@ -282,12 +287,12 @@ void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonV // we don't have a map below this key yet, so set it up now settingMap[key] = QVariantMap(); } - + QVariantMap& thisMap = *reinterpret_cast(settingMap[key].data()); foreach(const QString childKey, newValue.toObject().keys()) { QJsonObject childDescriptionObject = settingDescription; - + // is this the key? if so we have the description already if (key != settingDescription[DESCRIPTION_NAME_KEY].toString()) { // otherwise find the description object for this childKey under columns @@ -299,12 +304,20 @@ void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonV break; } } - } + } } - - updateSetting(childKey, newValue.toObject()[childKey], thisMap, childDescriptionObject); + + QString sanitizedKey = childKey; + + if (key == SETTINGS_PATHS_KEY && !sanitizedKey.startsWith('/')) { + // We perform special handling for paths here. + // If we got sent a path without a leading slash then we add it. + sanitizedKey.prepend("/"); + } + + updateSetting(sanitizedKey, newValue.toObject()[childKey], thisMap, childDescriptionObject); } - + if (settingMap[key].toMap().isEmpty()) { // we've cleared all of the settings below this value, so remove this one too settingMap.remove(key); @@ -316,47 +329,104 @@ void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonV } } -void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, - QVariantMap& settingsVariant, - const QJsonArray& descriptionArray) { - // Iterate on the setting groups - foreach(const QString& groupKey, postedObject.keys()) { - QJsonValue groupValue = postedObject[groupKey]; - - if (!settingsVariant.contains(groupKey)) { - // we don't have a map below this key yet, so set it up now - settingsVariant[groupKey] = QVariantMap(); +QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName) { + foreach(const QJsonValue& settingValue, groupObject[DESCRIPTION_SETTINGS_KEY].toArray()) { + QJsonObject settingObject = settingValue.toObject(); + if (settingObject[DESCRIPTION_NAME_KEY].toString() == settingName) { + return settingObject; } - - // Iterate on the settings - foreach(const QString& settingKey, groupValue.toObject().keys()) { - QJsonValue settingValue = groupValue.toObject()[settingKey]; - - QJsonObject thisDescription; - if (settingExists(groupKey, settingKey, descriptionArray, thisDescription)) { - QVariantMap& thisMap = *reinterpret_cast(settingsVariant[groupKey].data()); - updateSetting(settingKey, settingValue, thisMap, thisDescription); + } + + return QJsonObject(); +} + +void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, + QVariantMap& settingsVariant) { + // Iterate on the setting groups + foreach(const QString& rootKey, postedObject.keys()) { + QJsonValue rootValue = postedObject[rootKey]; + + if (!settingsVariant.contains(rootKey)) { + // we don't have a map below this key yet, so set it up now + settingsVariant[rootKey] = QVariantMap(); + } + + QVariantMap& thisMap = settingsVariant; + + QJsonObject groupDescriptionObject; + + // we need to check the description array to see if this is a root setting or a group setting + foreach(const QJsonValue& groupValue, _descriptionArray) { + if (groupValue.toObject()[DESCRIPTION_NAME_KEY] == rootKey) { + // we matched a group - keep this since we'll use it below to update the settings + groupDescriptionObject = groupValue.toObject(); + + // change the map we will update to be the map for this group + thisMap = *reinterpret_cast(settingsVariant[rootKey].data()); + + break; } } - - if (settingsVariant[groupKey].toMap().empty()) { + + if (groupDescriptionObject.isEmpty()) { + // this is a root value, so we can call updateSetting for it directly + // first we need to find our description value for it + + QJsonObject matchingDescriptionObject; + + foreach(const QJsonValue& groupValue, _descriptionArray) { + // find groups with root values (they don't have a group name) + QJsonObject groupObject = groupValue.toObject(); + if (!groupObject.contains(DESCRIPTION_NAME_KEY)) { + // this is a group with root values - check if our setting is in here + matchingDescriptionObject = settingDescriptionFromGroup(groupObject, rootKey); + + if (!matchingDescriptionObject.isEmpty()) { + break; + } + } + } + + if (!matchingDescriptionObject.isEmpty()) { + updateSetting(rootKey, rootValue, thisMap, matchingDescriptionObject); + } else { + qDebug() << "Setting for root key" << rootKey << "does not exist - cannot update setting."; + } + } else { + // this is a group - iterate on the settings in the group + foreach(const QString& settingKey, rootValue.toObject().keys()) { + // make sure this particular setting exists and we have a description object for it + QJsonObject matchingDescriptionObject = settingDescriptionFromGroup(groupDescriptionObject, settingKey); + + // if we matched the setting then update the value + if (!matchingDescriptionObject.isEmpty()) { + QJsonValue settingValue = rootValue.toObject()[settingKey]; + updateSetting(settingKey, settingValue, thisMap, matchingDescriptionObject); + } else { + qDebug() << "Could not find description for setting" << settingKey << "in group" << rootKey << + "- cannot update setting."; + } + } + } + + if (settingsVariant[rootKey].toMap().empty()) { // we've cleared all of the settings below this value, so remove this one too - settingsVariant.remove(groupKey); + settingsVariant.remove(rootKey); } } } void DomainServerSettingsManager::persistToFile() { - + // make sure we have the dir the settings file is supposed to live in QFileInfo settingsFileInfo(_configMap.getUserConfigFilename()); - + if (!settingsFileInfo.dir().exists()) { settingsFileInfo.dir().mkpath("."); } - + QFile settingsFile(_configMap.getUserConfigFilename()); - + if (settingsFile.open(QIODevice::WriteOnly)) { settingsFile.write(QJsonDocument::fromVariant(_configMap.getUserConfig()).toJson()); } else { diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 1034838d28..450baf66ae 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -18,27 +18,28 @@ #include #include +const QString SETTINGS_PATHS_KEY = "paths"; + class DomainServerSettingsManager : public QObject { Q_OBJECT public: DomainServerSettingsManager(); bool handlePublicHTTPRequest(HTTPConnection* connection, const QUrl& url); bool handleAuthenticatedHTTPRequest(HTTPConnection* connection, const QUrl& url); - + void setupConfigMap(const QStringList& argumentList); QVariant valueOrDefaultValueForKeyPath(const QString& keyPath); - + QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); } private: QJsonObject responseObjectForType(const QString& typeValue, bool isAuthenticated = false); - void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, QVariantMap& settingsVariant, - const QJsonArray& descriptionArray); - bool settingExists(const QString& groupName, const QString& settingName, - const QJsonArray& descriptionArray, QJsonObject& settingDescription); + void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, QVariantMap& settingsVariant); + void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap, const QJsonObject& settingDescription); + QJsonObject settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName); void persistToFile(); - + QJsonArray _descriptionArray; HifiConfigVariantMap _configMap; }; diff --git a/examples/grab.js b/examples/grab.js index abffef6cb2..6bdf218ce2 100644 --- a/examples/grab.js +++ b/examples/grab.js @@ -260,9 +260,11 @@ function update(deltaTime) { } Entities.editEntity(grabbedEntity, { + position: currentPosition, + rotation: currentRotation, velocity: newVelocity, angularVelocity: angularVelocity - }) + }); updateDropLine(targetPosition); } } diff --git a/examples/html/entityProperties.html b/examples/html/entityProperties.html index 1b004bbd3a..4ce787d78a 100644 --- a/examples/html/entityProperties.html +++ b/examples/html/entityProperties.html @@ -633,7 +633,7 @@ elColorBlue.addEventListener('change', colorChangeFunction); $('#property-color').colpick({ colorScheme:'dark', - layout:'rgbhex', + layout:'hex', color:'000000', onSubmit: function(hsb, hex, rgb, el) { $(el).css('background-color', '#'+hex); @@ -651,7 +651,7 @@ elLightColorBlue.addEventListener('change', lightColorChangeFunction); $('#property-light-color').colpick({ colorScheme:'dark', - layout:'rgbhex', + layout:'hex', color:'000000', onSubmit: function(hsb, hex, rgb, el) { $(el).css('background-color', '#'+hex); @@ -686,7 +686,7 @@ elTextTextColorBlue.addEventListener('change', textTextColorChangeFunction); $('#property-text-text-color').colpick({ colorScheme:'dark', - layout:'rgbhex', + layout:'hex', color:'000000', onSubmit: function(hsb, hex, rgb, el) { $(el).css('background-color', '#'+hex); @@ -702,7 +702,7 @@ elTextBackgroundColorBlue.addEventListener('change', textBackgroundColorChangeFunction); $('#property-text-background-color').colpick({ colorScheme:'dark', - layout:'rgbhex', + layout:'hex', color:'000000', onSubmit: function(hsb, hex, rgb, el) { $(el).css('background-color', '#'+hex); @@ -714,7 +714,7 @@ elZoneStageSunModelEnabled.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('stage','sunModelEnabled')); $('#property-zone-key-light-color').colpick({ colorScheme:'dark', - layout:'rgbhex', + layout:'hex', color:'000000', onSubmit: function(hsb, hex, rgb, el) { $(el).css('background-color', '#'+hex); @@ -751,7 +751,7 @@ elZoneSkyboxColorBlue.addEventListener('change', zoneSkyboxColorChangeFunction); $('#property-zone-skybox-color').colpick({ colorScheme:'dark', - layout:'rgbhex', + layout:'hex', color:'000000', onSubmit: function(hsb, hex, rgb, el) { $(el).css('background-color', '#'+hex); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 199f91f087..54b4a8bb54 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1366,12 +1366,12 @@ void Application::keyPressEvent(QKeyEvent* event) { } -//#define VR_MENU_ONLY_IN_HMD +#define VR_MENU_ONLY_IN_HMD void Application::keyReleaseEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Alt && _altPressed && _window->isActiveWindow()) { #ifdef VR_MENU_ONLY_IN_HMD - if (OculusManager::isConnected()) { + if (isHMDMode()) { #endif VrMenu::toggle(); #ifdef VR_MENU_ONLY_IN_HMD @@ -1949,6 +1949,7 @@ void Application::setActiveFaceTracker() { #endif #ifdef HAVE_DDE bool isUsingDDE = Menu::getInstance()->isOptionChecked(MenuOption::UseCamera); + Menu::getInstance()->getActionForOption(MenuOption::BinaryEyelidControl)->setVisible(isUsingDDE); Menu::getInstance()->getActionForOption(MenuOption::UseAudioForMouth)->setVisible(isUsingDDE); Menu::getInstance()->getActionForOption(MenuOption::VelocityFilter)->setVisible(isUsingDDE); Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setVisible(isUsingDDE); diff --git a/interface/src/DatagramProcessor.cpp b/interface/src/DatagramProcessor.cpp index f477be0718..51adb7ba1a 100644 --- a/interface/src/DatagramProcessor.cpp +++ b/interface/src/DatagramProcessor.cpp @@ -25,7 +25,7 @@ DatagramProcessor::DatagramProcessor(QObject* parent) : QObject(parent) { - + } void DatagramProcessor::processDatagrams() { @@ -35,23 +35,23 @@ void DatagramProcessor::processDatagrams() { if (_isShuttingDown) { return; // bail early... we're shutting down. } - + HifiSockAddr senderSockAddr; - + static QByteArray incomingPacket; - + Application* application = Application::getInstance(); auto nodeList = DependencyManager::get(); - + while (DependencyManager::get()->getNodeSocket().hasPendingDatagrams()) { incomingPacket.resize(nodeList->getNodeSocket().pendingDatagramSize()); nodeList->readDatagram(incomingPacket, senderSockAddr.getAddressPointer(), senderSockAddr.getPortPointer()); - + _inPacketCount++; _inByteCount += incomingPacket.size(); - + if (nodeList->packetVersionAndHashMatch(incomingPacket)) { - + PacketType incomingType = packetTypeForPacket(incomingPacket); // only process this packet if we have a match on the packet version switch (incomingType) { @@ -72,14 +72,14 @@ void DatagramProcessor::processDatagrams() { Qt::QueuedConnection, Q_ARG(QByteArray, incomingPacket)); } - + // update having heard from the audio-mixer and record the bytes received SharedNodePointer audioMixer = nodeList->sendingNodeForPacket(incomingPacket); - + if (audioMixer) { audioMixer->setLastHeardMicrostamp(usecTimestampNow()); } - + break; } case PacketTypeEntityAddResponse: @@ -94,7 +94,7 @@ void DatagramProcessor::processDatagrams() { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::networkReceive()... _octreeProcessor.queueReceivedPacket()"); SharedNodePointer matchedNode = DependencyManager::get()->sendingNodeForPacket(incomingPacket); - + if (matchedNode) { // add this packet to our list of octree packets and process them on the octree data processing application->_octreeProcessor.queueReceivedPacket(matchedNode, incomingPacket); @@ -107,10 +107,10 @@ void DatagramProcessor::processDatagrams() { case PacketTypeAvatarBillboard: { // update having heard from the avatar-mixer and record the bytes received SharedNodePointer avatarMixer = nodeList->sendingNodeForPacket(incomingPacket); - + if (avatarMixer) { avatarMixer->setLastHeardMicrostamp(usecTimestampNow()); - + QMetaObject::invokeMethod(DependencyManager::get().data(), "processAvatarMixerDatagram", Q_ARG(const QByteArray&, incomingPacket), Q_ARG(const QWeakPointer&, avatarMixer)); @@ -135,20 +135,20 @@ void DatagramProcessor::processDatagrams() { case PacketTypeNoisyMute: case PacketTypeMuteEnvironment: { bool mute = !DependencyManager::get()->isMuted(); - + if (incomingType == PacketTypeMuteEnvironment) { glm::vec3 position; float radius; - + int headerSize = numBytesForPacketHeaderGivenPacketType(PacketTypeMuteEnvironment); memcpy(&position, incomingPacket.constData() + headerSize, sizeof(glm::vec3)); memcpy(&radius, incomingPacket.constData() + headerSize + sizeof(glm::vec3), sizeof(float)); float distance = glm::distance(DependencyManager::get()->getMyAvatar()->getPosition(), position); - + mute = mute && (distance < radius); } - + if (mute) { DependencyManager::get()->toggleMute(); if (incomingType == PacketTypeMuteEnvironment) { diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 61213e7334..e1587684cf 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -388,6 +388,8 @@ Menu::Menu() { } #ifdef HAVE_DDE faceTrackingMenu->addSeparator(); + QAction* binaryEyelidControl = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::BinaryEyelidControl, 0, true); + binaryEyelidControl->setVisible(false); QAction* useAudioForMouth = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::UseAudioForMouth, 0, true); useAudioForMouth->setVisible(false); QAction* ddeFiltering = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::VelocityFilter, 0, true); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 96c8fedf0d..435df47487 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -149,6 +149,7 @@ namespace MenuOption { const QString AudioStatsShowInjectedStreams = "Audio Stats Show Injected Streams"; const QString AvatarReceiveStats = "Show Receive Stats"; const QString BandwidthDetails = "Bandwidth Details"; + const QString BinaryEyelidControl = "Binary Eyelid Control"; const QString BlueSpeechSphere = "Blue Sphere While Speaking"; const QString BookmarkLocation = "Bookmark Location"; const QString Bookmarks = "Bookmarks"; diff --git a/interface/src/devices/DdeFaceTracker.cpp b/interface/src/devices/DdeFaceTracker.cpp index 14728f021b..e0b888746f 100644 --- a/interface/src/devices/DdeFaceTracker.cpp +++ b/interface/src/devices/DdeFaceTracker.cpp @@ -137,7 +137,7 @@ struct Packet { }; static const float STARTING_DDE_MESSAGE_TIME = 0.033f; - +static const float DEFAULT_DDE_EYE_CLOSING_THRESHOLD = 0.8f; static const int CALIBRATION_SAMPLES = 150; #ifdef WIN32 @@ -182,6 +182,7 @@ DdeFaceTracker::DdeFaceTracker(const QHostAddress& host, quint16 serverPort, qui _lastEyeBlinks(), _filteredEyeBlinks(), _lastEyeCoefficients(), + _eyeClosingThreshold("ddeEyeClosingThreshold", DEFAULT_DDE_EYE_CLOSING_THRESHOLD), _isCalibrating(false), _calibrationCount(0), _calibrationValues(), @@ -194,8 +195,8 @@ DdeFaceTracker::DdeFaceTracker(const QHostAddress& host, quint16 serverPort, qui _coefficientAverages.resize(NUM_FACESHIFT_BLENDSHAPES); _calibrationValues.resize(NUM_FACESHIFT_BLENDSHAPES); - _eyeStates[0] = EYE_OPEN; - _eyeStates[1] = EYE_OPEN; + _eyeStates[0] = EYE_UNCONTROLLED; + _eyeStates[1] = EYE_UNCONTROLLED; connect(&_udpSocket, SIGNAL(readyRead()), SLOT(readPendingDatagrams())); connect(&_udpSocket, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(socketErrorOccurred(QAbstractSocket::SocketError))); @@ -450,57 +451,72 @@ void DdeFaceTracker::decodePacket(const QByteArray& buffer) { // Finesse EyeBlink values float eyeCoefficients[2]; - for (int i = 0; i < 2; i++) { - // Scale EyeBlink values so that they can be used to control both EyeBlink and EyeOpen - // -ve values control EyeOpen; +ve values control EyeBlink - static const float EYE_CONTROL_THRESHOLD = 0.5f; // Resting eye value - eyeCoefficients[i] = (_filteredEyeBlinks[i] - EYE_CONTROL_THRESHOLD) / (1.0f - EYE_CONTROL_THRESHOLD); - - // Change to closing or opening states - const float EYE_CONTROL_HYSTERISIS = 0.25f; - const float EYE_CLOSING_THRESHOLD = 0.8f; - const float EYE_OPENING_THRESHOLD = EYE_CONTROL_THRESHOLD - EYE_CONTROL_HYSTERISIS; - if ((_eyeStates[i] == EYE_OPEN || _eyeStates[i] == EYE_OPENING) && eyeCoefficients[i] > EYE_CLOSING_THRESHOLD) { - _eyeStates[i] = EYE_CLOSING; - } else if ((_eyeStates[i] == EYE_CLOSED || _eyeStates[i] == EYE_CLOSING) - && eyeCoefficients[i] < EYE_OPENING_THRESHOLD) { - _eyeStates[i] = EYE_OPENING; + if (Menu::getInstance()->isOptionChecked(MenuOption::BinaryEyelidControl)) { + if (_eyeStates[0] == EYE_UNCONTROLLED) { + _eyeStates[0] = EYE_OPEN; + _eyeStates[1] = EYE_OPEN; } - const float EYELID_MOVEMENT_RATE = 10.0f; // units/second - const float EYE_OPEN_SCALE = 0.2f; - if (_eyeStates[i] == EYE_CLOSING) { - // Close eyelid until it's fully closed - float closingValue = _lastEyeCoefficients[i] + EYELID_MOVEMENT_RATE * _averageMessageTime; - if (closingValue >= 1.0) { - _eyeStates[i] = EYE_CLOSED; - eyeCoefficients[i] = 1.0; - } else { - eyeCoefficients[i] = closingValue; + for (int i = 0; i < 2; i++) { + // Scale EyeBlink values so that they can be used to control both EyeBlink and EyeOpen + // -ve values control EyeOpen; +ve values control EyeBlink + static const float EYE_CONTROL_THRESHOLD = 0.5f; // Resting eye value + eyeCoefficients[i] = (_filteredEyeBlinks[i] - EYE_CONTROL_THRESHOLD) / (1.0f - EYE_CONTROL_THRESHOLD); + + // Change to closing or opening states + const float EYE_CONTROL_HYSTERISIS = 0.25f; + float eyeClosingThreshold = getEyeClosingThreshold(); + float eyeOpeningThreshold = eyeClosingThreshold - EYE_CONTROL_HYSTERISIS; + if ((_eyeStates[i] == EYE_OPEN || _eyeStates[i] == EYE_OPENING) && eyeCoefficients[i] > eyeClosingThreshold) { + _eyeStates[i] = EYE_CLOSING; + } else if ((_eyeStates[i] == EYE_CLOSED || _eyeStates[i] == EYE_CLOSING) + && eyeCoefficients[i] < eyeOpeningThreshold) { + _eyeStates[i] = EYE_OPENING; } - } else if (_eyeStates[i] == EYE_OPENING) { - // Open eyelid until it meets the current adjusted value - float openingValue = _lastEyeCoefficients[i] - EYELID_MOVEMENT_RATE * _averageMessageTime; - if (openingValue < eyeCoefficients[i] * EYE_OPEN_SCALE) { - _eyeStates[i] = EYE_OPEN; + + const float EYELID_MOVEMENT_RATE = 10.0f; // units/second + const float EYE_OPEN_SCALE = 0.2f; + if (_eyeStates[i] == EYE_CLOSING) { + // Close eyelid until it's fully closed + float closingValue = _lastEyeCoefficients[i] + EYELID_MOVEMENT_RATE * _averageMessageTime; + if (closingValue >= 1.0) { + _eyeStates[i] = EYE_CLOSED; + eyeCoefficients[i] = 1.0; + } else { + eyeCoefficients[i] = closingValue; + } + } else if (_eyeStates[i] == EYE_OPENING) { + // Open eyelid until it meets the current adjusted value + float openingValue = _lastEyeCoefficients[i] - EYELID_MOVEMENT_RATE * _averageMessageTime; + if (openingValue < eyeCoefficients[i] * EYE_OPEN_SCALE) { + _eyeStates[i] = EYE_OPEN; + eyeCoefficients[i] = eyeCoefficients[i] * EYE_OPEN_SCALE; + } else { + eyeCoefficients[i] = openingValue; + } + } else if (_eyeStates[i] == EYE_OPEN) { + // Reduce eyelid movement eyeCoefficients[i] = eyeCoefficients[i] * EYE_OPEN_SCALE; - } else { - eyeCoefficients[i] = openingValue; + } else if (_eyeStates[i] == EYE_CLOSED) { + // Keep eyelid fully closed + eyeCoefficients[i] = 1.0; } - } else if (_eyeStates[i] == EYE_OPEN) { - // Reduce eyelid movement - eyeCoefficients[i] = eyeCoefficients[i] * EYE_OPEN_SCALE; - } else if (_eyeStates[i] == EYE_CLOSED) { - // Keep eyelid fully closed - eyeCoefficients[i] = 1.0; } + + if (_eyeStates[0] == EYE_OPEN && _eyeStates[1] == EYE_OPEN) { + // Couple eyelids + eyeCoefficients[0] = eyeCoefficients[1] = (eyeCoefficients[0] + eyeCoefficients[0]) / 2.0f; + } + + _lastEyeCoefficients[0] = eyeCoefficients[0]; + _lastEyeCoefficients[1] = eyeCoefficients[1]; + } else { + _eyeStates[0] = EYE_UNCONTROLLED; + _eyeStates[1] = EYE_UNCONTROLLED; + + eyeCoefficients[0] = _filteredEyeBlinks[0]; + eyeCoefficients[1] = _filteredEyeBlinks[1]; } - if (_eyeStates[0] == EYE_OPEN && _eyeStates[1] == EYE_OPEN) { - // Couple eyelids - eyeCoefficients[0] = eyeCoefficients[1] = (eyeCoefficients[0] + eyeCoefficients[0]) / 2.0f; - } - _lastEyeCoefficients[0] = eyeCoefficients[0]; - _lastEyeCoefficients[1] = eyeCoefficients[1]; // Use EyeBlink values to control both EyeBlink and EyeOpen if (eyeCoefficients[0] > 0) { @@ -544,6 +560,10 @@ void DdeFaceTracker::decodePacket(const QByteArray& buffer) { } } +void DdeFaceTracker::setEyeClosingThreshold(float eyeClosingThreshold) { + _eyeClosingThreshold.set(eyeClosingThreshold); +} + static const int CALIBRATION_BILLBOARD_WIDTH = 240; static const int CALIBRATION_BILLBOARD_HEIGHT = 180; static const int CALIBRATION_BILLBOARD_TOP_MARGIN = 60; diff --git a/interface/src/devices/DdeFaceTracker.h b/interface/src/devices/DdeFaceTracker.h index 7019802603..8c30c5a4c3 100644 --- a/interface/src/devices/DdeFaceTracker.h +++ b/interface/src/devices/DdeFaceTracker.h @@ -50,6 +50,9 @@ public: float getMouthSmileLeft() const { return getBlendshapeCoefficient(_mouthSmileLeftIndex); } float getMouthSmileRight() const { return getBlendshapeCoefficient(_mouthSmileRightIndex); } + float getEyeClosingThreshold() { return _eyeClosingThreshold.get(); } + void setEyeClosingThreshold(float eyeClosingThreshold); + public slots: void setEnabled(bool enabled); void calibrate(); @@ -89,8 +92,7 @@ private: int _rightBlinkIndex; int _leftEyeOpenIndex; int _rightEyeOpenIndex; - - // Brows + int _browDownLeftIndex; int _browDownRightIndex; int _browUpCenterIndex; @@ -114,6 +116,7 @@ private: float _filteredBrowUp; enum EyeState { + EYE_UNCONTROLLED, EYE_OPEN, EYE_CLOSING, EYE_CLOSED, @@ -123,6 +126,8 @@ private: float _lastEyeBlinks[2]; float _filteredEyeBlinks[2]; float _lastEyeCoefficients[2]; + Setting::Handle _eyeClosingThreshold; + QVector _coefficientAverages; bool _isCalibrating; diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 4622ffd7ed..febd6f90bf 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -135,6 +136,10 @@ void PreferencesDialog::loadPreferences() { ui.pupilDilationSlider->setValue(myAvatar->getHead()->getPupilDilation() * ui.pupilDilationSlider->maximum()); + auto dde = DependencyManager::get(); + ui.ddeEyeClosingThresholdSlider->setValue(dde->getEyeClosingThreshold() * + ui.ddeEyeClosingThresholdSlider->maximum()); + auto faceshift = DependencyManager::get(); ui.faceshiftEyeDeflectionSider->setValue(faceshift->getEyeDeflection() * ui.faceshiftEyeDeflectionSider->maximum()); @@ -222,6 +227,10 @@ void PreferencesDialog::savePreferences() { qApp->setFieldOfView(ui.fieldOfViewSpin->value()); + auto dde = DependencyManager::get(); + dde->setEyeClosingThreshold(ui.ddeEyeClosingThresholdSlider->value() / + (float)ui.ddeEyeClosingThresholdSlider->maximum()); + auto faceshift = DependencyManager::get(); faceshift->setEyeDeflection(ui.faceshiftEyeDeflectionSider->value() / (float)ui.faceshiftEyeDeflectionSider->maximum()); diff --git a/interface/ui/preferencesDialog.ui b/interface/ui/preferencesDialog.ui index d26bdaee8a..e74b89075e 100644 --- a/interface/ui/preferencesDialog.ui +++ b/interface/ui/preferencesDialog.ui @@ -1256,7 +1256,7 @@ - Pupil dillation + Pupil dilation 0 @@ -1310,6 +1310,82 @@ + + + + 0 + + + 7 + + + 0 + + + 7 + + + + + + Arial + + + + Camera binary eyelid threshold + + + 0 + + + ddeEyeClosingThresholdSlider + + + + + + + + Arial + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 130 + 0 + + + + + Arial + + + + Qt::Horizontal + + + + + diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 9b99c236b3..48d9655e43 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -43,11 +43,11 @@ bool AddressManager::isConnected() { const QUrl AddressManager::currentAddress() const { QUrl hifiURL; - + hifiURL.setScheme(HIFI_URL_SCHEME); hifiURL.setHost(_rootPlaceName); hifiURL.setPath(currentPath()); - + return hifiURL; } @@ -64,10 +64,10 @@ void AddressManager::storeCurrentAddress() { } const QString AddressManager::currentPath(bool withOrientation) const { - + if (_positionGetter) { QString pathString = "/" + createByteArray(_positionGetter()); - + if (withOrientation) { if (_orientationGetter) { QString orientationString = createByteArray(_orientationGetter()); @@ -76,9 +76,9 @@ const QString AddressManager::currentPath(bool withOrientation) const { qCDebug(networking) << "Cannot add orientation to path without a getter for position." << "Call AddressManager::setOrientationGetter to pass a function that will return a glm::quat"; } - + } - + return pathString; } else { qCDebug(networking) << "Cannot create address path without a getter for position." @@ -90,29 +90,29 @@ const QString AddressManager::currentPath(bool withOrientation) const { const JSONCallbackParameters& AddressManager::apiCallbackParameters() { static bool hasSetupParameters = false; static JSONCallbackParameters callbackParams; - + if (!hasSetupParameters) { callbackParams.jsonCallbackReceiver = this; callbackParams.jsonCallbackMethod = "handleAPIResponse"; callbackParams.errorCallbackReceiver = this; callbackParams.errorCallbackMethod = "handleAPIError"; } - + return callbackParams; } bool AddressManager::handleUrl(const QUrl& lookupUrl) { if (lookupUrl.scheme() == HIFI_URL_SCHEME) { - + qCDebug(networking) << "Trying to go to URL" << lookupUrl.toString(); - + // there are 4 possible lookup strings - + // 1. global place name (name of domain or place) - example: sanfrancisco // 2. user name (prepended with @) - example: @philip // 3. location string (posX,posY,posZ/eulerX,eulerY,eulerZ) // 4. domain network address (IP or dns resolvable hostname) - + // use our regex'ed helpers to figure out what we're supposed to do with this if (!handleUsername(lookupUrl.authority())) { // we're assuming this is either a network address or global place name @@ -120,24 +120,23 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl) { if (handleNetworkAddress(lookupUrl.host() + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())))) { // we may have a path that defines a relative viewpoint - if so we should jump to that now - handleRelativeViewpoint(lookupUrl.path()); + handlePath(lookupUrl.path()); } else { // wasn't an address - lookup the place name // we may have a path that defines a relative viewpoint - pass that through the lookup so we can go to it after attemptPlaceNameLookup(lookupUrl.host(), lookupUrl.path()); - } } - + return true; } else if (lookupUrl.toString().startsWith('/')) { qCDebug(networking) << "Going to relative path" << lookupUrl.path(); - + // if this is a relative path then handle it as a relative viewpoint - handleRelativeViewpoint(lookupUrl.path()); + handlePath(lookupUrl.path()); emit lookupResultsFinished(); } - + return false; } @@ -146,16 +145,16 @@ void AddressManager::handleLookupString(const QString& lookupString) { // make this a valid hifi URL and handle it off to handleUrl QString sanitizedString = lookupString.trimmed(); QUrl lookupURL; - + if (!lookupString.startsWith('/')) { const QRegExp HIFI_SCHEME_REGEX = QRegExp(HIFI_URL_SCHEME + ":\\/{1,2}", Qt::CaseInsensitive); sanitizedString = sanitizedString.remove(HIFI_SCHEME_REGEX); - + lookupURL = QUrl(HIFI_URL_SCHEME + "://" + sanitizedString); } else { lookupURL = QUrl(lookupString); } - + handleUrl(lookupURL); } } @@ -163,101 +162,103 @@ void AddressManager::handleLookupString(const QString& lookupString) { void AddressManager::handleAPIResponse(QNetworkReply& requestReply) { QJsonObject responseObject = QJsonDocument::fromJson(requestReply.readAll()).object(); QJsonObject dataObject = responseObject["data"].toObject(); - + goToAddressFromObject(dataObject.toVariantMap(), requestReply); - + emit lookupResultsFinished(); } const char OVERRIDE_PATH_KEY[] = "override_path"; void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const QNetworkReply& reply) { - + const QString DATA_OBJECT_PLACE_KEY = "place"; const QString DATA_OBJECT_USER_LOCATION_KEY = "location"; - + QVariantMap locationMap; if (dataObject.contains(DATA_OBJECT_PLACE_KEY)) { locationMap = dataObject[DATA_OBJECT_PLACE_KEY].toMap(); } else { locationMap = dataObject[DATA_OBJECT_USER_LOCATION_KEY].toMap(); } - + if (!locationMap.isEmpty()) { const QString LOCATION_API_ROOT_KEY = "root"; const QString LOCATION_API_DOMAIN_KEY = "domain"; const QString LOCATION_API_ONLINE_KEY = "online"; - + if (!locationMap.contains(LOCATION_API_ONLINE_KEY) || locationMap[LOCATION_API_ONLINE_KEY].toBool()) { - + QVariantMap rootMap = locationMap[LOCATION_API_ROOT_KEY].toMap(); if (rootMap.isEmpty()) { rootMap = locationMap; } - + QVariantMap domainObject = rootMap[LOCATION_API_DOMAIN_KEY].toMap(); - + if (!domainObject.isEmpty()) { const QString DOMAIN_NETWORK_ADDRESS_KEY = "network_address"; const QString DOMAIN_NETWORK_PORT_KEY = "network_port"; const QString DOMAIN_ICE_SERVER_ADDRESS_KEY = "ice_server_address"; - + if (domainObject.contains(DOMAIN_NETWORK_ADDRESS_KEY)) { QString domainHostname = domainObject[DOMAIN_NETWORK_ADDRESS_KEY].toString(); - + quint16 domainPort = domainObject.contains(DOMAIN_NETWORK_PORT_KEY) ? domainObject[DOMAIN_NETWORK_PORT_KEY].toUInt() : DEFAULT_DOMAIN_SERVER_PORT; - + qCDebug(networking) << "Possible domain change required to connect to" << domainHostname << "on" << domainPort; emit possibleDomainChangeRequired(domainHostname, domainPort); } else { QString iceServerAddress = domainObject[DOMAIN_ICE_SERVER_ADDRESS_KEY].toString(); - + const QString DOMAIN_ID_KEY = "id"; QString domainIDString = domainObject[DOMAIN_ID_KEY].toString(); QUuid domainID(domainIDString); - + qCDebug(networking) << "Possible domain change required to connect to domain with ID" << domainID << "via ice-server at" << iceServerAddress; - + emit possibleDomainChangeRequiredViaICEForID(iceServerAddress, domainID); } - + // set our current root place id to the ID that came back const QString PLACE_ID_KEY = "id"; _rootPlaceID = rootMap[PLACE_ID_KEY].toUuid(); - + // set our current root place name to the name that came back const QString PLACE_NAME_KEY = "name"; QString newRootPlaceName = rootMap[PLACE_NAME_KEY].toString(); setRootPlaceName(newRootPlaceName); - + // check if we had a path to override the path returned QString overridePath = reply.property(OVERRIDE_PATH_KEY).toString(); - + if (!overridePath.isEmpty()) { - if (!handleRelativeViewpoint(overridePath)){ - qCDebug(networking) << "User entered path could not be handled as a viewpoint - " << overridePath; - } + handlePath(overridePath); } else { // take the path that came back const QString PLACE_PATH_KEY = "path"; QString returnedPath = locationMap[PLACE_PATH_KEY].toString(); - + bool shouldFaceViewpoint = locationMap.contains(LOCATION_API_ONLINE_KEY); - + if (!returnedPath.isEmpty()) { // try to parse this returned path as a viewpoint, that's the only thing it could be for now - if (!handleRelativeViewpoint(returnedPath, shouldFaceViewpoint)) { - qCDebug(networking) << "Received a location path that was could not be handled as a viewpoint -" << returnedPath; + if (!handleViewpoint(returnedPath, shouldFaceViewpoint)) { + qCDebug(networking) << "Received a location path that was could not be handled as a viewpoint -" + << returnedPath; } + } else { + // we didn't override the path or get one back - ask the DS for the viewpoint of its index path + // which we will jump to if it exists + emit pathChangeRequired(INDEX_PATH); } } - - + } else { qCDebug(networking) << "Received an address manager API response with no domain key. Cannot parse."; qCDebug(networking) << locationMap; @@ -274,7 +275,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const void AddressManager::handleAPIError(QNetworkReply& errorReply) { qCDebug(networking) << "AddressManager API error -" << errorReply.error() << "-" << errorReply.errorString(); - + if (errorReply.error() == QNetworkReply::ContentNotFoundError) { emit lookupResultIsNotFound(); } @@ -286,12 +287,12 @@ const QString GET_PLACE = "/api/v1/places/%1"; void AddressManager::attemptPlaceNameLookup(const QString& lookupString, const QString& overridePath) { // assume this is a place name and see if we can get any info on it QString placeName = QUrl::toPercentEncoding(lookupString); - + QVariantMap requestParams; if (!overridePath.isEmpty()) { requestParams.insert(OVERRIDE_PATH_KEY, overridePath); } - + AccountManager::getInstance().sendRequest(GET_PLACE.arg(placeName), AccountManagerAuth::None, QNetworkAccessManager::GetOperation, @@ -302,47 +303,55 @@ void AddressManager::attemptPlaceNameLookup(const QString& lookupString, const Q bool AddressManager::handleNetworkAddress(const QString& lookupString) { const QString IP_ADDRESS_REGEX_STRING = "^((?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}" "(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))(?::(\\d{1,5}))?$"; - + const QString HOSTNAME_REGEX_STRING = "^((?:[A-Z0-9]|[A-Z0-9][A-Z0-9\\-]{0,61}[A-Z0-9])" "(?:\\.(?:[A-Z0-9]|[A-Z0-9][A-Z0-9\\-]{0,61}[A-Z0-9]))+|localhost)(?::(\\d{1,5}))?$"; - + QRegExp ipAddressRegex(IP_ADDRESS_REGEX_STRING); - + if (ipAddressRegex.indexIn(lookupString) != -1) { QString domainIPString = ipAddressRegex.cap(1); - + qint16 domainPort = DEFAULT_DOMAIN_SERVER_PORT; if (!ipAddressRegex.cap(2).isEmpty()) { domainPort = (qint16) ipAddressRegex.cap(2).toInt(); } - + emit lookupResultsFinished(); setDomainInfo(domainIPString, domainPort); - + return true; } - + QRegExp hostnameRegex(HOSTNAME_REGEX_STRING, Qt::CaseInsensitive); - + if (hostnameRegex.indexIn(lookupString) != -1) { QString domainHostname = hostnameRegex.cap(1); - + quint16 domainPort = DEFAULT_DOMAIN_SERVER_PORT; - + if (!hostnameRegex.cap(2).isEmpty()) { domainPort = (qint16) hostnameRegex.cap(2).toInt(); } - + emit lookupResultsFinished(); setDomainInfo(domainHostname, domainPort); - + return true; } - + return false; } -bool AddressManager::handleRelativeViewpoint(const QString& lookupString, bool shouldFace) { +void AddressManager::handlePath(const QString& path) { + if (!handleViewpoint(path)) { + qCDebug(networking) << "User entered path could not be handled as a viewpoint - " << path << + "- wll attempt to ask domain-server to resolve."; + emit pathChangeRequired(path); + } +} + +bool AddressManager::handleViewpoint(const QString& viewpointString, bool shouldFace) { const QString FLOAT_REGEX_STRING = "([-+]?[0-9]*\\.?[0-9]+(?:[eE][-+]?[0-9]+)?)"; const QString SPACED_COMMA_REGEX_STRING = "\\s*,\\s*"; const QString POSITION_REGEX_STRING = QString("\\/") + FLOAT_REGEX_STRING + SPACED_COMMA_REGEX_STRING + @@ -350,29 +359,29 @@ bool AddressManager::handleRelativeViewpoint(const QString& lookupString, bool s const QString QUAT_REGEX_STRING = QString("\\/") + FLOAT_REGEX_STRING + SPACED_COMMA_REGEX_STRING + FLOAT_REGEX_STRING + SPACED_COMMA_REGEX_STRING + FLOAT_REGEX_STRING + SPACED_COMMA_REGEX_STRING + FLOAT_REGEX_STRING + "\\s*$"; - + QRegExp positionRegex(POSITION_REGEX_STRING); - - if (positionRegex.indexIn(lookupString) != -1) { + + if (positionRegex.indexIn(viewpointString) != -1) { // we have at least a position, so emit our signal to say we need to change position glm::vec3 newPosition(positionRegex.cap(1).toFloat(), positionRegex.cap(2).toFloat(), positionRegex.cap(3).toFloat()); - + if (!isNaN(newPosition.x) && !isNaN(newPosition.y) && !isNaN(newPosition.z)) { glm::quat newOrientation; - + QRegExp orientationRegex(QUAT_REGEX_STRING); - + // we may also have an orientation - if (lookupString[positionRegex.matchedLength() - 1] == QChar('/') - && orientationRegex.indexIn(lookupString, positionRegex.matchedLength() - 1) != -1) { - + if (viewpointString[positionRegex.matchedLength() - 1] == QChar('/') + && orientationRegex.indexIn(viewpointString, positionRegex.matchedLength() - 1) != -1) { + glm::quat newOrientation = glm::normalize(glm::quat(orientationRegex.cap(4).toFloat(), orientationRegex.cap(1).toFloat(), orientationRegex.cap(2).toFloat(), orientationRegex.cap(3).toFloat())); - + if (!isNaN(newOrientation.x) && !isNaN(newOrientation.y) && !isNaN(newOrientation.z) && !isNaN(newOrientation.w)) { emit locationChangeRequired(newPosition, true, newOrientation, shouldFace); @@ -381,31 +390,31 @@ bool AddressManager::handleRelativeViewpoint(const QString& lookupString, bool s qCDebug(networking) << "Orientation parsed from lookup string is invalid. Will not use for location change."; } } - + emit locationChangeRequired(newPosition, false, newOrientation, shouldFace); - + } else { qCDebug(networking) << "Could not jump to position from lookup string because it has an invalid value."; } - + return true; + } else { + return false; } - - return false; } const QString GET_USER_LOCATION = "/api/v1/users/%1/location"; bool AddressManager::handleUsername(const QString& lookupString) { const QString USERNAME_REGEX_STRING = "^@(\\S+)"; - + QRegExp usernameRegex(USERNAME_REGEX_STRING); - + if (usernameRegex.indexIn(lookupString) != -1) { goToUser(usernameRegex.cap(1)); return true; } - + return false; } @@ -420,9 +429,9 @@ void AddressManager::setRootPlaceName(const QString& rootPlaceName) { void AddressManager::setDomainInfo(const QString& hostname, quint16 port) { _rootPlaceName = hostname; _rootPlaceID = QUuid(); - + qCDebug(networking) << "Possible domain change required to connect to domain at" << hostname << "on" << port; - + emit possibleDomainChangeRequired(hostname, port); } diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index db1cd6a3c8..5831d62603 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -24,6 +24,7 @@ const QString HIFI_URL_SCHEME = "hifi"; const QString DEFAULT_HIFI_ADDRESS = "hifi://entry"; +const QString INDEX_PATH = "/"; typedef const glm::vec3& (*PositionGetter)(); typedef glm::quat (*OrientationGetter)(); @@ -39,32 +40,34 @@ class AddressManager : public QObject, public Dependency { public: bool isConnected(); const QString& getProtocol() { return HIFI_URL_SCHEME; }; - + const QUrl currentAddress() const; const QString currentPath(bool withOrientation = true) const; - + const QUuid& getRootPlaceID() const { return _rootPlaceID; } - + const QString& getRootPlaceName() const { return _rootPlaceName; } void setRootPlaceName(const QString& rootPlaceName); - + void attemptPlaceNameLookup(const QString& lookupString, const QString& overridePath = QString()); - + void setPositionGetter(PositionGetter positionGetter) { _positionGetter = positionGetter; } void setOrientationGetter(OrientationGetter orientationGetter) { _orientationGetter = orientationGetter; } - + void loadSettings(const QString& lookupString = QString()); - + public slots: void handleLookupString(const QString& lookupString); + void goToUser(const QString& username); void goToAddressFromObject(const QVariantMap& addressMap, const QNetworkReply& reply); - + bool goToViewpoint(const QString& viewpointString) { return handleViewpoint(viewpointString); } + void storeCurrentAddress(); - + void copyAddress(); void copyPath(); - + signals: void lookupResultsFinished(); void lookupResultIsOffline(); @@ -74,6 +77,7 @@ signals: void locationChangeRequired(const glm::vec3& newPosition, bool hasOrientationChange, const glm::quat& newOrientation, bool shouldFaceLocation); + void pathChangeRequired(const QString& newPath); void rootPlaceNameChanged(const QString& newRootPlaceName); protected: AddressManager(); @@ -82,15 +86,16 @@ private slots: void handleAPIError(QNetworkReply& errorReply); private: void setDomainInfo(const QString& hostname, quint16 port); - + const JSONCallbackParameters& apiCallbackParameters(); - + bool handleUrl(const QUrl& lookupUrl); - + bool handleNetworkAddress(const QString& lookupString); - bool handleRelativeViewpoint(const QString& pathSubsection, bool shouldFace = false); + void handlePath(const QString& path); + bool handleViewpoint(const QString& viewpointString, bool shouldFace = false); bool handleUsername(const QString& lookupString); - + QString _rootPlaceName; QUuid _rootPlaceID; PositionGetter _positionGetter; diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 4f9585cceb..f7d460d7dd 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -36,19 +36,19 @@ DomainHandler::DomainHandler(QObject* parent) : _settingsObject(), _failedSettingsRequests(0) { - + } void DomainHandler::clearConnectionInfo() { _uuid = QUuid(); - + _icePeer = NetworkPeer(); - + if (requiresICE()) { // if we connected to this domain with ICE, re-set the socket so we reconnect through the ice-server - _sockAddr.setAddress(QHostAddress::Null); + _sockAddr.clear(); } - + setIsConnected(false); } @@ -65,12 +65,15 @@ void DomainHandler::softReset() { void DomainHandler::hardReset() { softReset(); - + qCDebug(networking) << "Hard reset in NodeList DomainHandler."; _iceDomainID = QUuid(); _iceServerSockAddr = HifiSockAddr(); _hostname = QString(); - _sockAddr.setAddress(QHostAddress::Null); + _sockAddr.clear(); + + // clear any pending path we may have wanted to ask the previous DS about + _pendingPath.clear(); } void DomainHandler::setSockAddr(const HifiSockAddr& sockAddr, const QString& hostname) { @@ -80,7 +83,7 @@ void DomainHandler::setSockAddr(const HifiSockAddr& sockAddr, const QString& hos // change the sockAddr _sockAddr = sockAddr; } - + // some callers may pass a hostname, this is not to be used for lookup but for DTLS certificate verification _hostname = hostname; } @@ -93,29 +96,29 @@ void DomainHandler::setUUID(const QUuid& uuid) { } void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) { - + if (hostname != _hostname || _sockAddr.getPort() != port) { // re-set the domain info so that auth information is reloaded hardReset(); - + if (hostname != _hostname) { // set the new hostname _hostname = hostname; - + qCDebug(networking) << "Updated domain hostname to" << _hostname; - + // re-set the sock addr to null and fire off a lookup of the IP address for this domain-server's hostname qCDebug(networking, "Looking up DS hostname %s.", _hostname.toLocal8Bit().constData()); QHostInfo::lookupHost(_hostname, this, SLOT(completedHostnameLookup(const QHostInfo&))); - + UserActivityLogger::getInstance().changedDomain(_hostname); emit hostnameChanged(_hostname); } - + if (_sockAddr.getPort() != port) { qCDebug(networking) << "Updated domain port to" << port; } - + // grab the port by reading the string after the colon _sockAddr.setPort(port); } @@ -125,16 +128,16 @@ void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, if (id != _uuid) { // re-set the domain info to connect to new domain hardReset(); - + _iceDomainID = id; - + HifiSockAddr* replaceableSockAddr = &_iceServerSockAddr; replaceableSockAddr->~HifiSockAddr(); replaceableSockAddr = new (replaceableSockAddr) HifiSockAddr(iceServerHostname, ICE_SERVER_DEFAULT_PORT); - + // refresh our ICE client UUID to something new _iceClientID = QUuid::createUuid(); - + qCDebug(networking) << "ICE required to connect to domain via ice server at" << iceServerHostname; } } @@ -142,23 +145,29 @@ void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, void DomainHandler::activateICELocalSocket() { _sockAddr = _icePeer.getLocalSocket(); _hostname = _sockAddr.getAddress().toString(); + emit completedSocketDiscovery(); } void DomainHandler::activateICEPublicSocket() { _sockAddr = _icePeer.getPublicSocket(); _hostname = _sockAddr.getAddress().toString(); + emit completedSocketDiscovery(); } void DomainHandler::completedHostnameLookup(const QHostInfo& hostInfo) { for (int i = 0; i < hostInfo.addresses().size(); i++) { if (hostInfo.addresses()[i].protocol() == QAbstractSocket::IPv4Protocol) { _sockAddr.setAddress(hostInfo.addresses()[i]); + qCDebug(networking, "DS at %s is at %s", _hostname.toLocal8Bit().constData(), _sockAddr.getAddress().toString().toLocal8Bit().constData()); + + emit completedSocketDiscovery(); + return; - } + } } - + // if we got here then we failed to lookup the address qCDebug(networking, "Failed domain server lookup"); } @@ -166,10 +175,10 @@ void DomainHandler::completedHostnameLookup(const QHostInfo& hostInfo) { void DomainHandler::setIsConnected(bool isConnected) { if (_isConnected != isConnected) { _isConnected = isConnected; - + if (_isConnected) { emit connectedToDomain(_hostname); - + // we've connected to new domain - time to ask it for global settings requestDomainSettings(); } else { @@ -195,9 +204,9 @@ void DomainHandler::requestDomainSettings() { settingsJSONURL.setPath("/settings.json"); Assignment::Type assignmentType = Assignment::typeForNodeType(DependencyManager::get()->getOwnerType()); settingsJSONURL.setQuery(QString("type=%1").arg(assignmentType)); - + qCDebug(networking) << "Requesting domain-server settings at" << settingsJSONURL.toString(); - + QNetworkRequest settingsRequest(settingsJSONURL); settingsRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); QNetworkReply* reply = NetworkAccessManager::getInstance().get(settingsRequest); @@ -210,23 +219,23 @@ const int MAX_SETTINGS_REQUEST_FAILED_ATTEMPTS = 5; void DomainHandler::settingsRequestFinished() { QNetworkReply* settingsReply = reinterpret_cast(sender()); - + int replyCode = settingsReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - + if (settingsReply->error() == QNetworkReply::NoError && replyCode != 301 && replyCode != 302) { // parse the JSON to a QJsonObject and save it _settingsObject = QJsonDocument::fromJson(settingsReply->readAll()).object(); - + qCDebug(networking) << "Received domain settings."; emit settingsReceived(_settingsObject); - + // reset failed settings requests to 0, we got them _failedSettingsRequests = 0; } else { // error grabbing the settings - in some cases this means we are stuck // so we should retry until we get it qCDebug(networking) << "Error getting domain settings -" << settingsReply->errorString() << "- retrying"; - + if (++_failedSettingsRequests >= MAX_SETTINGS_REQUEST_FAILED_ATTEMPTS) { qCDebug(networking) << "Failed to retreive domain-server settings" << MAX_SETTINGS_REQUEST_FAILED_ATTEMPTS << "times. Re-setting connection to domain."; @@ -235,7 +244,7 @@ void DomainHandler::settingsRequestFinished() { emit settingsReceiveFail(); } else { requestDomainSettings(); - } + } } settingsReply->deleteLater(); } @@ -243,30 +252,30 @@ void DomainHandler::settingsRequestFinished() { void DomainHandler::parseDTLSRequirementPacket(const QByteArray& dtlsRequirementPacket) { // figure out the port that the DS wants us to use for us to talk to them with DTLS int numBytesPacketHeader = numBytesForPacketHeader(dtlsRequirementPacket); - + unsigned short dtlsPort = 0; memcpy(&dtlsPort, dtlsRequirementPacket.data() + numBytesPacketHeader, sizeof(dtlsPort)); - + qCDebug(networking) << "domain-server DTLS port changed to" << dtlsPort << "- Enabling DTLS."; - + _sockAddr.setPort(dtlsPort); - + // initializeDTLSSession(); } void DomainHandler::processICEResponsePacket(const QByteArray& icePacket) { QDataStream iceResponseStream(icePacket); iceResponseStream.skipRawData(numBytesForPacketHeader(icePacket)); - + NetworkPeer packetPeer; iceResponseStream >> packetPeer; - + if (packetPeer.getUUID() != _iceDomainID) { qCDebug(networking) << "Received a network peer with ID that does not match current domain. Will not attempt connection."; } else { qCDebug(networking) << "Received network peer object for domain -" << packetPeer; _icePeer = packetPeer; - + emit requestICEConnectionAttempt(); } } diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 35ef7d8e2c..80a211405b 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -31,67 +31,79 @@ class DomainHandler : public QObject { Q_OBJECT public: DomainHandler(QObject* parent = 0); - + void clearConnectionInfo(); void clearSettings(); - + const QUuid& getUUID() const { return _uuid; } void setUUID(const QUuid& uuid); - + const QString& getHostname() const { return _hostname; } - + const QHostAddress& getIP() const { return _sockAddr.getAddress(); } void setIPToLocalhost() { _sockAddr.setAddress(QHostAddress(QHostAddress::LocalHost)); } - + const HifiSockAddr& getSockAddr() { return _sockAddr; } void setSockAddr(const HifiSockAddr& sockAddr, const QString& hostname); - + unsigned short getPort() const { return _sockAddr.getPort(); } void setPort(quint16 port) { _sockAddr.setPort(port); } - + const QUuid& getAssignmentUUID() const { return _assignmentUUID; } void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; } - + const QUuid& getICEDomainID() const { return _iceDomainID; } - + const QUuid& getICEClientID() const { return _iceClientID; } - + bool requiresICE() const { return !_iceServerSockAddr.isNull(); } const HifiSockAddr& getICEServerSockAddr() const { return _iceServerSockAddr; } NetworkPeer& getICEPeer() { return _icePeer; } void activateICELocalSocket(); void activateICEPublicSocket(); - + bool isConnected() const { return _isConnected; } void setIsConnected(bool isConnected); - + bool hasSettings() const { return !_settingsObject.isEmpty(); } void requestDomainSettings(); const QJsonObject& getSettingsObject() const { return _settingsObject; } - + void parseDTLSRequirementPacket(const QByteArray& dtlsRequirementPacket); void processICEResponsePacket(const QByteArray& icePacket); - + + void setPendingPath(const QString& pendingPath) { _pendingPath = pendingPath; } + const QString& getPendingPath() { return _pendingPath; } + void clearPendingPath() { _pendingPath.clear(); } + + bool isSocketKnown() const { return !_sockAddr.getAddress().isNull(); } + void softReset(); public slots: void setHostnameAndPort(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT); void setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id); - + private slots: void completedHostnameLookup(const QHostInfo& hostInfo); void settingsRequestFinished(); signals: void hostnameChanged(const QString& hostname); + + // NOTE: the emission of completedSocketDiscovery does not mean a connection to DS is established + // It means that, either from DNS lookup or ICE, we think we have a socket we can talk to DS on + void completedSocketDiscovery(); + void connectedToDomain(const QString& hostname); void disconnectedFromDomain(); + void requestICEConnectionAttempt(); - + void settingsReceived(const QJsonObject& domainSettingsObject); void settingsReceiveFail(); - + private: void hardReset(); - + QUuid _uuid; QString _hostname; HifiSockAddr _sockAddr; @@ -103,6 +115,7 @@ private: bool _isConnected; QJsonObject _settingsObject; int _failedSettingsRequests; + QString _pendingPath; }; #endif // hifi_DomainHandler_h diff --git a/libraries/networking/src/HifiSockAddr.h b/libraries/networking/src/HifiSockAddr.h index 4d3944012e..9151e51af2 100644 --- a/libraries/networking/src/HifiSockAddr.h +++ b/libraries/networking/src/HifiSockAddr.h @@ -29,26 +29,27 @@ public: HifiSockAddr(const HifiSockAddr& otherSockAddr); HifiSockAddr(const QString& hostname, quint16 hostOrderPort, bool shouldBlockForLookup = false); HifiSockAddr(const sockaddr* sockaddr); - + bool isNull() const { return _address.isNull() && _port == 0; } + void clear() { _address = QHostAddress::Null; _port = 0;} HifiSockAddr& operator=(const HifiSockAddr& rhsSockAddr); void swap(HifiSockAddr& otherSockAddr); - + bool operator==(const HifiSockAddr& rhsSockAddr) const; bool operator!=(const HifiSockAddr& rhsSockAddr) const { return !(*this == rhsSockAddr); } - + const QHostAddress& getAddress() const { return _address; } QHostAddress* getAddressPointer() { return &_address; } void setAddress(const QHostAddress& address) { _address = address; } - + quint16 getPort() const { return _port; } quint16* getPortPointer() { return &_port; } void setPort(quint16 port) { _port = port; } - + static int packSockAddr(unsigned char* packetData, const HifiSockAddr& packSockAddr); static int unpackSockAddr(const unsigned char* packetData, HifiSockAddr& unpackDestSockAddr); - + friend QDebug operator<<(QDebug debug, const HifiSockAddr& sockAddr); friend QDataStream& operator<<(QDataStream& dataStream, const HifiSockAddr& sockAddr); friend QDataStream& operator>>(QDataStream& dataStream, HifiSockAddr& sockAddr); diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 7f57642e9e..dcfb006b2b 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -46,23 +46,30 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned firstCall = false; } auto addressManager = DependencyManager::get(); - + // handle domain change signals from AddressManager connect(addressManager.data(), &AddressManager::possibleDomainChangeRequired, &_domainHandler, &DomainHandler::setHostnameAndPort); - + connect(addressManager.data(), &AddressManager::possibleDomainChangeRequiredViaICEForID, &_domainHandler, &DomainHandler::setIceServerHostnameAndID); - + + // handle a request for a path change from the AddressManager + connect(addressManager.data(), &AddressManager::pathChangeRequired, this, &NodeList::handleDSPathQuery); + + // in case we don't know how to talk to DS when a path change is requested + // fire off any pending DS path query when we get socket information + connect(&_domainHandler, &DomainHandler::completedSocketDiscovery, this, &NodeList::sendPendingDSPathQuery); + // clear our NodeList when the domain changes connect(&_domainHandler, &DomainHandler::disconnectedFromDomain, this, &NodeList::reset); - + // handle ICE signal from DS so connection is attempted immediately connect(&_domainHandler, &DomainHandler::requestICEConnectionAttempt, this, &NodeList::handleICEConnectionToDomainServer); // clear out NodeList when login is finished connect(&AccountManager::getInstance(), &AccountManager::loginComplete , this, &NodeList::reset); - + // clear our NodeList when logout is requested connect(&AccountManager::getInstance(), &AccountManager::logoutComplete , this, &NodeList::reset); } @@ -75,8 +82,8 @@ qint64 NodeList::sendStats(const QJsonObject& statsObject, const HifiSockAddr& d QStringList statsStringList = JSONBreakableMarshal::toStringList(statsObject, ""); int numBytesWritten = numBytesForPacketHeader; - - // enumerate the resulting strings - pack them and send off packets once we hit MTU size + + // enumerate the resulting strings - pack them and send off packets once we hit MTU size foreach(const QString& statsItem, statsStringList) { QByteArray utf8String = statsItem.toUtf8(); utf8String.append('\0'); @@ -85,14 +92,14 @@ qint64 NodeList::sendStats(const QJsonObject& statsObject, const HifiSockAddr& d // send off the current packet since the next string will make us too big statsPacket.resize(numBytesWritten); writeUnverifiedDatagram(statsPacket, destination); - + // reset the number of bytes written to the size of our packet header numBytesWritten = numBytesForPacketHeader; } - + // write this string into the stats packet statsPacket.replace(numBytesWritten, utf8String.size(), utf8String); - + // keep track of the number of bytes we have written numBytesWritten += utf8String.size(); } @@ -102,7 +109,7 @@ qint64 NodeList::sendStats(const QJsonObject& statsObject, const HifiSockAddr& d statsPacket.resize(numBytesWritten); writeUnverifiedDatagram(statsPacket, destination); } - + // enumerate the resulting strings, breaking them into MTU sized packets return 0; } @@ -114,26 +121,26 @@ qint64 NodeList::sendStatsToDomainServer(const QJsonObject& statsObject) { void NodeList::timePingReply(const QByteArray& packet, const SharedNodePointer& sendingNode) { QDataStream packetStream(packet); packetStream.skipRawData(numBytesForPacketHeader(packet)); - + quint8 pingType; quint64 ourOriginalTime, othersReplyTime; - + packetStream >> pingType >> ourOriginalTime >> othersReplyTime; - + quint64 now = usecTimestampNow(); int pingTime = now - ourOriginalTime; int oneWayFlightTime = pingTime / 2; // half of the ping is our one way flight - + // The other node's expected time should be our original time plus the one way flight time // anything other than that is clock skew quint64 othersExprectedReply = ourOriginalTime + oneWayFlightTime; int clockSkew = othersReplyTime - othersExprectedReply; - + sendingNode->setPingMs(pingTime / 1000); sendingNode->updateClockSkewUsec(clockSkew); const bool wantDebug = false; - + if (wantDebug) { qCDebug(networking) << "PING_REPLY from node " << *sendingNode << "\n" << " now: " << now << "\n" << @@ -172,7 +179,7 @@ void NodeList::processNodeData(const HifiSockAddr& senderSockAddr, const QByteAr matchingNode->setLastHeardMicrostamp(usecTimestampNow()); QByteArray replyPacket = constructPingReplyPacket(packet); writeDatagram(replyPacket, matchingNode, senderSockAddr); - + // If we don't have a symmetric socket for this node and this socket doesn't match // what we have for public and local then set it as the symmetric. // This allows a server on a reachable port to communicate with nodes on symmetric NATs @@ -182,7 +189,7 @@ void NodeList::processNodeData(const HifiSockAddr& senderSockAddr, const QByteAr } } } - + break; } case PacketTypePingReply: { @@ -190,14 +197,14 @@ void NodeList::processNodeData(const HifiSockAddr& senderSockAddr, const QByteAr if (sendingNode) { sendingNode->setLastHeardMicrostamp(usecTimestampNow()); - + // activate the appropriate socket for this node, if not yet updated activateSocketFromNodeCommunication(packet, sendingNode); - + // set the ping time for this node for stat collection timePingReply(packet, sendingNode); } - + break; } case PacketTypeUnverifiedPing: { @@ -208,7 +215,7 @@ void NodeList::processNodeData(const HifiSockAddr& senderSockAddr, const QByteAr } case PacketTypeUnverifiedPingReply: { qCDebug(networking) << "Received reply from domain-server on" << senderSockAddr; - + // for now we're unsafely assuming this came back from the domain if (senderSockAddr == _domainHandler.getICEPeer().getLocalSocket()) { qCDebug(networking) << "Connecting to domain using local socket"; @@ -226,6 +233,10 @@ void NodeList::processNodeData(const HifiSockAddr& senderSockAddr, const QByteAr processSTUNResponse(packet); break; } + case PacketTypeDomainServerPathResponse: { + handleDSPathQueryResponse(packet); + break; + } default: LimitedNodeList::processNodeData(senderSockAddr, packet); break; @@ -234,17 +245,17 @@ void NodeList::processNodeData(const HifiSockAddr& senderSockAddr, const QByteAr void NodeList::reset() { LimitedNodeList::reset(); - + _numNoReplyDomainCheckIns = 0; // refresh the owner UUID to the NULL UUID setSessionUUID(QUuid()); - + if (sender() != &_domainHandler) { // clear the domain connection information, unless they're the ones that asked us to reset _domainHandler.softReset(); } - + // if we setup the DTLS socket, also disconnect from the DTLS socket readyRead() so it can handle handshaking if (_dtlsSocket) { disconnect(_dtlsSocket, 0, this, 0); @@ -267,7 +278,7 @@ void NodeList::sendSTUNRequest() { if (!_hasCompletedInitialSTUNFailure) { qCDebug(networking) << "Sending intial stun request to" << STUN_SERVER_HOSTNAME; } - + LimitedNodeList::sendSTUNRequest(); _stunRequestsSinceSuccess++; @@ -292,9 +303,9 @@ bool NodeList::processSTUNResponse(const QByteArray& packet) { if (LimitedNodeList::processSTUNResponse(packet)) { // reset the number of failed STUN requests since last success _stunRequestsSinceSuccess = 0; - + _hasCompletedInitialSTUNFailure = true; - + return true; } else { return false; @@ -309,23 +320,23 @@ void NodeList::sendDomainServerCheckIn() { } else if (_domainHandler.getIP().isNull() && _domainHandler.requiresICE()) { handleICEConnectionToDomainServer(); } else if (!_domainHandler.getIP().isNull()) { - + bool isUsingDTLS = false; - + PacketType domainPacketType = !_domainHandler.isConnected() ? PacketTypeDomainConnectRequest : PacketTypeDomainListRequest; - + if (!_domainHandler.isConnected()) { qCDebug(networking) << "Sending connect request to domain-server at" << _domainHandler.getHostname(); - + // is this our localhost domain-server? // if so we need to make sure we have an up-to-date local port in case it restarted - + if (_domainHandler.getSockAddr().getAddress() == QHostAddress::LocalHost || _domainHandler.getHostname() == "localhost") { - + static QSharedMemory* localDSPortSharedMem = NULL; - + quint16 domainPort = DEFAULT_DOMAIN_SERVER_PORT; getLocalServerPortFromSharedMemory(DOMAIN_SERVER_LOCAL_PORT_SMEM_KEY, localDSPortSharedMem, @@ -333,12 +344,12 @@ void NodeList::sendDomainServerCheckIn() { qCDebug(networking) << "Local domain-server port read from shared memory (or default) is" << domainPort; _domainHandler.setPort(domainPort); } - + } - + // construct the DS check in packet QUuid packetUUID = _sessionUUID; - + if (domainPacketType == PacketTypeDomainConnectRequest) { if (!_domainHandler.getAssignmentUUID().isNull()) { // this is a connect request and we're an assigned node @@ -351,70 +362,166 @@ void NodeList::sendDomainServerCheckIn() { packetUUID = _domainHandler.getICEClientID(); } } - + QByteArray domainServerPacket = byteArrayWithUUIDPopulatedHeader(domainPacketType, packetUUID); QDataStream packetStream(&domainServerPacket, QIODevice::Append); - + // pack our data to send to the domain-server packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList(); - - + + // if this is a connect request, and we can present a username signature, send it along if (!_domainHandler.isConnected()) { DataServerAccountInfo& accountInfo = AccountManager::getInstance().getAccountInfo(); packetStream << accountInfo.getUsername(); - + const QByteArray& usernameSignature = AccountManager::getInstance().getAccountInfo().getUsernameSignature(); - + if (!usernameSignature.isEmpty()) { qCDebug(networking) << "Including username signature in domain connect request."; packetStream << usernameSignature; } } - + if (!isUsingDTLS) { writeUnverifiedDatagram(domainServerPacket, _domainHandler.getSockAddr()); } - + const int NUM_DOMAIN_SERVER_CHECKINS_PER_STUN_REQUEST = 5; static unsigned int numDomainCheckins = 0; - + // send a STUN request every Nth domain server check in so we update our public socket, if required if (numDomainCheckins++ % NUM_DOMAIN_SERVER_CHECKINS_PER_STUN_REQUEST == 0) { sendSTUNRequest(); } - + if (_numNoReplyDomainCheckIns >= MAX_SILENT_DOMAIN_SERVER_CHECK_INS) { // we haven't heard back from DS in MAX_SILENT_DOMAIN_SERVER_CHECK_INS // so emit our signal that says that emit limitOfSilentDomainCheckInsReached(); } - + // increment the count of un-replied check-ins _numNoReplyDomainCheckIns++; } } +void NodeList::handleDSPathQuery(const QString& newPath) { + if (_domainHandler.isSocketKnown()) { + // if we have a DS socket we assume it will get this packet and send if off right away + sendDSPathQuery(newPath); + } else { + // otherwise we make it pending so that it can be sent once a connection is established + _domainHandler.setPendingPath(newPath); + } +} + +void NodeList::sendPendingDSPathQuery() { + + QString pendingPath = _domainHandler.getPendingPath(); + + if (!pendingPath.isEmpty()) { + qCDebug(networking) << "Attemping to send pending query to DS for path" << pendingPath; + + // this is a slot triggered if we just established a network link with a DS and want to send a path query + sendDSPathQuery(_domainHandler.getPendingPath()); + + // clear whatever the pending path was + _domainHandler.clearPendingPath(); + } +} + +void NodeList::sendDSPathQuery(const QString& newPath) { + // only send a path query if we know who our DS is or is going to be + if (_domainHandler.isSocketKnown()) { + // construct the path query packet + QByteArray pathQueryPacket = byteArrayWithPopulatedHeader(PacketTypeDomainServerPathQuery); + + // get the UTF8 representation of path query + QByteArray pathQueryUTF8 = newPath.toUtf8(); + + // get the size of the UTF8 representation of the desired path + quint16 numPathBytes = pathQueryUTF8.size(); + + if (pathQueryPacket.size() + numPathBytes + sizeof(numPathBytes) < MAX_PACKET_SIZE) { + // append the size of the path to the query packet + pathQueryPacket.append(reinterpret_cast(&numPathBytes), sizeof(numPathBytes)); + + // append the path itself to the query packet + pathQueryPacket.append(pathQueryUTF8); + + qCDebug(networking) << "Sending a path query packet for path" << newPath << "to domain-server at" + << _domainHandler.getSockAddr(); + + // send off the path query + writeUnverifiedDatagram(pathQueryPacket, _domainHandler.getSockAddr()); + } else { + qCDebug(networking) << "Path" << newPath << "would make PacketTypeDomainServerPathQuery packet > MAX_PACKET_SIZE." << + "Will not send query."; + } + } +} + +void NodeList::handleDSPathQueryResponse(const QByteArray& packet) { + // This is a response to a path query we theoretically made. + // In the future we may want to check that this was actually from our DS and for a query we actually made. + + int numHeaderBytes = numBytesForPacketHeaderGivenPacketType(PacketTypeDomainServerPathResponse); + const char* startPosition = packet.data() + numHeaderBytes; + const char* currentPosition = startPosition; + + // figure out how many bytes the path query is + qint16 numPathBytes; + memcpy(&numPathBytes, currentPosition, sizeof(numPathBytes)); + currentPosition += sizeof(numPathBytes); + + // make sure it is safe to pull the path + if (numPathBytes <= packet.size() - numHeaderBytes - (currentPosition - startPosition)) { + // pull the path from the packet + QString pathQuery = QString::fromUtf8(currentPosition, numPathBytes); + currentPosition += numPathBytes; + + // figure out how many bytes the viewpoint is + qint16 numViewpointBytes; + memcpy(&numViewpointBytes, currentPosition, sizeof(numViewpointBytes)); + currentPosition += sizeof(numViewpointBytes); + + // make sure it is safe to pull the viewpoint + if (numViewpointBytes <= packet.size() - numHeaderBytes - (currentPosition - startPosition)) { + // pull the viewpoint from the packet + QString viewpoint = QString::fromUtf8(currentPosition, numViewpointBytes); + + // Hand it off to the AddressManager so it can handle it as a relative viewpoint + if (DependencyManager::get()->goToViewpoint(viewpoint)) { + qCDebug(networking) << "Going to viewpoint" << viewpoint << "which was the lookup result for path" << pathQuery; + } else { + qCDebug(networking) << "Could not go to viewpoint" << viewpoint + << "which was the lookup result for path" << pathQuery; + } + } + } +} + void NodeList::handleICEConnectionToDomainServer() { if (_domainHandler.getICEPeer().isNull() || _domainHandler.getICEPeer().getConnectionAttempts() >= MAX_ICE_CONNECTION_ATTEMPTS) { - + _domainHandler.getICEPeer().resetConnectionAttemps(); - + LimitedNodeList::sendHeartbeatToIceServer(_domainHandler.getICEServerSockAddr(), _domainHandler.getICEClientID(), _domainHandler.getICEDomainID()); } else { qCDebug(networking) << "Sending ping packets to establish connectivity with domain-server with ID" << uuidStringWithoutCurlyBraces(_domainHandler.getICEDomainID()); - + // send the ping packet to the local and public sockets for this node QByteArray localPingPacket = constructPingPacket(PingType::Local, false, _domainHandler.getICEClientID()); writeUnverifiedDatagram(localPingPacket, _domainHandler.getICEPeer().getLocalSocket()); - + QByteArray publicPingPacket = constructPingPacket(PingType::Public, false, _domainHandler.getICEClientID()); writeUnverifiedDatagram(publicPingPacket, _domainHandler.getICEPeer().getPublicSocket()); - + _domainHandler.getICEPeer().incrementConnectionAttempts(); } } @@ -422,18 +529,18 @@ void NodeList::handleICEConnectionToDomainServer() { int NodeList::processDomainServerList(const QByteArray& packet) { // this is a packet from the domain server, reset the count of un-replied check-ins _numNoReplyDomainCheckIns = 0; - + // if this was the first domain-server list from this domain, we've now connected if (!_domainHandler.isConnected()) { _domainHandler.setUUID(uuidFromPacketHeader(packet)); _domainHandler.setIsConnected(true); } - + int readNodes = 0; QDataStream packetStream(packet); packetStream.skipRawData(numBytesForPacketHeader(packet)); - + // pull our owner UUID from the packet, it's always the first thing QUuid newUUID; packetStream >> newUUID; @@ -446,7 +553,7 @@ int NodeList::processDomainServerList(const QByteArray& packet) { bool thisNodeCanRez; packetStream >> thisNodeCanRez; setThisNodeCanRez(thisNodeCanRez); - + // pull each node in the packet while(packetStream.device()->pos() < packet.size()) { // setup variables to read into from QDataStream @@ -466,11 +573,11 @@ int NodeList::processDomainServerList(const QByteArray& packet) { SharedNodePointer node = addOrUpdateNode(nodeUUID, nodeType, nodePublicSocket, nodeLocalSocket, canAdjustLocks, canRez); - + packetStream >> connectionUUID; node->setConnectionSecret(connectionUUID); } - + // ping inactive nodes in conjunction with receipt of list from domain-server // this makes it happen every second and also pings any newly added nodes pingInactiveNodes(); @@ -479,25 +586,25 @@ int NodeList::processDomainServerList(const QByteArray& packet) { } void NodeList::sendAssignment(Assignment& assignment) { - + PacketType assignmentPacketType = assignment.getCommand() == Assignment::CreateCommand ? PacketTypeCreateAssignment : PacketTypeRequestAssignment; - + QByteArray packet = byteArrayWithPopulatedHeader(assignmentPacketType); QDataStream packetStream(&packet, QIODevice::Append); - + packetStream << assignment; _nodeSocket.writeDatagram(packet, _assignmentServerSocket.getAddress(), _assignmentServerSocket.getPort()); } void NodeList::pingPunchForInactiveNode(const SharedNodePointer& node) { - + // send the ping packet to the local and public sockets for this node QByteArray localPingPacket = constructPingPacket(PingType::Local); writeDatagram(localPingPacket, node, node->getLocalSocket()); - + QByteArray publicPingPacket = constructPingPacket(PingType::Public); writeDatagram(publicPingPacket, node, node->getPublicSocket()); @@ -520,10 +627,10 @@ void NodeList::activateSocketFromNodeCommunication(const QByteArray& packet, con // deconstruct this ping packet to see if it is a public or local reply QDataStream packetStream(packet); packetStream.skipRawData(numBytesForPacketHeader(packet)); - + quint8 pingType; packetStream >> pingType; - + // if this is a local or public ping then we can activate a socket // we do nothing with agnostic pings, those are simply for timing if (pingType == PingType::Local && sendingNode->getActiveSocket() != &sendingNode->getLocalSocket()) { diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index d907da6c5a..39b1e3e2d2 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -43,7 +43,7 @@ class Assignment; class NodeList : public LimitedNodeList { Q_OBJECT SINGLETON_DEPENDENCY - + public: NodeType_t getOwnerType() const { return _ownerType; } void setOwnerType(NodeType_t ownerType) { _ownerType = ownerType; } @@ -53,42 +53,49 @@ public: int getNumNoReplyDomainCheckIns() const { return _numNoReplyDomainCheckIns; } DomainHandler& getDomainHandler() { return _domainHandler; } - + const NodeSet& getNodeInterestSet() const { return _nodeTypesOfInterest; } void addNodeTypeToInterestSet(NodeType_t nodeTypeToAdd); void addSetOfNodeTypesToNodeInterestSet(const NodeSet& setOfNodeTypes); void resetNodeInterestSet() { _nodeTypesOfInterest.clear(); } void processNodeData(const HifiSockAddr& senderSockAddr, const QByteArray& packet); - + int processDomainServerList(const QByteArray& packet); void setAssignmentServerSocket(const HifiSockAddr& serverSocket) { _assignmentServerSocket = serverSocket; } void sendAssignment(Assignment& assignment); - + void pingPunchForInactiveNode(const SharedNodePointer& node); public slots: void reset(); void sendDomainServerCheckIn(); void pingInactiveNodes(); + void handleDSPathQuery(const QString& newPath); signals: void limitOfSilentDomainCheckInsReached(); +private slots: + void sendPendingDSPathQuery(); private: NodeList() : LimitedNodeList(0, 0) { assert(false); } // Not implemented, needed for DependencyManager templates compile NodeList(char ownerType, unsigned short socketListenPort = 0, unsigned short dtlsListenPort = 0); NodeList(NodeList const&); // Don't implement, needed to avoid copies of singleton void operator=(NodeList const&); // Don't implement, needed to avoid copies of singleton - + void sendSTUNRequest(); bool processSTUNResponse(const QByteArray& packet); void handleICEConnectionToDomainServer(); - + void processDomainServerAuthRequest(const QByteArray& packet); void requestAuthForDomainServer(); void activateSocketFromNodeCommunication(const QByteArray& packet, const SharedNodePointer& sendingNode); void timePingReply(const QByteArray& packet, const SharedNodePointer& sendingNode); - + + void handleDSPathQueryResponse(const QByteArray& packet); + + void sendDSPathQuery(const QString& newPath); + NodeType_t _ownerType; NodeSet _nodeTypesOfInterest; DomainHandler _domainHandler; @@ -96,7 +103,7 @@ private: HifiSockAddr _assignmentServerSocket; bool _hasCompletedInitialSTUNFailure; unsigned int _stunRequestsSinceSuccess; - + friend class Application; }; diff --git a/libraries/networking/src/PacketHeaders.cpp b/libraries/networking/src/PacketHeaders.cpp index 5f9c5c4fd0..887634cf80 100644 --- a/libraries/networking/src/PacketHeaders.cpp +++ b/libraries/networking/src/PacketHeaders.cpp @@ -72,7 +72,7 @@ PacketVersion versionForPacketType(PacketType packetType) { return 1; case PacketTypeEntityAddOrEdit: case PacketTypeEntityData: - return VERSION_ENTITIES_PARTICLE_ENTITIES_HAVE_TEXTURES; + return VERSION_ENTITIES_HAVE_LINE_TYPE; case PacketTypeEntityErase: return 2; case PacketTypeAudioStreamStats: diff --git a/libraries/networking/src/PacketHeaders.h b/libraries/networking/src/PacketHeaders.h index 6f71655edf..1dff5d7c79 100644 --- a/libraries/networking/src/PacketHeaders.h +++ b/libraries/networking/src/PacketHeaders.h @@ -47,8 +47,8 @@ enum PacketType { PacketTypeMuteEnvironment, PacketTypeAudioStreamStats, PacketTypeDataServerConfirm, // 20 - UNUSED_1, - UNUSED_2, + PacketTypeDomainServerPathQuery, + PacketTypeDomainServerPathResponse, UNUSED_3, UNUSED_4, UNUSED_5, // 25 @@ -96,7 +96,8 @@ const QSet NON_VERIFIED_PACKETS = QSet() << PacketTypeNodeJsonStats << PacketTypeEntityQuery << PacketTypeOctreeDataNack << PacketTypeEntityEditNack << PacketTypeIceServerHeartbeat << PacketTypeIceServerHeartbeatResponse - << PacketTypeUnverifiedPing << PacketTypeUnverifiedPingReply << PacketTypeStopNode; + << PacketTypeUnverifiedPing << PacketTypeUnverifiedPingReply << PacketTypeStopNode + << PacketTypeDomainServerPathQuery << PacketTypeDomainServerPathResponse; const QSet SEQUENCE_NUMBERED_PACKETS = QSet() << PacketTypeAvatarData; @@ -122,7 +123,7 @@ int numSequenceNumberBytesForType(PacketType packetType); int numBytesForPacketHeader(const QByteArray& packet); int numBytesForPacketHeader(const char* packet); -int numBytesForArithmeticCodedPacketType(PacketType packetType); +int numBytesForArithmeticCodedPacketType(PacketType packetType); int numBytesForPacketHeaderGivenPacketType(PacketType packetType); QUuid uuidFromPacketHeader(const QByteArray& packet); @@ -138,9 +139,9 @@ QByteArray hashForPacketAndConnectionUUID(const QByteArray& packet, const QUuid& PacketSequenceNumber sequenceNumberFromHeader(const QByteArray& packet, PacketType packetType = PacketTypeUnknown); -void replaceHashInPacket(QByteArray& packet, const QUuid& connectionUUID, PacketType packetType = PacketTypeUnknown); +void replaceHashInPacket(QByteArray& packet, const QUuid& connectionUUID, PacketType packetType = PacketTypeUnknown); -void replaceSequenceNumberInPacket(QByteArray& packet, PacketSequenceNumber sequenceNumber, +void replaceSequenceNumberInPacket(QByteArray& packet, PacketSequenceNumber sequenceNumber, PacketType packetType = PacketTypeUnknown); void replaceHashAndSequenceNumberInPacket(QByteArray& packet, const QUuid& connectionUUID, PacketSequenceNumber sequenceNumber, @@ -174,5 +175,6 @@ const PacketVersion VERSION_ENTITIES_ZONE_ENTITIES_HAVE_ATMOSPHERE = 20; const PacketVersion VERSION_ENTITIES_ZONE_ENTITIES_HAVE_SKYBOX = 21; const PacketVersion VERSION_ENTITIES_ZONE_ENTITIES_STAGE_HAS_AUTOMATIC_HOURDAY = 22; const PacketVersion VERSION_ENTITIES_PARTICLE_ENTITIES_HAVE_TEXTURES = 23; +const PacketVersion VERSION_ENTITIES_HAVE_LINE_TYPE = 24; #endif // hifi_PacketHeaders_h diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index c92260210e..3fe5d9ec3f 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -72,7 +72,7 @@ QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringL int configIndex = argumentList.indexOf(CONFIG_FILE_OPTION); QString configFilePath; - + if (configIndex != -1) { // we have a config file - try and read it configFilePath = argumentList[configIndex + 1]; @@ -82,8 +82,8 @@ QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringL QCoreApplication::organizationName(), QCoreApplication::applicationName()); } - - + + return mergedMap; } @@ -94,23 +94,23 @@ HifiConfigVariantMap::HifiConfigVariantMap() : _userConfig(), _mergedConfig() { - + } void HifiConfigVariantMap::loadMasterAndUserConfig(const QStringList& argumentList) { // check if there is a master config file const QString MASTER_CONFIG_FILE_OPTION = "--master-config"; - + int masterConfigIndex = argumentList.indexOf(MASTER_CONFIG_FILE_OPTION); if (masterConfigIndex != -1) { QString masterConfigFilepath = argumentList[masterConfigIndex + 1]; - + loadMapFromJSONFile(_masterConfig, masterConfigFilepath); } - + // load the user config const QString USER_CONFIG_FILE_OPTION = "--user-config"; - + int userConfigIndex = argumentList.indexOf(USER_CONFIG_FILE_OPTION); if (userConfigIndex != -1) { _userConfigFilename = argumentList[userConfigIndex + 1]; @@ -119,26 +119,26 @@ void HifiConfigVariantMap::loadMasterAndUserConfig(const QStringList& argumentLi QCoreApplication::organizationName(), QCoreApplication::applicationName()); } - + loadMapFromJSONFile(_userConfig, _userConfigFilename); - + // the merged config is initially matched to the master config _mergedConfig = _masterConfig; - + // then we merge in anything missing from the user config addMissingValuesToExistingMap(_mergedConfig, _userConfig); } void HifiConfigVariantMap::loadMapFromJSONFile(QVariantMap& existingMap, const QString& filename) { QFile configFile(filename); - + if (configFile.exists()) { qCDebug(shared) << "Reading JSON config file at" << filename; configFile.open(QIODevice::ReadOnly); - + QJsonDocument configDocument = QJsonDocument::fromJson(configFile.readAll()); existingMap = configDocument.toVariant().toMap(); - + } else { qCDebug(shared) << "Could not find JSON config file at" << filename; } @@ -148,7 +148,7 @@ void HifiConfigVariantMap::addMissingValuesToExistingMap(QVariantMap& existingMa foreach(const QString& key, newMap.keys()) { if (existingMap.contains(key)) { // if this is just a regular value, we're done - we don't ovveride - + if (newMap[key].canConvert(QMetaType::QVariantMap) && existingMap[key].canConvert(QMetaType::QVariantMap)) { // there's a variant map below and the existing map has one too, so we need to keep recursing addMissingValuesToExistingMap(*static_cast(existingMap[key].data()), newMap[key].toMap()); @@ -159,11 +159,11 @@ void HifiConfigVariantMap::addMissingValuesToExistingMap(QVariantMap& existingMa } } -const QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath) { +QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath) { int dotIndex = keyPath.indexOf('.'); - + QString firstKey = (dotIndex == -1) ? keyPath : keyPath.mid(0, dotIndex); - + if (variantMap.contains(firstKey)) { if (dotIndex == -1) { return &variantMap[firstKey]; @@ -171,6 +171,6 @@ const QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath) return valueForKeyPath(*static_cast(variantMap[firstKey].data()), keyPath.mid(dotIndex + 1)); } } - + return NULL; } diff --git a/libraries/shared/src/HifiConfigVariantMap.h b/libraries/shared/src/HifiConfigVariantMap.h index 6bdeb15589..3566f446a2 100644 --- a/libraries/shared/src/HifiConfigVariantMap.h +++ b/libraries/shared/src/HifiConfigVariantMap.h @@ -17,26 +17,26 @@ class HifiConfigVariantMap { public: static QVariantMap mergeCLParametersWithJSONConfig(const QStringList& argumentList); - + HifiConfigVariantMap(); void loadMasterAndUserConfig(const QStringList& argumentList); - + const QVariantMap& getMasterConfig() const { return _masterConfig; } QVariantMap& getUserConfig() { return _userConfig; } QVariantMap& getMergedConfig() { return _mergedConfig; } - + const QString& getUserConfigFilename() const { return _userConfigFilename; } private: QString _userConfigFilename; - + QVariantMap _masterConfig; QVariantMap _userConfig; QVariantMap _mergedConfig; - + void loadMapFromJSONFile(QVariantMap& existingMap, const QString& filename); void addMissingValuesToExistingMap(QVariantMap& existingMap, const QVariantMap& newMap); }; -const QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath); +QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath); #endif // hifi_HifiConfigVariantMap_h diff --git a/libraries/ui/src/InfoView.cpp b/libraries/ui/src/InfoView.cpp index 3f80c97c4c..dd754f209a 100644 --- a/libraries/ui/src/InfoView.cpp +++ b/libraries/ui/src/InfoView.cpp @@ -51,15 +51,16 @@ void InfoView::show(const QString& path, bool firstOrChangedOnly) { } if (firstOrChangedOnly) { const QString lastVersion = infoVersion.get(); + const QString version = fetchVersion(url); // If we have version information stored if (lastVersion != QString::null) { // Check to see the document version. If it's valid and matches // the stored version, we're done, so exit - const QString version = fetchVersion(url); if (version == QString::null || version == lastVersion) { return; } } + infoVersion.set(version); } auto offscreenUi = DependencyManager::get(); QString infoViewName(NAME + "_" + path);