Merge pull request #3598 from birarda/domain-tunnel

initial pass at using keypairs for username verification for basic domain-server ACL
This commit is contained in:
AndrewMeadows 2014-10-16 10:49:03 -07:00
commit 4fbc589606
35 changed files with 537 additions and 667 deletions

View file

@ -861,8 +861,6 @@ void OctreeServer::readPendingDatagram(const QByteArray& receivedPacket, const H
}
} else if (packetType == PacketTypeJurisdictionRequest) {
_jurisdictionSender->queueReceivedPacket(matchingNode, receivedPacket);
} else if (packetType == PacketTypeSignedTransactionPayment) {
handleSignedTransactionPayment(packetType, receivedPacket);
} else if (_octreeInboundPacketProcessor && getOctree()->handlesEditPacketType(packetType)) {
_octreeInboundPacketProcessor->queueReceivedPacket(matchingNode, receivedPacket);
} else {
@ -1245,51 +1243,6 @@ QString OctreeServer::getStatusLink() {
return result;
}
void OctreeServer::handleSignedTransactionPayment(PacketType packetType, const QByteArray& datagram) {
// for now we're not verifying that this is actual payment for any octree edits
// just use the AccountManager to send it up to the data server and have it redeemed
AccountManager& accountManager = AccountManager::getInstance();
const int NUM_BYTES_SIGNED_TRANSACTION_BINARY_MESSAGE = 72;
const int NUM_BYTES_SIGNED_TRANSACTION_BINARY_SIGNATURE = 256;
int numBytesPacketHeader = numBytesForPacketHeaderGivenPacketType(packetType);
// pull out the transaction message in binary
QByteArray messageHex = datagram.mid(numBytesPacketHeader, NUM_BYTES_SIGNED_TRANSACTION_BINARY_MESSAGE).toHex();
// pull out the binary signed message digest
QByteArray signatureHex = datagram.mid(numBytesPacketHeader + NUM_BYTES_SIGNED_TRANSACTION_BINARY_MESSAGE,
NUM_BYTES_SIGNED_TRANSACTION_BINARY_SIGNATURE).toHex();
// setup the QJSONObject we are posting
QJsonObject postObject;
const QString TRANSACTION_OBJECT_MESSAGE_KEY = "message";
const QString TRANSACTION_OBJECT_SIGNATURE_KEY = "signature";
const QString POST_OBJECT_TRANSACTION_KEY = "transaction";
QJsonObject transactionObject;
transactionObject.insert(TRANSACTION_OBJECT_MESSAGE_KEY, QString(messageHex));
transactionObject.insert(TRANSACTION_OBJECT_SIGNATURE_KEY, QString(signatureHex));
postObject.insert(POST_OBJECT_TRANSACTION_KEY, transactionObject);
// setup our callback params
JSONCallbackParameters callbackParameters;
callbackParameters.jsonCallbackReceiver = this;
callbackParameters.jsonCallbackMethod = "handleSignedTransactionPaymentResponse";
accountManager.unauthenticatedRequest("/api/v1/transactions/redeem", QNetworkAccessManager::PostOperation,
callbackParameters, QJsonDocument(postObject).toJson());
}
void OctreeServer::handleSignedTransactionPaymentResponse(const QJsonObject& jsonObject) {
// pull the ID to debug the transaction
QString transactionIDString = jsonObject["data"].toObject()["transaction"].toObject()["id"].toString();
qDebug() << "Redeemed transaction with ID" << transactionIDString << "successfully.";
}
void OctreeServer::sendStatsPacket() {
// TODO: we have too many stats to fit in a single MTU... so for now, we break it into multiple JSON objects and
// send them separately. What we really should do is change the NodeList::sendStatsToDomainServer() to handle the

View file

@ -127,8 +127,6 @@ public slots:
void nodeKilled(SharedNodePointer node);
void sendStatsPacket();
void handleSignedTransactionPaymentResponse(const QJsonObject& jsonObject);
void readPendingDatagrams() { }; // this will not be called since our datagram processing thread will handle
void readPendingDatagram(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr);
@ -141,7 +139,6 @@ protected:
QString getConfiguration();
QString getStatusLink();
void handleSignedTransactionPayment(PacketType packetType, const QByteArray& datagram);
void setupDatagramProcessingThread();
int _argc;

View file

@ -38,4 +38,18 @@ endif ()
# link the shared hifi libraries
link_hifi_libraries(embedded-webserver networking shared)
# find OpenSSL
find_package(OpenSSL REQUIRED)
if (APPLE AND ${OPENSSL_INCLUDE_DIR} STREQUAL "/usr/include")
# this is a user on OS X using system OpenSSL, which is going to throw warnings since they're deprecating for their common crypto
message(WARNING "The found version of OpenSSL is the OS X system version. This will produce deprecation warnings."
"\nWe recommend you install a newer version (at least 1.0.1h) in a different directory and set OPENSSL_ROOT_DIR in your env so Cmake can find it.")
endif ()
include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}")
# append OpenSSL to our list of libraries to link
list(APPEND ${TARGET_NAME}_LIBRARIES_TO_LINK "${OPENSSL_LIBRARIES}")
link_shared_dependencies()

View file

@ -59,6 +59,20 @@
"type": "password",
"help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.",
"value-hidden": true
},
{
"name": "allowed_users",
"type": "table",
"label": "Allowed Users",
"help": "List the High Fidelity names for people you want to be able to connect to this domain.<br/>An empty list means everyone.<br/>You can always connect from this machine.",
"numbered": false,
"columns": [
{
"name": "username",
"label": "Username",
"can_set": true
}
]
}
]
},

View file

@ -259,7 +259,7 @@ function makeTable(setting, setting_name, setting_value) {
html += "<td class='data'><strong>" + col.label + "</strong></td>" // Data
})
html += "<td class='buttons'><strong>+/-</strong></td></tr>"
html += "<td class='buttons'></td></tr>"
// populate rows in the table from existing values
var row_num = 1
@ -279,13 +279,13 @@ function makeTable(setting, setting_name, setting_value) {
html += "<td class='" + Settings.DATA_COL_CLASS + "'>"
if (isArray) {
colIsArray = _.isArray(row)
colValue = colIsArray ? row : row[col.name]
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 += "<input type='hidden' name='" + setting_name + "[" + indexOrName + "]"
+ (colIsArray ? "" : "." + col.name) + "' value='" + colValue + "'/>"
+ (rowIsObject ? "." + col.name : "") + "' value='" + colValue + "'/>"
} else if (row.hasOwnProperty(col.name)) {
html += row[col.name]
}

View file

@ -9,6 +9,9 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <openssl/err.h>
#include <openssl/rsa.h>
#include <QtCore/QDir>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
@ -44,8 +47,6 @@ DomainServer::DomainServer(int argc, char* argv[]) :
_oauthProviderURL(),
_oauthClientID(),
_hostname(),
_networkReplyUUIDMap(),
_sessionAuthenticationHash(),
_webAuthenticationStateSet(),
_cookieSessionHash(),
_settingsManager()
@ -80,6 +81,9 @@ DomainServer::DomainServer(int argc, char* argv[]) :
// setup automatic networking settings with data server
setupAutomaticNetworking();
// preload some user public keys so they can connect on first request
preloadAllowedUserPublicKeys();
}
}
@ -507,8 +511,6 @@ void DomainServer::populateDefaultStaticAssignmentsExcludingTypes(const QSet<Ass
}
}
const QString ALLOWED_ROLES_CONFIG_KEY = "allowed-roles";
const NodeSet STATICALLY_ASSIGNED_NODES = NodeSet() << NodeType::AudioMixer
<< NodeType::AvatarMixer << NodeType::VoxelServer << NodeType::ParticleServer << NodeType::EntityServer
<< NodeType::MetavoxelServer;
@ -517,8 +519,11 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock
NodeType_t nodeType;
HifiSockAddr publicSockAddr, localSockAddr;
QDataStream packetStream(packet);
packetStream.skipRawData(numBytesForPacketHeader(packet));
int numPreInterestBytes = parseNodeDataFromByteArray(nodeType, publicSockAddr, localSockAddr, packet, senderSockAddr);
parseNodeDataFromByteArray(packetStream, nodeType, publicSockAddr, localSockAddr, senderSockAddr);
QUuid packetUUID = uuidFromPacketHeader(packet);
@ -551,33 +556,20 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock
}
QString connectedUsername;
if (!isAssignment && !_oauthProviderURL.isEmpty() && _settingsManager.getSettingsMap().contains(ALLOWED_ROLES_CONFIG_KEY)) {
// this is an Agent, and we require authentication so we can compare the user's roles to our list of allowed ones
if (_sessionAuthenticationHash.contains(packetUUID)) {
connectedUsername = _sessionAuthenticationHash.take(packetUUID);
if (connectedUsername.isEmpty()) {
// we've decided this is a user that isn't allowed in, return out
// TODO: provide information to the user so they know why they can't connect
return;
} else {
// we're letting this user in, don't return and remove their UUID from the hash
_sessionAuthenticationHash.remove(packetUUID);
}
} else {
// we don't know anything about this client
// we have an OAuth provider, ask this interface client to auth against it
QByteArray oauthRequestByteArray = byteArrayWithPopulatedHeader(PacketTypeDomainOAuthRequest);
QDataStream oauthRequestStream(&oauthRequestByteArray, QIODevice::Append);
QUrl authorizationURL = packetUUID.isNull() ? oauthAuthorizationURL() : oauthAuthorizationURL(packetUUID);
oauthRequestStream << authorizationURL;
// send this oauth request datagram back to the client
LimitedNodeList::getInstance()->writeUnverifiedDatagram(oauthRequestByteArray, senderSockAddr);
return;
}
QList<NodeType_t> nodeInterestList;
QString username;
QByteArray usernameSignature;
packetStream >> nodeInterestList >> username >> usernameSignature;
if (!isAssignment && !shouldAllowConnectionFromNode(username, usernameSignature, senderSockAddr)) {
// this is an agent and we've decided we won't let them connect - send them a packet to deny connection
QByteArray usernameRequestByteArray = byteArrayWithPopulatedHeader(PacketTypeDomainConnectionDenied);
// send this oauth request datagram back to the client
LimitedNodeList::getInstance()->writeUnverifiedDatagram(usernameRequestByteArray, senderSockAddr);
return;
}
if ((!isAssignment && !STATICALLY_ASSIGNED_NODES.contains(nodeType))
@ -610,15 +602,109 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock
}
// if we have a username from an OAuth connect request, set it on the DomainServerNodeData
nodeData->setUsername(connectedUsername);
nodeData->setUsername(username);
nodeData->setSendingSockAddr(senderSockAddr);
// reply back to the user with a PacketTypeDomainList
sendDomainListToNode(newNode, senderSockAddr, nodeInterestListFromPacket(packet, numPreInterestBytes));
sendDomainListToNode(newNode, senderSockAddr, nodeInterestList.toSet());
}
}
const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users";
bool DomainServer::shouldAllowConnectionFromNode(const QString& username,
const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr) {
static const QVariant* allowedUsersVariant = valueForKeyPath(_settingsManager.getSettingsMap(),
ALLOWED_USERS_SETTINGS_KEYPATH);
static QStringList allowedUsers = allowedUsersVariant ? allowedUsersVariant->toStringList() : QStringList();
if (allowedUsers.count() > 0) {
// this is an agent, we need to ask them to provide us with their signed username to see if they are allowed in
// we always let in a user who is sending a packet from our local socket or from the localhost address
if (senderSockAddr.getAddress() != LimitedNodeList::getInstance()->getLocalSockAddr().getAddress()
&& senderSockAddr.getAddress() != QHostAddress::LocalHost) {
if (allowedUsers.contains(username)) {
// 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<const unsigned char*>(publicKeyArray.constData());
// first load up the public key into an RSA struct
RSA* rsaPublicKey = d2i_RSAPublicKey(NULL, &publicKeyData, publicKeyArray.size());
if (rsaPublicKey) {
QByteArray decryptedArray(RSA_size(rsaPublicKey), 0);
int decryptResult = RSA_public_decrypt(usernameSignature.size(),
reinterpret_cast<const unsigned char*>(usernameSignature.constData()),
reinterpret_cast<unsigned char*>(decryptedArray.data()),
rsaPublicKey, RSA_PKCS1_PADDING);
if (decryptResult != -1) {
if (username == 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.";
}
} else {
qDebug() << "Couldn't decrypt user signature for" << username << "- denying connection.";
}
// free up the public key, we don't need it anymore
RSA_free(rsaPublicKey);
} else {
// we can't let this user in since we couldn't convert their public key to an RSA key we could use
qDebug() << "Couldn't convert data to RSA key for" << username << "- denying connection.";
}
}
requestUserPublicKey(username);
}
}
} else {
// since we have no allowed user list, let them all in
return true;
}
return false;
}
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
foreach(const QString& username, allowedUsers) {
requestUserPublicKey(username);
}
}
}
void DomainServer::requestUserPublicKey(const QString& username) {
// even if we have a public key for them right now, request a new one in case it has just changed
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().unauthenticatedRequest(USER_PUBLIC_KEY_PATH.arg(username),
QNetworkAccessManager::GetOperation, callbackParams);
}
QUrl DomainServer::oauthRedirectURL() {
return QString("https://%1:%2/oauth").arg(_hostname).arg(_httpsManager->serverPort());
}
@ -653,12 +739,9 @@ QUrl DomainServer::oauthAuthorizationURL(const QUuid& stateUUID) {
return authorizationURL;
}
int DomainServer::parseNodeDataFromByteArray(NodeType_t& nodeType, HifiSockAddr& publicSockAddr,
HifiSockAddr& localSockAddr, const QByteArray& packet,
const HifiSockAddr& senderSockAddr) {
QDataStream packetStream(packet);
packetStream.skipRawData(numBytesForPacketHeader(packet));
int DomainServer::parseNodeDataFromByteArray(QDataStream& packetStream, NodeType_t& nodeType,
HifiSockAddr& publicSockAddr, HifiSockAddr& localSockAddr,
const HifiSockAddr& senderSockAddr) {
packetStream >> nodeType;
packetStream >> publicSockAddr >> localSockAddr;
@ -925,7 +1008,30 @@ void DomainServer::sendPendingTransactionsToServer() {
++i;
}
}
}
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());
}
}
}
void DomainServer::transactionJSONCallback(const QJsonObject& data) {
@ -1095,9 +1201,13 @@ void DomainServer::processDatagram(const QByteArray& receivedPacket, const HifiS
if (!nodeUUID.isNull() && nodeList->nodeWithUUID(nodeUUID)) {
NodeType_t throwawayNodeType;
HifiSockAddr nodePublicAddress, nodeLocalAddress;
QDataStream packetStream(receivedPacket);
packetStream.skipRawData(numBytesForPacketHeader(receivedPacket));
int numNodeInfoBytes = parseNodeDataFromByteArray(throwawayNodeType, nodePublicAddress, nodeLocalAddress,
receivedPacket, senderSockAddr);
int numNodeInfoBytes = parseNodeDataFromByteArray(packetStream, throwawayNodeType,
nodePublicAddress, nodeLocalAddress,
senderSockAddr);
SharedNodePointer checkInNode = nodeList->updateSocketsForNode(nodeUUID, nodePublicAddress, nodeLocalAddress);
@ -1545,13 +1655,6 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u
// we've redirected the user back to our homepage
return true;
} else {
qDebug() << "Requesting a token for user with session UUID" << uuidStringWithoutCurlyBraces(stateUUID);
// insert this to our pending token replies so we can associate the returned access token with the right UUID
_networkReplyUUIDMap.insert(tokenReply, stateUUID);
connect(tokenReply, &QNetworkReply::finished, this, &DomainServer::handleTokenRequestFinished);
}
}
@ -1695,22 +1798,6 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
const QString OAUTH_JSON_ACCESS_TOKEN_KEY = "access_token";
void DomainServer::handleTokenRequestFinished() {
QNetworkReply* networkReply = reinterpret_cast<QNetworkReply*>(sender());
QUuid matchingSessionUUID = _networkReplyUUIDMap.take(networkReply);
if (!matchingSessionUUID.isNull() && networkReply->error() == QNetworkReply::NoError) {
qDebug() << "Received access token for user with UUID" << uuidStringWithoutCurlyBraces(matchingSessionUUID)
<< "-" << "requesting profile.";
QNetworkReply* profileReply = profileRequestGivenTokenReply(networkReply);
connect(profileReply, &QNetworkReply::finished, this, &DomainServer::handleProfileRequestFinished);
_networkReplyUUIDMap.insert(profileReply, matchingSessionUUID);
}
}
QNetworkReply* DomainServer::profileRequestGivenTokenReply(QNetworkReply* tokenReply) {
// pull the access token from the returned JSON and store it with the matching session UUID
@ -1719,54 +1806,12 @@ QNetworkReply* DomainServer::profileRequestGivenTokenReply(QNetworkReply* tokenR
// 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/users/profile");
profileURL.setPath("/api/v1/user/profile");
profileURL.setQuery(QString("%1=%2").arg(OAUTH_JSON_ACCESS_TOKEN_KEY, accessToken));
return NetworkAccessManager::getInstance().get(QNetworkRequest(profileURL));
}
void DomainServer::handleProfileRequestFinished() {
QNetworkReply* networkReply = reinterpret_cast<QNetworkReply*>(sender());
QUuid matchingSessionUUID = _networkReplyUUIDMap.take(networkReply);
if (!matchingSessionUUID.isNull() && networkReply->error() == QNetworkReply::NoError) {
QJsonDocument profileJSON = QJsonDocument::fromJson(networkReply->readAll());
if (profileJSON.object()["status"].toString() == "success") {
// pull the user roles from the response
QJsonArray userRolesArray = profileJSON.object()["data"].toObject()["user"].toObject()["roles"].toArray();
QStringList allowedRolesArray = _settingsManager.getSettingsMap().value(ALLOWED_ROLES_CONFIG_KEY).toStringList();
QString connectableUsername;
QString profileUsername = profileJSON.object()["data"].toObject()["user"].toObject()["username"].toString();
foreach(const QJsonValue& roleValue, userRolesArray) {
if (allowedRolesArray.contains(roleValue.toString())) {
// the user has a role that lets them in
// set the bool to true and break
connectableUsername = profileUsername;
break;
}
}
if (connectableUsername.isEmpty()) {
qDebug() << "User" << profileUsername << "with session UUID"
<< uuidStringWithoutCurlyBraces(matchingSessionUUID)
<< "does not have an allowable role. Refusing connection.";
} else {
qDebug() << "User" << profileUsername << "with session UUID"
<< uuidStringWithoutCurlyBraces(matchingSessionUUID)
<< "has an allowable role. Can connect.";
}
// insert this UUID and a flag that indicates if they are allowed to connect
_sessionAuthenticationHash.insert(matchingSessionUUID, connectableUsername);
}
}
}
const QString DS_SETTINGS_SESSIONS_GROUP = "web-sessions";
Headers DomainServer::setupCookieHeadersFromProfileReply(QNetworkReply* profileReply) {

View file

@ -52,6 +52,7 @@ public slots:
/// 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();
@ -82,8 +83,17 @@ private:
void processDatagram(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr);
void handleConnectRequest(const QByteArray& packet, const HifiSockAddr& senderSockAddr);
int parseNodeDataFromByteArray(NodeType_t& nodeType, HifiSockAddr& publicSockAddr,
HifiSockAddr& localSockAddr, const QByteArray& packet, const HifiSockAddr& senderSockAddr);
bool shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr);
void preloadAllowedUserPublicKeys();
void requestUserPublicKey(const QString& username);
int parseNodeDataFromByteArray(QDataStream& packetStream,
NodeType_t& nodeType,
HifiSockAddr& publicSockAddr,
HifiSockAddr& localSockAddr,
const HifiSockAddr& senderSockAddr);
NodeSet nodeInterestListFromPacket(const QByteArray& packet, int numPreceedingBytes);
void sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr& senderSockAddr,
const NodeSet& nodeInterestList);
@ -131,13 +141,11 @@ private:
QString _oauthClientID;
QString _oauthClientSecret;
QString _hostname;
QMap<QNetworkReply*, QUuid> _networkReplyUUIDMap;
QHash<QUuid, QString> _sessionAuthenticationHash;
QSet<QUuid> _webAuthenticationStateSet;
QHash<QUuid, DomainServerWebSessionData> _cookieSessionHash;
HifiSockAddr _localSockAddr;
QHash<QString, QByteArray> _userPublicKeys;
QHash<QUuid, NetworkPeer> _connectingICEPeers;
QHash<QUuid, HifiSockAddr> _connectedICEPeers;

View file

@ -105,7 +105,6 @@ link_hifi_libraries(shared octree voxels fbx metavoxels networking particles ent
# find any optional and required libraries
find_package(ZLIB REQUIRED)
find_package(OpenSSL REQUIRED)
# perform standard include and linking for found externals
foreach(EXTERNAL ${OPTIONAL_EXTERNALS})
@ -169,10 +168,9 @@ endif ()
# include headers for interface and InterfaceConfig.
include_directories("${PROJECT_SOURCE_DIR}/src" "${PROJECT_BINARY_DIR}/includes")
include_directories("${OPENSSL_INCLUDE_DIR}")
target_link_libraries(
${TARGET_NAME} ${ZLIB_LIBRARIES} ${OPENSSL_LIBRARIES}
${TARGET_NAME} ${ZLIB_LIBRARIES}
Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL Qt5::Script Qt5::Svg Qt5::WebKitWidgets
)

View file

@ -72,7 +72,6 @@
#include "InterfaceVersion.h"
#include "Menu.h"
#include "ModelUploader.h"
#include "PaymentManager.h"
#include "Util.h"
#include "devices/MIDIManager.h"
#include "devices/OculusManager.h"
@ -90,7 +89,6 @@
#include "scripting/WindowScriptingInterface.h"
#include "ui/InfoView.h"
#include "ui/OAuthWebViewHandler.h"
#include "ui/Snapshot.h"
#include "ui/Stats.h"
#include "ui/TextRenderer.h"
@ -220,10 +218,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
listenPort = atoi(portStr);
}
// call the OAuthWebviewHandler static getter so that its instance lives in our thread
// make sure it is ready before the NodeList might need it
OAuthWebViewHandler::getInstance();
// start the nodeThread so its event loop is running
_nodeThread->start();
@ -256,11 +250,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
connect(&domainHandler, &DomainHandler::settingsReceived, this, &Application::domainSettingsReceived);
connect(&domainHandler, &DomainHandler::hostnameChanged, Menu::getInstance(), &Menu::clearLoginDialogDisplayedFlag);
// hookup VoxelEditSender to PaymentManager so we can pay for octree edits
const PaymentManager& paymentManager = PaymentManager::getInstance();
connect(&_voxelEditSender, &VoxelEditPacketSender::octreePaymentRequired,
&paymentManager, &PaymentManager::sendSignedPayment);
// update our location every 5 seconds in the data-server, assuming that we are authenticated with one
const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * 1000;
@ -3483,7 +3472,7 @@ void Application::updateLocationInServer() {
rootObject.insert(LOCATION_KEY_IN_ROOT, locationObject);
accountManager.authenticatedRequest("/api/v1/users/location", QNetworkAccessManager::PutOperation,
accountManager.authenticatedRequest("/api/v1/user/location", QNetworkAccessManager::PutOperation,
JSONCallbackParameters(), QJsonDocument(rootObject).toJson());
}
}
@ -3526,20 +3515,18 @@ void Application::domainChanged(const QString& domainHostname) {
// reset the voxels renderer
_voxels.killLocalVoxels();
// reset the auth URL for OAuth web view handler
OAuthWebViewHandler::getInstance().clearLastAuthorizationURL();
}
void Application::connectedToDomain(const QString& hostname) {
AccountManager& accountManager = AccountManager::getInstance();
const QUuid& domainID = NodeList::getInstance()->getDomainHandler().getUUID();
if (accountManager.isLoggedIn() && !domainID.isNull()) {
// update our data-server with the domain-server we're logged in with
if (accountManager.isLoggedIn()) {
// update our domain-server with the data-server we're logged in with
QString domainPutJsonString = "{\"location\":{\"domain_id\":\"" + uuidStringWithoutCurlyBraces(domainID) + "\"}}";
QString domainPutJsonString = "{\"address\":{\"domain\":\"" + hostname + "\"}}";
accountManager.authenticatedRequest("/api/v1/users/address", QNetworkAccessManager::PutOperation,
accountManager.authenticatedRequest("/api/v1/user/location", QNetworkAccessManager::PutOperation,
JSONCallbackParameters(), domainPutJsonString.toUtf8());
}
}

View file

@ -11,11 +11,11 @@
#include <QtCore/QWeakPointer>
#include <AccountManager.h>
#include <PerfStat.h>
#include "Application.h"
#include "Menu.h"
#include "ui/OAuthWebViewHandler.h"
#include "DatagramProcessor.h"
@ -136,16 +136,11 @@ void DatagramProcessor::processDatagrams() {
application->_bandwidthMeter.inputStream(BandwidthMeter::AVATARS).updateValue(incomingPacket.size());
break;
}
case PacketTypeDomainOAuthRequest: {
QDataStream readStream(incomingPacket);
readStream.skipRawData(numBytesForPacketHeader(incomingPacket));
QUrl authorizationURL;
readStream >> authorizationURL;
QMetaObject::invokeMethod(&OAuthWebViewHandler::getInstance(), "displayWebviewForAuthorizationURL",
Q_ARG(const QUrl&, authorizationURL));
case PacketTypeDomainConnectionDenied: {
// output to the log so the user knows they got a denied connection request
// and check and signal for an access token so that we can make sure they are logged in
qDebug() << "The domain-server denied a connection request.";
AccountManager::getInstance().checkAndSignalForAccessToken();
break;
}
case PacketTypeMuteEnvironment: {

View file

@ -364,7 +364,9 @@ void ModelUploader::send() {
_progressBar = NULL;
}
void ModelUploader::checkJSON(const QJsonObject& jsonResponse) {
void ModelUploader::checkJSON(QNetworkReply& requestReply) {
QJsonObject jsonResponse = QJsonDocument::fromJson(requestReply.readAll()).object();
if (jsonResponse.contains("status") && jsonResponse.value("status").toString() == "success") {
qDebug() << "status : success";
JSONCallbackParameters callbackParams;
@ -426,7 +428,7 @@ void ModelUploader::uploadUpdate(qint64 bytesSent, qint64 bytesTotal) {
}
}
void ModelUploader::uploadSuccess(const QJsonObject& jsonResponse) {
void ModelUploader::uploadSuccess(QNetworkReply& requestReply) {
if (_progressDialog) {
_progressDialog->accept();
}

View file

@ -40,9 +40,9 @@ public slots:
void send();
private slots:
void checkJSON(const QJsonObject& jsonResponse);
void checkJSON(QNetworkReply& requestReply);
void uploadUpdate(qint64 bytesSent, qint64 bytesTotal);
void uploadSuccess(const QJsonObject& jsonResponse);
void uploadSuccess(QNetworkReply& requestReply);
void uploadFailed(QNetworkReply& errorReply);
void checkS3();
void processCheck();

View file

@ -1,64 +0,0 @@
//
// PaymentManager.cpp
// interface/src
//
// Created by Stephen Birarda on 2014-07-30.
// 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 <QtCore/QDateTime>
#include <QtCore/QDebug>
#include <QtCore/QUuid>
#include <Menu.h>
#include <NodeList.h>
#include <PacketHeaders.h>
#include "SignedWalletTransaction.h"
#include "PaymentManager.h"
PaymentManager& PaymentManager::getInstance() {
static PaymentManager sharedInstance;
return sharedInstance;
}
void PaymentManager::sendSignedPayment(qint64 satoshiAmount, const QUuid& nodeUUID, const QUuid& destinationWalletUUID) {
QByteArray walletPrivateKeyByteArray = Menu::getInstance()->getWalletPrivateKey();
if (!walletPrivateKeyByteArray.isEmpty()) {
// setup a signed wallet transaction
const qint64 DEFAULT_TRANSACTION_EXPIRY_SECONDS = 60;
qint64 currentTimestamp = QDateTime::currentDateTimeUtc().toTime_t();
SignedWalletTransaction newTransaction(destinationWalletUUID, satoshiAmount,
currentTimestamp, DEFAULT_TRANSACTION_EXPIRY_SECONDS);
// send the signed transaction to the redeeming node
QByteArray transactionByteArray = byteArrayWithPopulatedHeader(PacketTypeSignedTransactionPayment);
// append the binary message and the signed message digest
transactionByteArray.append(newTransaction.binaryMessage());
QByteArray signedMessageDigest = newTransaction.signedMessageDigest();
if (!signedMessageDigest.isEmpty()) {
transactionByteArray.append(signedMessageDigest);
qDebug() << "Paying" << satoshiAmount << "satoshis to" << destinationWalletUUID << "via" << nodeUUID;
// use the NodeList to send that to the right node
NodeList* nodeList = NodeList::getInstance();
nodeList->writeDatagram(transactionByteArray, nodeList->nodeWithUUID(nodeUUID));
} else {
qDebug() << "Payment of" << satoshiAmount << "satoshis to" << destinationWalletUUID << "via" << nodeUUID <<
"cannot be sent because there was an error signing the transaction.";
}
} else {
qDebug() << "Payment of" << satoshiAmount << "satoshis to" << destinationWalletUUID << "via" << nodeUUID <<
"cannot be sent because there is no stored wallet private key.";
}
}

View file

@ -1,25 +0,0 @@
//
// PaymentManager.h
// interface/src
//
// Created by Stephen Birarda on 2014-07-30.
// 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
//
#ifndef hifi_PaymentManager_h
#define hifi_PaymentManager_h
#include <QtCore/QObject>
class PaymentManager : public QObject {
Q_OBJECT
public:
static PaymentManager& getInstance();
public slots:
void sendSignedPayment(qint64 satoshiAmount, const QUuid& nodeUUID, const QUuid& destinationWalletUUID);
};
#endif // hifi_PaymentManager_h

View file

@ -1,86 +0,0 @@
//
// SignedWalletTransaction.cpp
// interface/src
//
// Created by Stephen Birarda on 2014-07-11.
// 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 <QtCore/QCryptographicHash>
#include <QtCore/QDebug>
#include <QtCore/QFile>
#include <openssl/bio.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <AccountManager.h>
#include "Menu.h"
#include "SignedWalletTransaction.h"
SignedWalletTransaction::SignedWalletTransaction(const QUuid& destinationUUID, qint64 amount,
qint64 messageTimestamp, qint64 expiryDelta) :
WalletTransaction(destinationUUID, amount),
_messageTimestamp(messageTimestamp),
_expiryDelta(expiryDelta)
{
}
QByteArray SignedWalletTransaction::binaryMessage() {
// build the message using the components of this transaction
// UUID, source UUID, destination UUID, message timestamp, expiry delta, amount
QByteArray messageBinary;
messageBinary.append(_uuid.toRfc4122());
messageBinary.append(reinterpret_cast<const char*>(&_messageTimestamp), sizeof(_messageTimestamp));
messageBinary.append(reinterpret_cast<const char*>(&_expiryDelta), sizeof(_expiryDelta));
messageBinary.append(AccountManager::getInstance().getAccountInfo().getWalletID().toRfc4122());
messageBinary.append(_destinationUUID.toRfc4122());
messageBinary.append(reinterpret_cast<const char*>(&_amount), sizeof(_amount));
return messageBinary;
}
QByteArray SignedWalletTransaction::hexMessage() {
return binaryMessage().toHex();
}
QByteArray SignedWalletTransaction::messageDigest() {
return QCryptographicHash::hash(hexMessage(), QCryptographicHash::Sha256).toHex();
}
QByteArray SignedWalletTransaction::signedMessageDigest() {
// pull the current private key from menu into RSA structure in memory
QByteArray privateKeyByteArray = Menu::getInstance()->getWalletPrivateKey();
BIO* privateKeyBIO = NULL;
RSA* rsaPrivateKey = NULL;
privateKeyBIO = BIO_new_mem_buf(privateKeyByteArray.data(), privateKeyByteArray.size());
PEM_read_bio_RSAPrivateKey(privateKeyBIO, &rsaPrivateKey, NULL, NULL);
QByteArray digestToEncrypt = messageDigest();
QByteArray encryptedDigest(RSA_size(rsaPrivateKey), 0);
int encryptReturn = RSA_private_encrypt(digestToEncrypt.size(),
reinterpret_cast<const unsigned char*>(digestToEncrypt.constData()),
reinterpret_cast<unsigned char*>(encryptedDigest.data()),
rsaPrivateKey, RSA_PKCS1_PADDING);
// free the two structures used
BIO_free(privateKeyBIO);
RSA_free(rsaPrivateKey);
return (encryptReturn != -1) ? encryptedDigest : QByteArray();
}

View file

@ -1,32 +0,0 @@
//
// SignedWalletTransaction.h
// interfac/src
//
// Created by Stephen Birarda on 2014-07-11.
// 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
//
#ifndef hifi_SignedWalletTransaction_h
#define hifi_SignedWalletTransaction_h
#include <WalletTransaction.h>
class SignedWalletTransaction : public WalletTransaction {
Q_OBJECT
public:
SignedWalletTransaction(const QUuid& destinationUUID, qint64 amount, qint64 messageTimestamp, qint64 expiryDelta);
QByteArray binaryMessage();
QByteArray hexMessage();
QByteArray messageDigest();
QByteArray signedMessageDigest();
private:
qint64 _messageTimestamp;
qint64 _expiryDelta;
};
#endif // hifi_SignedWalletTransaction_h

View file

@ -20,10 +20,10 @@
const float MAX_AXIS = 32768.0f;
Joystick::Joystick(SDL_JoystickID instanceId, const QString& name, SDL_GameController* sdlGameController) :
_instanceId(instanceId),
_name(name),
_sdlGameController(sdlGameController),
_sdlJoystick(SDL_GameControllerGetJoystick(_sdlGameController)),
_instanceId(instanceId),
_name(name),
_axes(QVector<float>(SDL_JoystickNumAxes(_sdlJoystick))),
_buttons(QVector<bool>(SDL_JoystickNumButtons(_sdlJoystick)))
{

View file

@ -1,168 +0,0 @@
//
// OAuthWebViewHandler.cpp
// interface/src/ui
//
// Created by Stephen Birarda on 2014-05-01.
// 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 <qwebview.h>
#include <qurlquery.h>
#include <AccountManager.h>
#include "Application.h"
#include "OAuthWebViewHandler.h"
OAuthWebViewHandler& OAuthWebViewHandler::getInstance() {
static OAuthWebViewHandler sharedInstance;
return sharedInstance;
}
OAuthWebViewHandler::OAuthWebViewHandler() :
_activeWebView(NULL),
_webViewRedisplayTimer(),
_lastAuthorizationURL()
{
addHighFidelityRootCAToSSLConfig();
}
const char HIGH_FIDELITY_CA[] = "-----BEGIN CERTIFICATE-----\n"
"MIID6TCCA1KgAwIBAgIJANlfRkRD9A8bMA0GCSqGSIb3DQEBBQUAMIGqMQswCQYD\n"
"VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5j\n"
"aXNjbzEbMBkGA1UEChMSSGlnaCBGaWRlbGl0eSwgSW5jMRMwEQYDVQQLEwpPcGVy\n"
"YXRpb25zMRgwFgYDVQQDEw9oaWdoZmlkZWxpdHkuaW8xIjAgBgkqhkiG9w0BCQEW\n"
"E29wc0BoaWdoZmlkZWxpdHkuaW8wHhcNMTQwMzI4MjIzMzM1WhcNMjQwMzI1MjIz\n"
"MzM1WjCBqjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNV\n"
"BAcTDVNhbiBGcmFuY2lzY28xGzAZBgNVBAoTEkhpZ2ggRmlkZWxpdHksIEluYzET\n"
"MBEGA1UECxMKT3BlcmF0aW9uczEYMBYGA1UEAxMPaGlnaGZpZGVsaXR5LmlvMSIw\n"
"IAYJKoZIhvcNAQkBFhNvcHNAaGlnaGZpZGVsaXR5LmlvMIGfMA0GCSqGSIb3DQEB\n"
"AQUAA4GNADCBiQKBgQDyo1euYiPPEdnvDZnIjWrrP230qUKMSj8SWoIkbTJF2hE8\n"
"2eP3YOgbgSGBzZ8EJBxIOuNmj9g9Eg6691hIKFqy5W0BXO38P04Gg+pVBvpHFGBi\n"
"wpqGbfsjaUDuYmBeJRcMO0XYkLCRQG+lAQNHoFDdItWAJfC3FwtP3OCDnz8cNwID\n"
"AQABo4IBEzCCAQ8wHQYDVR0OBBYEFCSv2kmiGg6VFMnxXzLDNP304cPAMIHfBgNV\n"
"HSMEgdcwgdSAFCSv2kmiGg6VFMnxXzLDNP304cPAoYGwpIGtMIGqMQswCQYDVQQG\n"
"EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNj\n"
"bzEbMBkGA1UEChMSSGlnaCBGaWRlbGl0eSwgSW5jMRMwEQYDVQQLEwpPcGVyYXRp\n"
"b25zMRgwFgYDVQQDEw9oaWdoZmlkZWxpdHkuaW8xIjAgBgkqhkiG9w0BCQEWE29w\n"
"c0BoaWdoZmlkZWxpdHkuaW+CCQDZX0ZEQ/QPGzAMBgNVHRMEBTADAQH/MA0GCSqG\n"
"SIb3DQEBBQUAA4GBAEkQl3p+lH5vuoCNgyfa67nL0MsBEt+5RSBOgjwCjjASjzou\n"
"FTv5w0he2OypgMQb8i/BYtS1lJSFqjPJcSM1Salzrm3xDOK5pOXJ7h6SQLPDVEyf\n"
"Hy2/9d/to+99+SOUlvfzfgycgjOc+s/AV7Y+GBd7uzGxUdrN4egCZW1F6/mH\n"
"-----END CERTIFICATE-----\n";
void OAuthWebViewHandler::addHighFidelityRootCAToSSLConfig() {
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
// add the High Fidelity root CA to the list of trusted CA certificates
QByteArray highFidelityCACertificate(HIGH_FIDELITY_CA, sizeof(HIGH_FIDELITY_CA));
sslConfig.setCaCertificates(sslConfig.caCertificates() + QSslCertificate::fromData(highFidelityCACertificate));
// set the modified configuration
QSslConfiguration::setDefaultConfiguration(sslConfig);
}
const int WEB_VIEW_REDISPLAY_ELAPSED_MSECS = 5 * 1000;
void OAuthWebViewHandler::displayWebviewForAuthorizationURL(const QUrl& authorizationURL) {
if (!_activeWebView) {
if (!_lastAuthorizationURL.isEmpty()) {
if (_lastAuthorizationURL.host() == authorizationURL.host()
&& _webViewRedisplayTimer.elapsed() < WEB_VIEW_REDISPLAY_ELAPSED_MSECS) {
// this would be re-displaying an OAuth dialog for the same auth URL inside of the redisplay ms
// so return instead
return;
}
}
_lastAuthorizationURL = authorizationURL;
_activeWebView = new QWebView;
// keep the window on top and delete it when it closes
_activeWebView->setWindowFlags(Qt::Sheet);
_activeWebView->setAttribute(Qt::WA_DeleteOnClose);
qDebug() << "Displaying QWebView for OAuth authorization at" << authorizationURL.toString();
AccountManager& accountManager = AccountManager::getInstance();
QUrl codedAuthorizationURL = authorizationURL;
// check if we have an access token for this host - if so we can bypass login by adding it to the URL
if (accountManager.getAuthURL().host() == authorizationURL.host()
&& accountManager.hasValidAccessToken()) {
const QString ACCESS_TOKEN_QUERY_STRING_KEY = "access_token";
QUrlQuery authQuery(codedAuthorizationURL);
authQuery.addQueryItem(ACCESS_TOKEN_QUERY_STRING_KEY, accountManager.getAccountInfo().getAccessToken().token);
codedAuthorizationURL.setQuery(authQuery);
}
connect(_activeWebView.data(), &QWebView::urlChanged, this, &OAuthWebViewHandler::handleURLChanged);
_activeWebView->load(codedAuthorizationURL);
connect(_activeWebView->page()->networkAccessManager(), &QNetworkAccessManager::sslErrors,
this, &OAuthWebViewHandler::handleSSLErrors);
connect(_activeWebView->page()->networkAccessManager(), &QNetworkAccessManager::finished,
this, &OAuthWebViewHandler::handleReplyFinished);
connect(_activeWebView.data(), &QWebView::loadFinished, this, &OAuthWebViewHandler::handleLoadFinished);
// connect to the destroyed signal so after the web view closes we can start a timer
connect(_activeWebView.data(), &QWebView::destroyed, this, &OAuthWebViewHandler::handleWebViewDestroyed);
}
}
void OAuthWebViewHandler::handleSSLErrors(QNetworkReply* networkReply, const QList<QSslError>& errorList) {
qDebug() << "SSL Errors:" << errorList;
}
void OAuthWebViewHandler::handleLoadFinished(bool success) {
if (success && _activeWebView->url().host() == NodeList::getInstance()->getDomainHandler().getHostname()) {
qDebug() << "OAuth authorization code passed successfully to domain-server.";
// grab the UUID that is set as the state parameter in the auth URL
// since that is our new session UUID
QUrlQuery authQuery(_activeWebView->url());
const QString AUTH_STATE_QUERY_KEY = "state";
NodeList::getInstance()->setSessionUUID(QUuid(authQuery.queryItemValue(AUTH_STATE_QUERY_KEY)));
_activeWebView->close();
_activeWebView = NULL;
}
}
void OAuthWebViewHandler::handleReplyFinished(QNetworkReply* reply) {
if (_activeWebView && reply->error() != QNetworkReply::NoError) {
qDebug() << "Error loading" << reply->url() << "-" << reply->errorString();
_activeWebView->close();
}
}
void OAuthWebViewHandler::handleWebViewDestroyed(QObject* destroyedObject) {
_webViewRedisplayTimer.restart();
}
void OAuthWebViewHandler::handleURLChanged(const QUrl& url) {
// check if this is the authorization screen - if it is then we need to show the OAuthWebViewHandler
const QString ACCESS_TOKEN_URL_REGEX_STRING = "redirect_uri=[\\w:\\/\\.]+&access_token=";
QRegExp accessTokenRegex(ACCESS_TOKEN_URL_REGEX_STRING);
if (accessTokenRegex.indexIn(url.toString()) != -1) {
_activeWebView->show();
} else if (url.toString() == DEFAULT_NODE_AUTH_URL.toString() + "/login") {
// this is a login request - we're going to close the webview and signal the AccountManager that we need a login
qDebug() << "data-server replied with login request. Signalling that login is required to proceed with OAuth.";
_activeWebView->close();
AccountManager::getInstance().checkAndSignalForAccessToken();
}
}

View file

@ -1,43 +0,0 @@
//
// OAuthWebviewHandler.h
// interface/src/ui
//
// Created by Stephen Birarda on 2014-05-01.
// 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
//
#ifndef hifi_OAuthWebviewHandler_h
#define hifi_OAuthWebviewHandler_h
#include <QtCore/QUrl>
class QWebView;
class OAuthWebViewHandler : public QObject {
Q_OBJECT
public:
OAuthWebViewHandler();
static OAuthWebViewHandler& getInstance();
static void addHighFidelityRootCAToSSLConfig();
void clearLastAuthorizationURL() { _lastAuthorizationURL = QUrl(); }
public slots:
void displayWebviewForAuthorizationURL(const QUrl& authorizationURL);
private slots:
void handleSSLErrors(QNetworkReply* networkReply, const QList<QSslError>& errorList);
void handleLoadFinished(bool success);
void handleReplyFinished(QNetworkReply* reply);
void handleWebViewDestroyed(QObject* destroyedObject);
void handleURLChanged(const QUrl& url);
private:
QPointer<QWebView> _activeWebView;
QElapsedTimer _webViewRedisplayTimer;
QUrl _lastAuthorizationURL;
};
#endif // hifi_OAuthWebviewHandler_h

View file

@ -10,5 +10,19 @@ if (WIN32)
target_link_libraries(${TARGET_NAME} ws2_32.lib)
endif ()
# find OpenSSL
find_package(OpenSSL REQUIRED)
if (APPLE AND ${OPENSSL_INCLUDE_DIR} STREQUAL "/usr/include")
# this is a user on OS X using system OpenSSL, which is going to throw warnings since they're deprecating for their common crypto
message(WARNING "The found version of OpenSSL is the OS X system version. This will produce deprecation warnings."
"\nWe recommend you install a newer version (at least 1.0.1h) in a different directory and set OPENSSL_ROOT_DIR in your env so Cmake can find it.")
endif ()
include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}")
# append OpenSSL to our list of libraries to link
list(APPEND ${TARGET_NAME}_LIBRARIES_TO_LINK "${OPENSSL_LIBRARIES}")
# call macro to link our dependencies and bubble them up via a property on our target
link_shared_dependencies()

View file

@ -19,9 +19,11 @@
#include <QtCore/QUrlQuery>
#include <QtNetwork/QHttpMultiPart>
#include <QtNetwork/QNetworkRequest>
#include <qthread.h>
#include "NodeList.h"
#include "PacketHeaders.h"
#include "RSAKeypairGenerator.h"
#include "AccountManager.h"
@ -144,6 +146,12 @@ void AccountManager::setAuthURL(const QUrl& authURL) {
} else {
requestProfile();
}
// if we don't have a private key in settings we should generate a new keypair
if (!_accountInfo.hasPrivateKey()) {
qDebug() << "No private key present - generating a new key-pair.";
generateNewKeypair();
}
}
}
}
@ -280,14 +288,12 @@ void AccountManager::processReply() {
}
void AccountManager::passSuccessToCallback(QNetworkReply* requestReply) {
QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll());
JSONCallbackParameters callbackParams = _pendingCallbackMap.value(requestReply);
if (callbackParams.jsonCallbackReceiver) {
// invoke the right method on the callback receiver
QMetaObject::invokeMethod(callbackParams.jsonCallbackReceiver, qPrintable(callbackParams.jsonCallbackMethod),
Q_ARG(const QJsonObject&, jsonResponse.object()));
Q_ARG(QNetworkReply&, *requestReply));
// remove the related reply-callback group from the map
_pendingCallbackMap.remove(requestReply);
@ -295,7 +301,7 @@ void AccountManager::passSuccessToCallback(QNetworkReply* requestReply) {
} else {
if (VERBOSE_HTTP_REQUEST_DEBUGGING) {
qDebug() << "Received JSON response from data-server that has no matching callback.";
qDebug() << jsonResponse;
qDebug() << QJsonDocument::fromJson(requestReply->readAll());
}
}
}
@ -319,6 +325,16 @@ void AccountManager::passErrorToCallback(QNetworkReply* requestReply) {
}
}
void AccountManager::persistAccountToSettings() {
if (_shouldPersistToSettingsFile) {
// store this access token into the local settings
QSettings localSettings;
localSettings.beginGroup(ACCOUNTS_GROUP);
localSettings.setValue(_authURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE),
QVariant::fromValue(_accountInfo));
}
}
bool AccountManager::hasValidAccessToken() {
if (_accountInfo.getAccessToken().token.isEmpty() || _accountInfo.getAccessToken().isExpired()) {
@ -408,15 +424,10 @@ void AccountManager::requestAccessTokenFinished() {
emit loginComplete(rootURL);
if (_shouldPersistToSettingsFile) {
// store this access token into the local settings
QSettings localSettings;
localSettings.beginGroup(ACCOUNTS_GROUP);
localSettings.setValue(rootURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE),
QVariant::fromValue(_accountInfo));
}
persistAccountToSettings();
requestProfile();
generateNewKeypair();
}
} else {
// TODO: error handling
@ -434,7 +445,7 @@ void AccountManager::requestProfile() {
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QUrl profileURL = _authURL;
profileURL.setPath("/api/v1/users/profile");
profileURL.setPath("/api/v1/user/profile");
QNetworkRequest profileRequest(profileURL);
profileRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER, _accountInfo.getAccessToken().authorizationHeaderValue());
@ -458,15 +469,8 @@ void AccountManager::requestProfileFinished() {
// the username has changed to whatever came back
emit usernameChanged(_accountInfo.getUsername());
if (_shouldPersistToSettingsFile) {
// store the whole profile into the local settings
QUrl rootURL = profileReply->url();
rootURL.setPath("");
QSettings localSettings;
localSettings.beginGroup(ACCOUNTS_GROUP);
localSettings.setValue(rootURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE),
QVariant::fromValue(_accountInfo));
}
// store the whole profile into the local settings
persistAccountToSettings();
} else {
// TODO: error handling
@ -478,3 +482,57 @@ void AccountManager::requestProfileError(QNetworkReply::NetworkError error) {
// TODO: error handling
qDebug() << "AccountManager requestProfileError - " << error;
}
void AccountManager::generateNewKeypair() {
// setup a new QThread to generate the keypair on, in case it takes a while
QThread* generateThread = new QThread(this);
// setup a keypair generator
RSAKeypairGenerator* keypairGenerator = new RSAKeypairGenerator();
connect(generateThread, &QThread::started, keypairGenerator, &RSAKeypairGenerator::generateKeypair);
connect(keypairGenerator, &RSAKeypairGenerator::generatedKeypair, this, &AccountManager::processGeneratedKeypair);
connect(keypairGenerator, &RSAKeypairGenerator::errorGeneratingKeypair,
this, &AccountManager::handleKeypairGenerationError);
connect(keypairGenerator, &QObject::destroyed, generateThread, &QThread::quit);
connect(generateThread, &QThread::finished, generateThread, &QThread::deleteLater);
keypairGenerator->moveToThread(generateThread);
qDebug() << "Starting worker thread to generate 2048-bit RSA key-pair.";
generateThread->start();
}
void AccountManager::processGeneratedKeypair(const QByteArray& publicKey, const QByteArray& privateKey) {
qDebug() << "Generated 2048-bit RSA key-pair. Storing private key and uploading public key.";
// set the private key on our data-server account info
_accountInfo.setPrivateKey(privateKey);
persistAccountToSettings();
// upload the public key so data-web has an up-to-date key
const QString PUBLIC_KEY_UPDATE_PATH = "api/v1/user/public_key";
// setup a multipart upload to send up the public key
QHttpMultiPart* requestMultiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart keyPart;
keyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream"));
keyPart.setHeader(QNetworkRequest::ContentDispositionHeader,
QVariant("form-data; name=\"public_key\"; filename=\"public_key\""));
keyPart.setBody(publicKey);
requestMultiPart->append(keyPart);
authenticatedRequest(PUBLIC_KEY_UPDATE_PATH, QNetworkAccessManager::PutOperation,
JSONCallbackParameters(), QByteArray(), requestMultiPart);
// get rid of the keypair generator now that we don't need it anymore
sender()->deleteLater();
}
void AccountManager::handleKeypairGenerationError() {
// for now there isn't anything we do with this except get the worker thread to clean up
sender()->deleteLater();
}

View file

@ -70,7 +70,7 @@ public:
void requestAccessToken(const QString& login, const QString& password);
void requestProfile();
const DataServerAccountInfo& getAccountInfo() const { return _accountInfo; }
DataServerAccountInfo& getAccountInfo() { return _accountInfo; }
public slots:
void requestAccessTokenFinished();
@ -91,13 +91,19 @@ signals:
void balanceChanged(qint64 newBalance);
private slots:
void processReply();
void handleKeypairGenerationError();
void processGeneratedKeypair(const QByteArray& publicKey, const QByteArray& privateKey);
private:
AccountManager();
AccountManager(AccountManager const& other); // not implemented
void operator=(AccountManager const& other); // not implemented
void persistAccountToSettings();
void passSuccessToCallback(QNetworkReply* reply);
void passErrorToCallback(QNetworkReply* reply);
void generateNewKeypair();
Q_INVOKABLE void invokedRequest(const QString& path,
bool requiresAuthentication,

View file

@ -10,6 +10,7 @@
//
#include <qdebug.h>
#include <qjsondocument.h>
#include <qregexp.h>
#include <qstringlist.h>
@ -134,8 +135,9 @@ void AddressManager::handleLookupString(const QString& lookupString) {
}
}
void AddressManager::handleAPIResponse(const QJsonObject &jsonObject) {
QJsonObject dataObject = jsonObject["data"].toObject();
void AddressManager::handleAPIResponse(QNetworkReply& requestReply) {
QJsonObject responseObject = QJsonDocument::fromJson(requestReply.readAll()).object();
QJsonObject dataObject = responseObject["data"].toObject();
const QString ADDRESS_API_DOMAIN_KEY = "domain";
const QString ADDRESS_API_ONLINE_KEY = "online";
@ -190,7 +192,7 @@ void AddressManager::handleAPIResponse(const QJsonObject &jsonObject) {
} else {
qDebug() << "Received an address manager API response with no domain key. Cannot parse.";
qDebug() << jsonObject;
qDebug() << responseObject;
}
} else {
// we've been told that this result exists but is offline, emit our signal so the application can handle

View file

@ -42,7 +42,7 @@ public:
public slots:
void handleLookupString(const QString& lookupString);
void handleAPIResponse(const QJsonObject& jsonObject);
void handleAPIResponse(QNetworkReply& requestReply);
void handleAPIError(QNetworkReply& errorReply);
void goToUser(const QString& username);
signals:

View file

@ -9,6 +9,9 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <openssl/rsa.h>
#include <qjsondocument.h>
#include <QtCore/QDebug>
#include "DataServerAccountInfo.h"
@ -20,7 +23,9 @@ DataServerAccountInfo::DataServerAccountInfo() :
_discourseApiKey(),
_walletID(),
_balance(0),
_hasBalance(false)
_hasBalance(false),
_privateKey(),
_usernameSignature()
{
}
@ -33,6 +38,7 @@ DataServerAccountInfo::DataServerAccountInfo(const DataServerAccountInfo& otherI
_walletID = otherInfo._walletID;
_balance = otherInfo._balance;
_hasBalance = otherInfo._hasBalance;
_privateKey = otherInfo._privateKey;
}
DataServerAccountInfo& DataServerAccountInfo::operator=(const DataServerAccountInfo& otherInfo) {
@ -51,6 +57,7 @@ void DataServerAccountInfo::swap(DataServerAccountInfo& otherInfo) {
swap(_walletID, otherInfo._walletID);
swap(_balance, otherInfo._balance);
swap(_hasBalance, otherInfo._hasBalance);
swap(_privateKey, otherInfo._privateKey);
}
void DataServerAccountInfo::setAccessTokenFromJSON(const QJsonObject& jsonObject) {
@ -61,6 +68,9 @@ void DataServerAccountInfo::setUsername(const QString& username) {
if (_username != username) {
_username = username;
// clear our username signature so it has to be re-created
_usernameSignature = QByteArray();
qDebug() << "Username changed to" << username;
}
}
@ -92,7 +102,8 @@ void DataServerAccountInfo::setBalance(qint64 balance) {
}
}
void DataServerAccountInfo::setBalanceFromJSON(const QJsonObject& jsonObject) {
void DataServerAccountInfo::setBalanceFromJSON(QNetworkReply& requestReply) {
QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object();
if (jsonObject["status"].toString() == "success") {
qint64 balanceInSatoshis = jsonObject["data"].toObject()["wallet"].toObject()["balance"].toDouble();
setBalance(balanceInSatoshis);
@ -111,12 +122,58 @@ void DataServerAccountInfo::setProfileInfoFromJSON(const QJsonObject& jsonObject
setWalletID(QUuid(user["wallet_id"].toString()));
}
const QByteArray& DataServerAccountInfo::getUsernameSignature() {
if (_usernameSignature.isEmpty()) {
if (!_privateKey.isEmpty()) {
const char* privateKeyData = _privateKey.constData();
RSA* rsaPrivateKey = d2i_RSAPrivateKey(NULL,
reinterpret_cast<const unsigned char**>(&privateKeyData),
_privateKey.size());
if (rsaPrivateKey) {
QByteArray usernameByteArray = _username.toUtf8();
_usernameSignature.resize(RSA_size(rsaPrivateKey));
int encryptReturn = RSA_private_encrypt(usernameByteArray.size(),
reinterpret_cast<const unsigned char*>(usernameByteArray.constData()),
reinterpret_cast<unsigned char*>(_usernameSignature.data()),
rsaPrivateKey, RSA_PKCS1_PADDING);
if (encryptReturn == -1) {
qDebug() << "Error encrypting username signature.";
qDebug() << "Will re-attempt on next domain-server check in.";
_usernameSignature = QByteArray();
}
// free the private key RSA struct now that we are done with it
RSA_free(rsaPrivateKey);
} else {
qDebug() << "Could not create RSA struct from QByteArray private key.";
qDebug() << "Will re-attempt on next domain-server check in.";
}
} else {
qDebug() << "No private key present in DataServerAccountInfo. Re-log to generate new key.";
qDebug() << "Returning empty username signature.";
}
}
return _usernameSignature;
}
void DataServerAccountInfo::setPrivateKey(const QByteArray& privateKey) {
_privateKey = privateKey;
// clear our username signature so it has to be re-created
_usernameSignature = QByteArray();
}
QDataStream& operator<<(QDataStream &out, const DataServerAccountInfo& info) {
out << info._accessToken << info._username << info._xmppPassword << info._discourseApiKey << info._walletID;
out << info._accessToken << info._username << info._xmppPassword << info._discourseApiKey
<< info._walletID << info._privateKey;
return out;
}
QDataStream& operator>>(QDataStream &in, DataServerAccountInfo& info) {
in >> info._accessToken >> info._username >> info._xmppPassword >> info._discourseApiKey >> info._walletID;
in >> info._accessToken >> info._username >> info._xmppPassword >> info._discourseApiKey
>> info._walletID >> info._privateKey;
return in;
}

View file

@ -13,6 +13,7 @@
#define hifi_DataServerAccountInfo_h
#include <QtCore/QObject>
#include <qnetworkreply.h>
#include <QtCore/QUuid>
#include "OAuthAccessToken.h"
@ -41,13 +42,17 @@ public:
const QUuid& getWalletID() const { return _walletID; }
void setWalletID(const QUuid& walletID);
const QByteArray& getUsernameSignature();
bool hasPrivateKey() const { return !_privateKey.isEmpty(); }
void setPrivateKey(const QByteArray& privateKey);
qint64 getBalance() const { return _balance; }
float getBalanceInSatoshis() const { return _balance / SATOSHIS_PER_CREDIT; }
void setBalance(qint64 balance);
bool hasBalance() const { return _hasBalance; }
void setHasBalance(bool hasBalance) { _hasBalance = hasBalance; }
Q_INVOKABLE void setBalanceFromJSON(const QJsonObject& jsonObject);
Q_INVOKABLE void setBalanceFromJSON(QNetworkReply& requestReply);
bool hasProfile() const;
@ -67,6 +72,8 @@ private:
QUuid _walletID;
qint64 _balance;
bool _hasBalance;
QByteArray _privateKey;
QByteArray _usernameSignature;
};
#endif // hifi_DataServerAccountInfo_h

View file

@ -679,7 +679,6 @@ void LimitedNodeList::updateLocalSockAddr() {
qDebug() << "Local socket has changed from" << _localSockAddr << "to" << newSockAddr;
}
_localSockAddr = newSockAddr;
emit localSockAddrChanged(_localSockAddr);

View file

@ -101,6 +101,8 @@ public:
const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket);
SharedNodePointer updateSocketsForNode(const QUuid& uuid,
const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket);
const HifiSockAddr& getLocalSockAddr() const { return _localSockAddr; }
void processNodeData(const HifiSockAddr& senderSockAddr, const QByteArray& packet);
void processKillNode(const QByteArray& datagram);

View file

@ -302,11 +302,22 @@ void NodeList::sendDomainServerCheckIn() {
QDataStream packetStream(&domainServerPacket, QIODevice::Append);
// pack our data to send to the domain-server
packetStream << _ownerType << _publicSockAddr << _localSockAddr << (quint8) _nodeTypesOfInterest.size();
packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList();
// copy over the bytes for node types of interest, if required
foreach (NodeType_t nodeTypeOfInterest, _nodeTypesOfInterest) {
packetStream << nodeTypeOfInterest;
// 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()) {
qDebug() << "Including username signature in domain connect request.";
packetStream << usernameSignature;
} else {
qDebug() << "Private key not present - domain connect request cannot include username signature";
}
}
if (!isUsingDTLS) {

View file

@ -114,7 +114,7 @@ QString nameForPacketType(PacketType type) {
PACKET_TYPE_NAME_LOOKUP(PacketTypeDomainListRequest);
PACKET_TYPE_NAME_LOOKUP(PacketTypeRequestAssignment);
PACKET_TYPE_NAME_LOOKUP(PacketTypeCreateAssignment);
PACKET_TYPE_NAME_LOOKUP(PacketTypeDomainOAuthRequest);
PACKET_TYPE_NAME_LOOKUP(PacketTypeDomainConnectionDenied);
PACKET_TYPE_NAME_LOOKUP(PacketTypeMuteEnvironment);
PACKET_TYPE_NAME_LOOKUP(PacketTypeAudioStreamStats);
PACKET_TYPE_NAME_LOOKUP(PacketTypeDataServerConfirm);

View file

@ -38,7 +38,7 @@ enum PacketType {
PacketTypeDomainListRequest,
PacketTypeRequestAssignment,
PacketTypeCreateAssignment,
PacketTypeDomainOAuthRequest,
PacketTypeDomainConnectionDenied,
PacketTypeMuteEnvironment,
PacketTypeAudioStreamStats,
PacketTypeDataServerConfirm,
@ -81,7 +81,7 @@ typedef char PacketVersion;
const QSet<PacketType> NON_VERIFIED_PACKETS = QSet<PacketType>()
<< PacketTypeDomainServerRequireDTLS << PacketTypeDomainConnectRequest
<< PacketTypeDomainList << PacketTypeDomainListRequest << PacketTypeDomainOAuthRequest
<< PacketTypeDomainList << PacketTypeDomainListRequest << PacketTypeDomainConnectionDenied
<< PacketTypeCreateAssignment << PacketTypeRequestAssignment << PacketTypeStunResponse
<< PacketTypeNodeJsonStats << PacketTypeVoxelQuery << PacketTypeParticleQuery << PacketTypeEntityQuery
<< PacketTypeOctreeDataNack << PacketTypeVoxelEditNack << PacketTypeParticleEditNack << PacketTypeEntityEditNack

View file

@ -0,0 +1,91 @@
//
// RSAKeypairGenerator.cpp
// libraries/networking/src
//
// Created by Stephen Birarda on 2014-10-14.
// 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 <openssl/err.h>
#include <openssl/rsa.h>
#include <openssl/x509.h>
#include <qdebug.h>
#include "RSAKeypairGenerator.h"
RSAKeypairGenerator::RSAKeypairGenerator(QObject* parent) :
QObject(parent)
{
}
void RSAKeypairGenerator::generateKeypair() {
RSA* keyPair = RSA_new();
BIGNUM* exponent = BN_new();
const unsigned long RSA_KEY_EXPONENT = 65537;
BN_set_word(exponent, RSA_KEY_EXPONENT);
// seed the random number generator before we call RSA_generate_key_ex
srand(time(NULL));
const int RSA_KEY_BITS = 2048;
if (!RSA_generate_key_ex(keyPair, RSA_KEY_BITS, exponent, NULL)) {
qDebug() << "Error generating 2048-bit RSA Keypair -" << ERR_get_error();
emit errorGeneratingKeypair();
// we're going to bust out of here but first we cleanup the BIGNUM
BN_free(exponent);
return;
}
// we don't need the BIGNUM anymore so clean that up
BN_free(exponent);
// grab the public key and private key from the file
unsigned char* publicKeyDER = NULL;
int publicKeyLength = i2d_RSAPublicKey(keyPair, &publicKeyDER);
unsigned char* privateKeyDER = NULL;
int privateKeyLength = i2d_RSAPrivateKey(keyPair, &privateKeyDER);
if (publicKeyLength <= 0 || privateKeyLength <= 0) {
qDebug() << "Error getting DER public or private key from RSA struct -" << ERR_get_error();
emit errorGeneratingKeypair();
// cleanup the RSA struct
RSA_free(keyPair);
// cleanup the public and private key DER data, if required
if (publicKeyLength > 0) {
delete publicKeyDER;
}
if (privateKeyLength > 0) {
delete privateKeyDER;
}
return;
}
// we have the public key and private key in memory
// we can cleanup the RSA struct before we continue on
RSA_free(keyPair);
QByteArray publicKeyArray(reinterpret_cast<char*>(publicKeyDER), publicKeyLength);
QByteArray privateKeyArray(reinterpret_cast<char*>(privateKeyDER), privateKeyLength);
// cleanup the publicKeyDER and publicKeyDER data
delete publicKeyDER;
delete privateKeyDER;
emit generatedKeypair(publicKeyArray, privateKeyArray);
}

View file

@ -0,0 +1,28 @@
//
// RSAKeypairGenerator.h
// libraries/networking/src
//
// Created by Stephen Birarda on 2014-10-14.
// 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
//
#ifndef hifi_RSAKeypairGenerator_h
#define hifi_RSAKeypairGenerator_h
#include <qobject.h>
class RSAKeypairGenerator : public QObject {
Q_OBJECT
public:
RSAKeypairGenerator(QObject* parent = 0);
public slots:
void generateKeypair();
signals:
void errorGeneratingKeypair();
void generatedKeypair(const QByteArray& publicKey, const QByteArray& privateKey);
};
#endif // hifi_RSAKeypairGenerator_h

View file

@ -69,7 +69,7 @@ void UserActivityLogger::logAction(QString action, QJsonObject details, JSONCall
multipart);
}
void UserActivityLogger::requestFinished(const QJsonObject& object) {
void UserActivityLogger::requestFinished(QNetworkReply& requestReply) {
// qDebug() << object;
}

View file

@ -39,7 +39,7 @@ public slots:
void wentTo(QString destinationType, QString destinationName);
private slots:
void requestFinished(const QJsonObject& object);
void requestFinished(QNetworkReply& requestReply);
void requestError(QNetworkReply& errorReply);
private: