diff --git a/libguh-core/jsonrpc/jsonrpcserver.cpp b/libguh-core/jsonrpc/jsonrpcserver.cpp index 194d1e9e..f94f35e9 100644 --- a/libguh-core/jsonrpc/jsonrpcserver.cpp +++ b/libguh-core/jsonrpc/jsonrpcserver.cpp @@ -74,6 +74,20 @@ JsonRPCServer::JsonRPCServer(const QSslConfiguration &sslConfiguration, QObject QVariantMap returns; QVariantMap params; + params.clear(); returns.clear(); + setDescription("Hello", "Upon first connection, guh will automatically send a welcome message containing information about the setup. If this message is lost for whatever reason (connections with multiple hops might drop this if guh sends it too early), the exact same message can be retrieved multiple times by calling this Hello method. Note that the contents might change if the system changed its state in the meantime, e.g. initialSetupRequired might turn false if the initial setup has been performed in the meantime."); + setParams("Hello", params); + returns.insert("id", JsonTypes::basicTypeToString(JsonTypes::Int)); + returns.insert("server", JsonTypes::basicTypeToString(JsonTypes::String)); + returns.insert("name", JsonTypes::basicTypeToString(JsonTypes::String)); + returns.insert("version", JsonTypes::basicTypeToString(JsonTypes::String)); + returns.insert("uuid", JsonTypes::basicTypeToString(JsonTypes::Uuid)); + returns.insert("language", JsonTypes::basicTypeToString(JsonTypes::String)); + returns.insert("protocol version", JsonTypes::basicTypeToString(JsonTypes::String)); + returns.insert("initialSetupRequired", JsonTypes::basicTypeToString(JsonTypes::Bool)); + returns.insert("authenticationRequired", JsonTypes::basicTypeToString(JsonTypes::Bool)); + setReturns("Hello", returns); + params.clear(); returns.clear(); setDescription("Introspect", "Introspect this API."); setParams("Introspect", params); @@ -135,6 +149,13 @@ QString JsonRPCServer::name() const return QStringLiteral("JSONRPC"); } +JsonReply *JsonRPCServer::Hello(const QVariantMap ¶ms) const +{ + Q_UNUSED(params); + TransportInterface *interface = reinterpret_cast(property("transportInterface").toLongLong()); + return createReply(createWelcomeMessage(interface)); +} + JsonReply* JsonRPCServer::Introspect(const QVariantMap ¶ms) const { Q_UNUSED(params) @@ -292,6 +313,21 @@ void JsonRPCServer::sendUnauthorizedResponse(TransportInterface *interface, cons interface->sendData(clientId, QJsonDocument::fromVariant(errorResponse).toJson(QJsonDocument::Compact)); } +QVariantMap JsonRPCServer::createWelcomeMessage(TransportInterface *interface) const +{ + QVariantMap handshake; + handshake.insert("id", 0); + handshake.insert("server", "guhIO"); + handshake.insert("name", GuhCore::instance()->configuration()->serverName()); + handshake.insert("version", GUH_VERSION_STRING); + handshake.insert("uuid", GuhCore::instance()->configuration()->serverUuid().toString()); + handshake.insert("language", GuhCore::instance()->configuration()->locale().name()); + handshake.insert("protocol version", JSON_PROTOCOL_VERSION); + handshake.insert("initialSetupRequired", (interface->configuration().authenticationEnabled ? GuhCore::instance()->userManager()->users().isEmpty() : false)); + handshake.insert("authenticationRequired", interface->configuration().authenticationEnabled); + return handshake; +} + void JsonRPCServer::setup() { registerHandler(this); @@ -338,16 +374,16 @@ void JsonRPCServer::processData(const QUuid &clientId, const QByteArray &data) // check if authentication is required for this transport if (m_interfaces.value(interface)) { - // if there is no user in the system yet, let's fail unless this is a CreateUser or Introspect call + // if there is no user in the system yet, let's fail unless this is a CreateUser, Introspect or Hello call if (GuhCore::instance()->userManager()->users().isEmpty()) { - if (!(targetNamespace == "JSONRPC" && (method == "CreateUser" || method == "Introspect"))) { + if (!(targetNamespace == "JSONRPC" && (method == "CreateUser" || method == "Introspect" || method == "Hello"))) { sendUnauthorizedResponse(interface, clientId, commandId, "Initial setup required. Call CreateUser first."); return; } } else { - // ok, we have a user. if there isn't a valid token, let's fail unless this is a Authenticate or Introspect call + // ok, we have a user. if there isn't a valid token, let's fail unless this is a Authenticate, Introspect or Hello call QByteArray token = message.value("token").toByteArray(); - if (!(targetNamespace == "JSONRPC" && (method == "Authenticate" || method == "Introspect")) && !GuhCore::instance()->userManager()->verifyToken(token)) { + if (!(targetNamespace == "JSONRPC" && (method == "Authenticate" || method == "Introspect" || method == "Hello")) && !GuhCore::instance()->userManager()->verifyToken(token)) { sendUnauthorizedResponse(interface, clientId, commandId, "Forbidden: Invalid token."); return; } @@ -373,9 +409,10 @@ void JsonRPCServer::processData(const QUuid &clientId, const QByteArray &data) return; } - // Hack: attach clientId to handler to be able to handle the JSONRPC methods. Do not use this outside of jsonrpcserver + // Hack: attach some properties to the handler to be able to handle the JSONRPC methods. Do not use this outside of jsonrpcserver handler->setProperty("clientId", clientId); handler->setProperty("token", message.value("token").toByteArray()); + handler->setProperty("transportInterface", reinterpret_cast(interface)); qCDebug(dcJsonRpc()) << "Got method" << method.toLatin1().data(); @@ -453,17 +490,7 @@ void JsonRPCServer::clientConnected(const QUuid &clientId) // If authentication is required, notifications are disabled by default. Clients must enable them with a valid token m_clients.insert(clientId, !interface->configuration().authenticationEnabled); - QVariantMap handshake; - handshake.insert("id", 0); - handshake.insert("server", "guhIO"); - handshake.insert("name", GuhCore::instance()->configuration()->serverName()); - handshake.insert("version", GUH_VERSION_STRING); - handshake.insert("uuid", GuhCore::instance()->configuration()->serverUuid().toString()); - handshake.insert("language", GuhCore::instance()->configuration()->locale().name()); - handshake.insert("protocol version", JSON_PROTOCOL_VERSION); - handshake.insert("initialSetupRequired", (interface->configuration().authenticationEnabled ? GuhCore::instance()->userManager()->users().isEmpty() : false)); - handshake.insert("authenticationRequired", interface->configuration().authenticationEnabled); - interface->sendData(clientId, QJsonDocument::fromVariant(handshake).toJson(QJsonDocument::Compact)); + interface->sendData(clientId, QJsonDocument::fromVariant(createWelcomeMessage(interface)).toJson(QJsonDocument::Compact)); } void JsonRPCServer::clientDisconnected(const QUuid &clientId) diff --git a/libguh-core/jsonrpc/jsonrpcserver.h b/libguh-core/jsonrpc/jsonrpcserver.h index 3eb6faef..d1d0c18a 100644 --- a/libguh-core/jsonrpc/jsonrpcserver.h +++ b/libguh-core/jsonrpc/jsonrpcserver.h @@ -47,6 +47,7 @@ public: // JsonHandler API implementation QString name() const; + Q_INVOKABLE JsonReply *Hello(const QVariantMap ¶ms) const; Q_INVOKABLE JsonReply *Introspect(const QVariantMap ¶ms) const; Q_INVOKABLE JsonReply *Version(const QVariantMap ¶ms) const; Q_INVOKABLE JsonReply *SetNotificationStatus(const QVariantMap ¶ms); @@ -65,6 +66,7 @@ private: void sendResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QVariantMap ¶ms = QVariantMap()); void sendErrorResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error); void sendUnauthorizedResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error); + QVariantMap createWelcomeMessage(TransportInterface *interface) const; private slots: void setup(); diff --git a/tests/auto/jsonrpc/testjsonrpc.cpp b/tests/auto/jsonrpc/testjsonrpc.cpp index a39f45b8..b77998af 100644 --- a/tests/auto/jsonrpc/testjsonrpc.cpp +++ b/tests/auto/jsonrpc/testjsonrpc.cpp @@ -96,6 +96,7 @@ QStringList TestJSONRPC::extractRefs(const QVariant &variant) void TestJSONRPC::testHandshake() { + // first test if the handshake message is auto-sent upon connecting QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); QUuid newClientId = QUuid::createUuid(); @@ -108,6 +109,10 @@ void TestJSONRPC::testHandshake() QString guhVersionString(GUH_VERSION_STRING); QVERIFY2(handShake.value("version").toString() == guhVersionString, "Handshake version doesn't match Guh version."); + // And now check if it is sent again when calling JSONRPC.Hello + handShake = injectAndWait("JSONRPC.Hello").toMap(); + QCOMPARE(handShake.value("params").toMap().value("version").toString(), guhVersionString); + m_mockTcpServer->clientDisconnected(newClientId); } @@ -133,6 +138,18 @@ void TestJSONRPC::testInitialSetup() QCOMPARE(response.value("status").toString(), QStringLiteral("success")); + // Hello call should work in any case too + spy.clear(); + m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Hello\"}"); + if (spy.count() == 0) { + spy.wait(); + } + QVERIFY(spy.count() == 1); + jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray()); + response = jsonDoc.toVariant().toMap(); + qWarning() << "Calling Hello on uninitialized instance:" << response.value("status").toString() << response.value("error").toString(); + QCOMPARE(response.value("status").toString(), QStringLiteral("success")); + // Any other call should fail with "unauthorized" even if we use a previously valid token spy.clear(); m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Version\"}");