// Copyright (C) 2019 Alexey Edelev // Copyright (C) 2024 Dennis Oberst // Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause #include "chatmessages.pb.h" #include "qtgrpcchat.grpc.pb.h" #include "credentials/certificates.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { std::ostream &operator<<(std::ostream &os, const chat::Credentials &u) { return os << u.name() << ' ' << u.password() << '\n'; } std::istream &operator>>(std::istream &is, chat::Credentials &u) { std::string name, password; if (is >> name >> password) { u.set_name(std::move(name)); u.set_password(std::move(password)); } return is; } } // namespace class QtGrpcChatService; // A ChatRoomReactor instance represent the bidirectional streaming RPC, i.e. a connected client. class ChatRoomReactor : public grpc::ServerBidiReactor { public: explicit ChatRoomReactor(QtGrpcChatService *parent, grpc::CallbackServerContext *context); const std::string &name() const noexcept { return m_user.name(); } chat::UserStatus::Type userStatusType() const noexcept { return m_userStatusType; } //! [server-4] // Share \a response. It will be kept alive until the last write operation finishes. void startSharedWrite(std::shared_ptr response) { std::scoped_lock lock(m_writeMtx); if (m_response) { m_responseQueue.emplace(std::move(response)); } else { m_response = std::move(response); StartWrite(m_response.get()); } } //! [server-4] std::shared_ptr createMessage() const { auto message = std::make_shared(); message->set_username(name()); message->set_timestamp(std::chrono::system_clock::now().time_since_epoch().count()); return message; } private: void OnDone() override; void OnCancel() override; void OnReadDone(bool ok) override; void OnWriteDone(bool ok) override; private: static const std::string &userNameHeader() { static std::string userNameHeader = "user-name"; return userNameHeader; } static const std::string &userPasswordHeader() { static std::string userPasswordHeader = "user-password"; return userPasswordHeader; } bool m_hasLogin = false; grpc::CallbackServerContext *m_context; QtGrpcChatService *m_service; chat::Credentials m_user; chat::UserStatus::Type m_userStatusType; std::shared_ptr m_request; std::shared_ptr m_response; std::queue> m_responseQueue; std::mutex m_writeMtx; }; //! [server-1] class QtGrpcChatService final : public chat::QtGrpcChat::CallbackService //! [server-1] { public: static const std::string &httpsAddress() { static std::string address = "0.0.0.0:65002"; return address; } static const std::string &httpAddress() { static std::string address = "0.0.0.0:65003"; return address; } const std::string &dbPath() const noexcept { return m_dbPath; } const std::vector &activeClients() & noexcept { return m_activeClients; } bool login(ChatRoomReactor *client) { std::unique_lock lock(m_activeClientsMtx); if (std::find(m_activeClients.begin(), m_activeClients.end(), client) != m_activeClients.end()) return false; std::cout << "login: " << client << ", " << client->name() << '\n'; m_activeClients.push_back(client); // Notify all clients including the connecting one about the new login auto msg = client->createMessage(); msg->mutable_user_status()->set_type(chat::UserStatus::LOGIN); broadcast(msg, nullptr); // Send all active clients of the session to the connecting client for (auto *activeClient : m_activeClients) { if (activeClient == client) continue; msg = activeClient->createMessage(); msg->mutable_user_status()->set_type(activeClient->userStatusType()); msg->mutable_user_status()->set_fetched(true); client->startSharedWrite(msg); } msg.reset(); // detach return true; } bool logout(ChatRoomReactor *client) { std::unique_lock lock(m_activeClientsMtx); if (const auto it = std::find(m_activeClients.begin(), m_activeClients.end(), client); it != m_activeClients.end()) { std::cout << "logout: " << client << ", " << client->name() << '\n'; m_activeClients.erase(it); return true; } return false; } //! [server-3] // Broadcast \a message to all connected clients. Optionally \a skip a client void broadcast(const std::shared_ptr &message, const ChatRoomReactor *skip) { for (auto *client : activeClients()) { assert(client); if (skip && client == skip) continue; client->startSharedWrite(message); } } //! [server-3] private: //! [server-2] grpc::ServerBidiReactor * ChatRoom(grpc::CallbackServerContext *context) override { return new ChatRoomReactor(this, context); } grpc::ServerUnaryReactor *Register(grpc::CallbackServerContext *context, const chat::Credentials *request, chat::None * /*response*/) override //! [server-2] { auto *reactor = context->DefaultReactor(); if (request->name().empty() || request->password().empty()) { reactor->Finish({ grpc::StatusCode::INVALID_ARGUMENT, "Invalid user request" }); return reactor; } std::fstream file(m_dbPath, std::ios::in | std::ios::out | std::ios::app); if (!file) { reactor->Finish({ grpc::StatusCode::UNAVAILABLE, "Database unavailable" }); return reactor; } chat::Credentials credSearch; file.seekg(0); while (file >> credSearch) { if (credSearch.name() == request->name()) { reactor->Finish({ grpc::StatusCode::ALREADY_EXISTS, request->name() + " already exists" }); return reactor; } } file.clear(); if (!(file << *request)) { reactor->Finish({ grpc::StatusCode::DATA_LOSS, "Failed to register user" }); return reactor; } reactor->Finish(grpc::Status::OK); return reactor; } private: std::string m_dbPath = "users.db"; std::vector m_activeClients; std::mutex m_activeClientsMtx; }; ChatRoomReactor::ChatRoomReactor(QtGrpcChatService *parent, grpc::CallbackServerContext *context) : m_context(context), m_service(parent) { const auto &metadata = m_context->client_metadata(); const auto nameIt = metadata.find(userNameHeader()); const auto passIt = metadata.find(userPasswordHeader()); if (nameIt == metadata.end() || passIt == metadata.end()) { Finish({ grpc::StatusCode::NOT_FOUND, "Metadata not found" }); return; } if (nameIt->second.empty() || passIt->second.empty()) { Finish({ grpc::StatusCode::INVALID_ARGUMENT, "Invalid metadata" }); return; } for (const auto *client : m_service->activeClients()) { if (client->name() == nameIt->second) { Finish({ grpc::StatusCode::ALREADY_EXISTS, "User already active" }); return; } } std::fstream file(m_service->dbPath(), std::ios::in); if (!file) { Finish({ grpc::StatusCode::UNAVAILABLE, "Database unavailable" }); return; } chat::Credentials credentials; bool found = false; while (file >> credentials) { if (credentials.name() == nameIt->second) { found = true; break; } } if (!found) { Finish({ grpc::StatusCode::UNAUTHENTICATED, "User not found" }); return; } if (credentials.password() != passIt->second) { Finish({ grpc::StatusCode::INVALID_ARGUMENT, "Invalid password" }); return; } m_user = credentials; if (m_hasLogin = m_service->login(this); !m_hasLogin) { Finish({ grpc::StatusCode::INTERNAL, "Login failed" }); return; } m_userStatusType = chat::UserStatus::ACTIVE; m_request = std::make_shared(); StartRead(m_request.get()); } void ChatRoomReactor::OnDone() { if (m_hasLogin && m_service->logout(this)) { // If the stream is not cancelled we expect a graceful logout // attempt and inform all other users. Otherwise we expect // a reconnect attempt from the client. if (!m_context->IsCancelled()) { auto msg = createMessage(); msg->mutable_user_status()->set_type(chat::UserStatus::LOGOUT); m_service->broadcast(msg, this); } } else { std::cerr << "Failed to logout client" << m_user.name() << '\n'; } delete this; } void ChatRoomReactor::OnCancel() { Finish(grpc::Status::CANCELLED); } void ChatRoomReactor::OnReadDone(bool ok) { if (!ok) { if (m_context->IsCancelled()) return; Finish(grpc::Status::OK); return; } if (m_request->has_user_status()) m_userStatusType = m_request->user_status().type(); //! [server-5] // Distribute the incoming message to all other clients. m_service->broadcast(m_request, this); m_request = std::make_shared(); // detach StartRead(m_request.get()); //! [server-5] } void ChatRoomReactor::OnWriteDone(bool ok) { if (!ok) { if (m_context->IsCancelled()) return; Finish(grpc::Status::OK); return; } //! [server-6] std::scoped_lock lock(m_writeMtx); if (!m_responseQueue.empty()) { m_response = std::move(m_responseQueue.front()); m_responseQueue.pop(); StartWrite(m_response.get()); return; } m_response.reset(); //! [server-6] } int main(int /* argc */, char * /* argv */[]) { grpc::reflection::InitProtoReflectionServerBuilderPlugin(); std::unique_ptr server; QtGrpcChatService service; { grpc::ServerBuilder builder; //! [server-ssl] grpc::SslServerCredentialsOptions sslOpts; sslOpts.pem_key_cert_pairs.emplace_back(grpc::SslServerCredentialsOptions::PemKeyCertPair{ LocalhostKey, LocalhostCert, }); builder.AddListeningPort(QtGrpcChatService::httpsAddress(), grpc::SslServerCredentials(sslOpts)); builder.AddListeningPort(QtGrpcChatService::httpAddress(), grpc::InsecureServerCredentials()); //! [server-ssl] builder.RegisterService(&service); server = builder.BuildAndStart(); std::cout << "Server listening on https://" << QtGrpcChatService::httpsAddress() << '\n'; std::cout << "Server listening on http://" << QtGrpcChatService::httpAddress() << '\n'; } server->Wait(); }