// // AddressManager.cpp // libraries/networking/src // // Created by Stephen Birarda on 2014-09-10. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include #include #include #include #include #include #include #include #include #include "NodeList.h" #include "NetworkLogging.h" #include "AddressManager.h" const QString ADDRESS_MANAGER_SETTINGS_GROUP = "AddressManager"; const QString SETTINGS_CURRENT_ADDRESS_KEY = "address"; Setting::Handle currentAddressHandle(QStringList() << ADDRESS_MANAGER_SETTINGS_GROUP << "address", DEFAULT_HIFI_ADDRESS); AddressManager::AddressManager() : _rootPlaceName(), _rootPlaceID(), _positionGetter(NULL), _orientationGetter(NULL) { connect(qApp, &QCoreApplication::aboutToQuit, this, &AddressManager::storeCurrentAddress); } bool AddressManager::isConnected() { return DependencyManager::get()->getDomainHandler().isConnected(); } const QUrl AddressManager::currentAddress() const { QUrl hifiURL; hifiURL.setScheme(HIFI_URL_SCHEME); hifiURL.setHost(_rootPlaceName); hifiURL.setPath(currentPath()); return hifiURL; } void AddressManager::loadSettings(const QString& lookupString) { if (lookupString.isEmpty()) { handleLookupString(currentAddressHandle.get().toString()); } else { handleLookupString(lookupString); } } void AddressManager::storeCurrentAddress() { currentAddressHandle.set(currentAddress()); } const QString AddressManager::currentPath(bool withOrientation) const { if (_positionGetter) { QString pathString = "/" + createByteArray(_positionGetter()); if (withOrientation) { if (_orientationGetter) { QString orientationString = createByteArray(_orientationGetter()); pathString += "/" + orientationString; } else { 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." << "Call AddressManager::setPositionGetter to pass a function that will return a const glm::vec3&"; return QString(); } } 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 // check if it is a network address first 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()); } 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()); emit lookupResultsFinished(); } return false; } void AddressManager::handleLookupString(const QString& lookupString) { if (!lookupString.isEmpty()) { // 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); } } 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; } } 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; } } } } else { qCDebug(networking) << "Received an address manager API response with no domain key. Cannot parse."; qCDebug(networking) << locationMap; } } else { // we've been told that this result exists but is offline, emit our signal so the application can handle emit lookupResultIsOffline(); } } else { qCDebug(networking) << "Received an address manager API response with no location key or place key. Cannot parse."; qCDebug(networking) << locationMap; } } void AddressManager::handleAPIError(QNetworkReply& errorReply) { qCDebug(networking) << "AddressManager API error -" << errorReply.error() << "-" << errorReply.errorString(); if (errorReply.error() == QNetworkReply::ContentNotFoundError) { emit lookupResultIsNotFound(); } emit lookupResultsFinished(); } 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, apiCallbackParameters(), QByteArray(), NULL, requestParams); } 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) { 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 + FLOAT_REGEX_STRING + SPACED_COMMA_REGEX_STRING + FLOAT_REGEX_STRING + "\\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) { // 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) { 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); return true; } else { 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; } 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; } void AddressManager::setRootPlaceName(const QString& rootPlaceName) { if (rootPlaceName != _rootPlaceName) { _rootPlaceName = rootPlaceName; emit rootPlaceNameChanged(_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); } void AddressManager::goToUser(const QString& username) { QString formattedUsername = QUrl::toPercentEncoding(username); // this is a username - pull the captured name and lookup that user's location AccountManager::getInstance().sendRequest(GET_USER_LOCATION.arg(formattedUsername), AccountManagerAuth::Optional, QNetworkAccessManager::GetOperation, apiCallbackParameters()); } void AddressManager::copyAddress() { QApplication::clipboard()->setText(currentAddress().toString()); } void AddressManager::copyPath() { QApplication::clipboard()->setText(currentPath()); }