// // IceServer.cpp // ice-server/src // // Created by Stephen Birarda on 2014-10-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 "IceServer.h" #include #include #include #include #include #include #include #include #include #include const int CLEAR_INACTIVE_PEERS_INTERVAL_MSECS = 1 * 1000; const int PEER_SILENCE_THRESHOLD_MSECS = 5 * 1000; IceServer::IceServer(int argc, char* argv[]) : QCoreApplication(argc, argv), _id(QUuid::createUuid()), _serverSocket(0, false), _activePeers() { // start the ice-server socket qDebug() << "ice-server socket is listening on" << ICE_SERVER_DEFAULT_PORT; _serverSocket.bind(QHostAddress::AnyIPv4, ICE_SERVER_DEFAULT_PORT); // set processPacket as the verified packet callback for the udt::Socket _serverSocket.setPacketHandler([this](std::unique_ptr packet) { processPacket(std::move(packet)); }); // set packetVersionMatch as the verify packet operator for the udt::Socket using std::placeholders::_1; _serverSocket.setPacketFilterOperator(std::bind(&IceServer::packetVersionMatch, this, _1)); // setup our timer to clear inactive peers QTimer* inactivePeerTimer = new QTimer(this); connect(inactivePeerTimer, &QTimer::timeout, this, &IceServer::clearInactivePeers); inactivePeerTimer->start(CLEAR_INACTIVE_PEERS_INTERVAL_MSECS); // handle public keys when they arrive from the QNetworkAccessManager auto& networkAccessManager = NetworkAccessManager::getInstance(); connect(&networkAccessManager, &QNetworkAccessManager::finished, this, &IceServer::publicKeyReplyFinished); } bool IceServer::packetVersionMatch(const udt::Packet& packet) { PacketType headerType = NLPacket::typeInHeader(packet); PacketVersion headerVersion = NLPacket::versionInHeader(packet); if (headerVersion == versionForPacketType(headerType)) { return true; } else { return false; } } void IceServer::processPacket(std::unique_ptr packet) { auto nlPacket = NLPacket::fromBase(std::move(packet)); // make sure that this packet at least looks like something we can read if (nlPacket->getPayloadSize() >= NLPacket::localHeaderSize(PacketType::ICEServerHeartbeat)) { if (nlPacket->getType() == PacketType::ICEServerHeartbeat) { SharedNetworkPeer peer = addOrUpdateHeartbeatingPeer(*nlPacket); if (peer) { // so that we can send packets to the heartbeating peer when we need, we need to activate a socket now peer->activateMatchingOrNewSymmetricSocket(nlPacket->getSenderSockAddr()); // we have an active and verified heartbeating peer // send them an ACK packet so they know that they are being heard and ready for ICE static auto ackPacket = NLPacket::create(PacketType::ICEServerHeartbeatACK); _serverSocket.writePacket(*ackPacket, nlPacket->getSenderSockAddr()); } else { // we couldn't verify this peer - respond back to them so they know they may need to perform keypair re-generation static auto deniedPacket = NLPacket::create(PacketType::ICEServerHeartbeatDenied); _serverSocket.writePacket(*deniedPacket, nlPacket->getSenderSockAddr()); } } else if (nlPacket->getType() == PacketType::ICEServerQuery) { QDataStream heartbeatStream(nlPacket.get()); // this is a node hoping to connect to a heartbeating peer - do we have the heartbeating peer? QUuid senderUUID; heartbeatStream >> senderUUID; // pull the public and private sock addrs for this peer HifiSockAddr publicSocket, localSocket; heartbeatStream >> publicSocket >> localSocket; // check if this node also included a UUID that they would like to connect to QUuid connectRequestID; heartbeatStream >> connectRequestID; SharedNetworkPeer matchingPeer = _activePeers.value(connectRequestID); if (matchingPeer) { qDebug() << "Sending information for peer" << connectRequestID << "to peer" << senderUUID; // we have the peer they want to connect to - send them pack the information for that peer sendPeerInformationPacket(*matchingPeer, &nlPacket->getSenderSockAddr()); // we also need to send them to the active peer they are hoping to connect to // create a dummy peer object we can pass to sendPeerInformationPacket NetworkPeer dummyPeer(senderUUID, publicSocket, localSocket); sendPeerInformationPacket(dummyPeer, matchingPeer->getActiveSocket()); } else { qDebug() << "Peer" << senderUUID << "asked for" << connectRequestID << "but no matching peer found"; } } } } SharedNetworkPeer IceServer::addOrUpdateHeartbeatingPeer(NLPacket& packet) { // pull the UUID, public and private sock addrs for this peer QUuid senderUUID; HifiSockAddr publicSocket, localSocket; QByteArray signature; QDataStream heartbeatStream(&packet); heartbeatStream >> senderUUID >> publicSocket >> localSocket; auto signedPlaintext = QByteArray::fromRawData(packet.getPayload(), heartbeatStream.device()->pos()); heartbeatStream >> signature; // make sure this is a verified heartbeat before performing any more processing if (isVerifiedHeartbeat(senderUUID, signedPlaintext, signature)) { // make sure we have this sender in our peer hash SharedNetworkPeer matchingPeer = _activePeers.value(senderUUID); if (!matchingPeer) { // if we don't have this sender we need to create them now matchingPeer = QSharedPointer::create(senderUUID, publicSocket, localSocket); _activePeers.insert(senderUUID, matchingPeer); qDebug() << "Added a new network peer" << *matchingPeer; } else { // we already had the peer so just potentially update their sockets matchingPeer->setPublicSocket(publicSocket); matchingPeer->setLocalSocket(localSocket); } // update our last heard microstamp for this network peer to now matchingPeer->setLastHeardMicrostamp(usecTimestampNow()); return matchingPeer; } else { // not verified, return the empty peer object return SharedNetworkPeer(); } } bool IceServer::isVerifiedHeartbeat(const QUuid& domainID, const QByteArray& plaintext, const QByteArray& signature) { // make sure we're not already waiting for a public key for this domain-server if (!_pendingPublicKeyRequests.contains(domainID)) { // check if we have a public key for this domain ID - if we do not then fire off the request for it auto it = _domainPublicKeys.find(domainID); if (it != _domainPublicKeys.end()) { // attempt to verify the signature for this heartbeat const auto rsaPublicKey = it->second.get(); if (rsaPublicKey) { auto hashedPlaintext = QCryptographicHash::hash(plaintext, QCryptographicHash::Sha256); int verificationResult = RSA_verify(NID_sha256, reinterpret_cast(hashedPlaintext.constData()), hashedPlaintext.size(), reinterpret_cast(signature.constData()), signature.size(), rsaPublicKey); if (verificationResult == 1) { // this is the only success case - we return true here to indicate that the heartbeat is verified return true; } else { qDebug() << "Failed to verify heartbeat for" << domainID << "- re-requesting public key from API."; } } else { // we can't let this user in since we couldn't convert their public key to an RSA key we could use qWarning() << "Public key for" << domainID << "is not a usable RSA* public key."; qWarning() << "Re-requesting public key from API"; } } // we could not verify this heartbeat (missing public key, could not load public key, bad actor) // ask the metaverse API for the right public key and return false to indicate that this is not verified requestDomainPublicKey(domainID); } return false; } void IceServer::requestDomainPublicKey(const QUuid& domainID) { // send a request to the metaverse API for the public key for this domain auto& networkAccessManager = NetworkAccessManager::getInstance(); QUrl publicKeyURL { NetworkingConstants::METAVERSE_SERVER_URL() }; QString publicKeyPath = QString("/api/v1/domains/%1/public_key").arg(uuidStringWithoutCurlyBraces(domainID)); publicKeyURL.setPath(publicKeyPath); QNetworkRequest publicKeyRequest { publicKeyURL }; publicKeyRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); publicKeyRequest.setAttribute(QNetworkRequest::User, domainID); qDebug() << "Requesting public key for domain with ID" << domainID; // add this to the set of pending public key requests _pendingPublicKeyRequests.insert(domainID); networkAccessManager.get(publicKeyRequest); } void IceServer::publicKeyReplyFinished(QNetworkReply* reply) { // get the domain ID from the QNetworkReply attribute QUuid domainID = reply->request().attribute(QNetworkRequest::User).toUuid(); if (reply->error() == QNetworkReply::NoError) { // pull out the public key and store it for this domain // the response should be JSON QJsonDocument responseDocument = QJsonDocument::fromJson(reply->readAll()); static const QString DATA_KEY = "data"; static const QString PUBLIC_KEY_KEY = "public_key"; static const QString STATUS_KEY = "status"; static const QString SUCCESS_VALUE = "success"; auto responseObject = responseDocument.object(); if (responseObject[STATUS_KEY].toString() == SUCCESS_VALUE) { auto dataObject = responseObject[DATA_KEY].toObject(); if (dataObject.contains(PUBLIC_KEY_KEY)) { // grab the base 64 public key from the API response auto apiPublicKey = QByteArray::fromBase64(dataObject[PUBLIC_KEY_KEY].toString().toUtf8()); // convert the downloaded public key to an RSA struct, if possible const unsigned char* publicKeyData = reinterpret_cast(apiPublicKey.constData()); RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, apiPublicKey.size()); if (rsaPublicKey) { _domainPublicKeys[domainID] = { rsaPublicKey, RSA_free }; } else { qWarning() << "Could not convert in-memory public key for" << domainID << "to usable RSA public key."; qWarning() << "Public key will be re-requested on next heartbeat."; } } else { qWarning() << "There was no public key present in response for domain with ID" << domainID; } } else { qWarning() << "The metaverse API did not return success for public key request for domain with ID" << domainID; } } else { // there was a problem getting the public key for the domain // log it since it will be re-requested on the next heartbeat qWarning() << "Error retreiving public key for domain with ID" << domainID << "-" << reply->errorString(); } // remove this domain ID from the list of pending public key requests _pendingPublicKeyRequests.remove(domainID); reply->deleteLater(); } void IceServer::sendPeerInformationPacket(const NetworkPeer& peer, const HifiSockAddr* destinationSockAddr) { auto peerPacket = NLPacket::create(PacketType::ICEServerPeerInformation); // get the byte array for this peer peerPacket->write(peer.toByteArray()); // write the current packet _serverSocket.writePacket(*peerPacket, *destinationSockAddr); } void IceServer::clearInactivePeers() { NetworkPeerHash::iterator peerItem = _activePeers.begin(); while (peerItem != _activePeers.end()) { SharedNetworkPeer peer = peerItem.value(); if ((usecTimestampNow() - peer->getLastHeardMicrostamp()) > (PEER_SILENCE_THRESHOLD_MSECS * 1000)) { qDebug() << "Removing peer from memory for inactivity -" << *peer; // if we had a public key for this domain, remove it now _domainPublicKeys.erase(peer->getUUID()); // remove the peer object peerItem = _activePeers.erase(peerItem); } else { // we didn't kill this peer, push the iterator forwards ++peerItem; } } }