├── .gitignore ├── json_bridge ├── README.md ├── tests │ ├── pipeIO │ │ ├── CMakeLists.txt │ │ └── test_pipeIO.cpp │ ├── bridgeCommunication │ │ ├── CMakeLists.txt │ │ ├── API_mock.h │ │ ├── test_bridgeCommunication.cpp │ │ └── API_mock.cpp │ └── CMakeLists.txt ├── src │ ├── messages │ │ ├── Disconnect.cpp │ │ ├── Registration.cpp │ │ ├── APICall.cpp │ │ └── Message.cpp │ ├── MumbleAssert.cpp │ ├── Util.cpp │ ├── BridgeClient.cpp │ ├── Bridge.cpp │ └── NamedPipe.cpp ├── include │ └── mumble │ │ └── json_bridge │ │ ├── Util.h │ │ ├── MumbleAssert.h │ │ ├── NonCopyable.h │ │ ├── messages │ │ ├── Registration.h │ │ ├── APICall.h │ │ └── Message.h │ │ ├── BridgeClient.h │ │ ├── Bridge.h │ │ └── NamedPipe.h └── CMakeLists.txt ├── vcpkg.json ├── plugin ├── CMakeLists.txt ├── README.md └── plugin.cpp ├── .gitmodules ├── cli ├── operations │ ├── toggle_local_user_mute.yaml │ ├── toggle_local_user_deaf.yaml │ ├── get_local_user_name.yaml │ ├── move_local_user.yaml │ └── move.yaml ├── JSONInstruction.h ├── Instruction.h ├── handleOperation.h ├── JSONInstruction.cpp ├── CMakeLists.txt ├── JSONInterface.h ├── main.cpp ├── JSONInterface.cpp └── README.md ├── README.md ├── scripts ├── runClangFormat.sh ├── generate_CLI_operations.py └── generate_APICall_implementation.py ├── CMakeLists.txt ├── LICENSE ├── cmake └── FindPython3Interpreter.cmake ├── .github └── workflows │ ├── pr-checks.yml │ └── build.yml └── .clang-format /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | compile_commands.json 3 | -------------------------------------------------------------------------------- /json_bridge/README.md: -------------------------------------------------------------------------------- 1 | # json-bridge 2 | 3 | This is the backend that implements the core functionality in terms of the JSON API. 4 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mumble-json-bridge", 3 | "version-string": "1.0.0", 4 | "dependencies": [ 5 | "boost-thread", 6 | "boost-program-options" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /json_bridge/tests/pipeIO/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Mumble Developers. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license 3 | # that can be found in the LICENSE file at the root of the 4 | # source tree. 5 | 6 | create_test(test_pipeIO 7 | test_pipeIO.cpp 8 | ) 9 | -------------------------------------------------------------------------------- /json_bridge/tests/bridgeCommunication/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Mumble Developers. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license 3 | # that can be found in the LICENSE file at the root of the 4 | # source tree. 5 | 6 | create_test(test_bridgeCommunication 7 | test_bridgeCommunication.cpp 8 | API_mock.cpp 9 | ) 10 | -------------------------------------------------------------------------------- /json_bridge/src/messages/Disconnect.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | 7 | namespace Mumble { 8 | namespace JsonBridge { 9 | namespace Messages {}; 10 | }; // namespace JsonBridge 11 | }; // namespace Mumble 12 | -------------------------------------------------------------------------------- /plugin/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Mumble Developers. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license 3 | # that can be found in the LICENSE file at the root of the 4 | # source tree. 5 | 6 | add_library(json_bridge_plugin 7 | SHARED 8 | plugin.cpp 9 | ) 10 | 11 | target_link_libraries(json_bridge_plugin PUBLIC json_bridge mumble_plugin_cpp_wrapper) 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "3rdParty/mumble-plugin-cpp"] 2 | path = 3rdParty/mumble-plugin-cpp 3 | url = https://github.com/mumble-voip/mumble-plugin-cpp.git 4 | [submodule "3rdParty/nlohmann-json"] 5 | path = 3rdParty/nlohmann-json 6 | url = https://github.com/nlohmann/json.git 7 | branch = release/3.9.1 8 | shallow = true 9 | [submodule "3rdParty/googletest"] 10 | path = 3rdParty/googletest 11 | url = https://github.com/google/googletest.git 12 | branch = v1.10.x 13 | -------------------------------------------------------------------------------- /cli/operations/toggle_local_user_mute.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - operation: toggle_local_user_mute 3 | depends: 4 | - name: isMuted 5 | type: boolean 6 | function: 7 | type: api 8 | name: isLocalUserMuted 9 | executes: 10 | function: 11 | type: api 12 | name: requestLocalUserMute 13 | parameter: 14 | - name: muted 15 | value: "!isMuted" 16 | 17 | -------------------------------------------------------------------------------- /cli/operations/toggle_local_user_deaf.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - operation: toggle_local_user_deaf 3 | depends: 4 | - name: isDeafened 5 | type: boolean 6 | function: 7 | type: api 8 | name: isLocalUserDeafened 9 | executes: 10 | function: 11 | type: api 12 | name: requestLocalUserDeaf 13 | parameter: 14 | - name: deafened 15 | value: "!isDeafened" 16 | 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mumble-json-bridge 2 | 3 | A Mumble plugin that offers a JSON API for Mumble interaction via named pipes. 4 | 5 | This project consists of 3 (more or less) separate parts: 6 | 1. [The backend](json_bridge/) 7 | 2. [The plugin](plugin/) 8 | 3. [The CLI](cli/) 9 | 10 | ## Build dependencies 11 | 12 | In order to build the project, you will require 13 | - A cpp17 conform compiler 14 | - CMake v3.10 or more recent 15 | - Boost (required components: program-options and thread) 16 | - Python3 with the [PyYAML](https://pypi.org/project/PyYAML/) package (only needed when building the CLI) 17 | -------------------------------------------------------------------------------- /json_bridge/src/MumbleAssert.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #include "mumble/json_bridge/MumbleAssert.h" 7 | 8 | #include 9 | #include 10 | 11 | 12 | void mumble_jsonbridge_assertionFailure(const char *message, const char *function, const char *file, int line) { 13 | std::cerr << "Assertion failure in function " << function << " (" << file << ":" << line << "): " << message 14 | << std::endl; 15 | std::terminate(); 16 | } 17 | -------------------------------------------------------------------------------- /plugin/README.md: -------------------------------------------------------------------------------- 1 | # JSON-Bridge Mumble plugin 2 | 3 | This project contains the Mumble plugin that will enable the JSON API functionality. In order to do so, 4 | the plugin has to be installed into Mumble. After that, make sure that the plugin is actually loaded 5 | from Mumble's settings. 6 | 7 | If the plugin fails to initialize on a Unix-system, it could be that the last named pipe that has been created 8 | did not get deleted (due to an improper termination of Mumble). In that case you have to delete the pipe 9 | manually before starting the plugin. In order to do so, execute 10 | 11 | ```bash 12 | rm /tmp/.mumble-json-bridge 13 | ``` 14 | 15 | -------------------------------------------------------------------------------- /scripts/runClangFormat.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright 2020 The Mumble Developers. All rights reserved. 4 | # Use of this source code is governed by a BSD-style license 5 | # that can be found in the LICENSE file at the root of the 6 | # Mumble source tree or at . 7 | 8 | currentDir=$(dirname $0) 9 | 10 | mainDir="$currentDir/.." 11 | 12 | cd "$mainDir" 13 | 14 | find -type f \( -iname "*.cpp" -o -iname "*.c" -o -iname "*.hpp" -o -iname "*.h" -o -iname "*.cxx" -o -iname "*.cc" \) \ 15 | -a -not -ipath "./3rdParty/*" -a -not -path "./.git/*" -a -not -ipath "./build/*" \ 16 | | xargs clang-format --style=file -i 17 | -------------------------------------------------------------------------------- /json_bridge/src/messages/Registration.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #include "mumble/json_bridge/messages/Registration.h" 7 | 8 | namespace Mumble { 9 | namespace JsonBridge { 10 | namespace Messages { 11 | 12 | Registration::Registration(const nlohmann::json &msg) : Message(MessageType::REGISTRATION) { 13 | MESSAGE_ASSERT_FIELD(msg, "pipe_path", string); 14 | MESSAGE_ASSERT_FIELD(msg, "secret", string); 15 | 16 | m_pipePath = msg["pipe_path"].get< std::string >(); 17 | m_secret = msg["secret"].get< std::string >(); 18 | } 19 | 20 | }; // namespace Messages 21 | }; // namespace JsonBridge 22 | }; // namespace Mumble 23 | -------------------------------------------------------------------------------- /cli/operations/get_local_user_name.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - operation: get_local_user_name 3 | depends: 4 | - name: connectionID 5 | type: number_integer 6 | function: 7 | type: api 8 | name: getActiveServerConnection 9 | - name: userID 10 | type: number_integer 11 | function: 12 | type: api 13 | name: getLocalUserID 14 | parameter: 15 | - name: connection 16 | value: connectionID 17 | executes: 18 | function: 19 | type: api 20 | name: getUserName 21 | parameter: 22 | - name: connection 23 | value: connectionID 24 | - name: user_id 25 | value: userID 26 | -------------------------------------------------------------------------------- /json_bridge/include/mumble/json_bridge/Util.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #ifndef MUMBLE_JSONBRIDGE_UTILS_H_ 7 | #define MUMBLE_JSONBRIDGE_UTILS_H_ 8 | 9 | #include 10 | 11 | namespace Mumble { 12 | namespace JsonBridge { 13 | namespace Util { 14 | 15 | /** 16 | * Generates a random string consisting of alphanumeric 17 | * characters and a few simple special characters. 18 | * 19 | * @param size The size of the string to generate 20 | * @return The generated string 21 | */ 22 | std::string generateRandomString(size_t size); 23 | 24 | }; // namespace Util 25 | }; // namespace JsonBridge 26 | }; // namespace Mumble 27 | 28 | #endif // MUMBLE_JSONBRIDGE_UTILS_H_ 29 | -------------------------------------------------------------------------------- /json_bridge/include/mumble/json_bridge/MumbleAssert.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #ifndef MUMBLE_JSONBRIDGE_ASSERT_H_ 7 | #define MUMBLE_JSONBRIDGE_ASSERT_H_ 8 | 9 | /** 10 | * Function called whenever an assertion fails 11 | * 12 | * @see MUMBLE_ASSERT 13 | */ 14 | void mumble_jsonbridge_assertionFailure(const char *message, const char *function, const char *file, int line); 15 | 16 | #define MUMBLE_ASSERT(condition) MUMBLE_ASSERT_MSG(condition, "Failed condition is \"" #condition "\"") 17 | 18 | #define MUMBLE_ASSERT_MSG(condition, msg) \ 19 | if (!(condition)) { \ 20 | mumble_jsonbridge_assertionFailure(msg, __FUNCTION__, __FILE__, __LINE__); \ 21 | } 22 | 23 | #endif // MUMBLE_JSONBRIDGE_ASSERT_H_ 24 | -------------------------------------------------------------------------------- /json_bridge/src/Util.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #include "mumble/json_bridge/Util.h" 7 | 8 | #include 9 | 10 | namespace Mumble { 11 | namespace JsonBridge { 12 | namespace Util { 13 | 14 | std::string generateRandomString(size_t size) { 15 | const char chars[] = "0123456789" 16 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 17 | "abcdefghijklmnopqrstuvwxyz" 18 | "+-*/()[]{}"; 19 | 20 | std::string str; 21 | str.resize(size); 22 | 23 | std::random_device rd; 24 | std::mt19937 mt(rd()); 25 | std::uniform_int_distribution< int > dist(0, sizeof(chars) - 1); 26 | 27 | for (size_t i = 0; i < size; i++) { 28 | str[i] = chars[dist(mt)]; 29 | } 30 | 31 | return str; 32 | } 33 | 34 | }; // namespace Util 35 | }; // namespace JsonBridge 36 | }; // namespace Mumble 37 | -------------------------------------------------------------------------------- /cli/JSONInstruction.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #ifndef MUMBLE_JSONBRIDGE_CLI_JSONINSTRUCTION_H_ 7 | #define MUMBLE_JSONBRIDGE_CLI_JSONINSTRUCTION_H_ 8 | 9 | #include "Instruction.h" 10 | 11 | #include 12 | 13 | namespace Mumble { 14 | namespace JsonBridge { 15 | namespace CLI { 16 | 17 | /** 18 | * An instruction that is received in JSON format 19 | */ 20 | class JSONInstruction : public Instruction { 21 | private: 22 | /** 23 | * The original JSON message 24 | */ 25 | nlohmann::json m_msg; 26 | 27 | public: 28 | JSONInstruction(const nlohmann::json &msg); 29 | virtual nlohmann::json execute(const JSONInterface &jsonInterface) override; 30 | }; 31 | 32 | }; // namespace CLI 33 | }; // namespace JsonBridge 34 | }; // namespace Mumble 35 | 36 | 37 | #endif // MUMBLE_JSONBRIDGE_CLI_JSONINSTRUCTION_H_ 38 | -------------------------------------------------------------------------------- /json_bridge/include/mumble/json_bridge/NonCopyable.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | 7 | #ifndef MUMBLE_JSONBRIDGE_NONCOPYABLE_H_ 8 | #define MUMBLE_JSONBRIDGE_NONCOPYABLE_H_ 9 | 10 | namespace Mumble { 11 | namespace JsonBridge { 12 | 13 | /** 14 | * Helper class that disables copy-constructors. Extending this class will prevent the auto-generation of 15 | * copy-constructors of the subclass. 16 | */ 17 | class NonCopyable { 18 | public: 19 | NonCopyable() = default; 20 | 21 | NonCopyable(NonCopyable &&) = default; 22 | NonCopyable &operator=(NonCopyable &&) = default; 23 | 24 | // Delete copy-constructor 25 | NonCopyable(const NonCopyable &) = delete; 26 | // Delete copy-assignment 27 | NonCopyable &operator=(const NonCopyable &) = delete; 28 | }; 29 | 30 | }; // namespace JsonBridge 31 | }; // namespace Mumble 32 | 33 | #endif // MUMBLE_JSONBRIDGE_NONCOPYABLE_H_ 34 | -------------------------------------------------------------------------------- /cli/Instruction.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #ifndef MUMBLE_JSONBRIDGE_CLI_INSTRUCTION_H_ 7 | #define MUMBLE_JSONBRIDGE_CLI_INSTRUCTION_H_ 8 | 9 | #include 10 | 11 | namespace Mumble { 12 | namespace JsonBridge { 13 | namespace CLI { 14 | 15 | class JSONInterface; 16 | 17 | /** 18 | * Abstract base class for all kinds of instructions that this CLI can process. 19 | */ 20 | class Instruction { 21 | public: 22 | /** 23 | * Executes this instruction. 24 | * 25 | * @param jsonInterface The JSONInterface to use for API calls 26 | * @returns The result of executing this instruction 27 | */ 28 | virtual nlohmann::json execute(const JSONInterface &jsonInterface) = 0; 29 | }; 30 | 31 | }; // namespace CLI 32 | }; // namespace JsonBridge 33 | }; // namespace Mumble 34 | 35 | 36 | #endif // MUMBLE_JSONBRIDGE_CLI_INSTRUCTION_H_ 37 | -------------------------------------------------------------------------------- /json_bridge/src/BridgeClient.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #include "mumble/json_bridge/BridgeClient.h" 7 | #include "mumble/json_bridge/NamedPipe.h" 8 | 9 | namespace Mumble { 10 | namespace JsonBridge { 11 | BridgeClient::BridgeClient(const std::filesystem::path &pipePath, const std::string &secret, client_id_t id) 12 | : m_pipePath(pipePath), m_secret(secret), m_id(id) {} 13 | 14 | BridgeClient::~BridgeClient() {} 15 | 16 | void BridgeClient::write(const std::string &message) const { NamedPipe::write(m_pipePath, message); } 17 | 18 | client_id_t BridgeClient::getID() const noexcept { return m_id; } 19 | 20 | const std::filesystem::path &BridgeClient::getPipePath() const noexcept { return m_pipePath; } 21 | 22 | bool BridgeClient::secretMatches(const std::string &secret) const noexcept { return m_secret == secret; } 23 | 24 | BridgeClient::operator bool() const noexcept { return m_id != INVALID_CLIENT_ID; } 25 | }; // namespace JsonBridge 26 | }; // namespace Mumble 27 | -------------------------------------------------------------------------------- /plugin/plugin.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #include "mumble/plugin/MumblePlugin.h" 7 | #include 8 | 9 | #include 10 | 11 | class MumbleJsonBridge : public MumblePlugin { 12 | private: 13 | Mumble::JsonBridge::Bridge m_bridge; 14 | 15 | public: 16 | MumbleJsonBridge() 17 | : MumblePlugin("JSON Bridge", "Mumble Developers", 18 | "This plugin offers a JSON API for Mumble interaction via named pipes"), 19 | m_bridge(m_api) {} 20 | 21 | mumble_error_t init() noexcept override { 22 | std::cout << "JSON-Bridge initialized" << std::endl; 23 | 24 | m_bridge.start(); 25 | 26 | return MUMBLE_STATUS_OK; 27 | } 28 | 29 | void shutdown() noexcept override { 30 | m_bridge.stop(true); 31 | std::cout << "JSON-Bridge shut down" << std::endl; 32 | } 33 | 34 | void releaseResource(const void *ptr) noexcept override { std::terminate(); } 35 | }; 36 | 37 | MumblePlugin &MumblePlugin::getPlugin() noexcept { 38 | static MumbleJsonBridge bridge; 39 | 40 | return bridge; 41 | } 42 | -------------------------------------------------------------------------------- /cli/handleOperation.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #ifndef MUMBLE_JSONBRIDGE_CLI_HANDLEOPERATION_H_ 7 | #define MUMBLE_JSONBRIDGE_CLI_HANDLEOPERATION_H_ 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | /** 15 | * Exception that is being thrown if the execution of an operation fails. 16 | */ 17 | class OperationException : public std::logic_error { 18 | public: 19 | using std::logic_error::logic_error; 20 | }; 21 | 22 | /** 23 | * Process a message that requests the execution of an operation (message_type == "operation"). 24 | * 25 | * @param msg The JSON representation of the respective message 26 | * @param executeQuery A functor that can be used to send API calls to the JSON bridge 27 | * @returns The result of the operation (JSON format). This will be the JSON response to 28 | * the last performed API call. 29 | */ 30 | nlohmann::json handleOperation(const nlohmann::json &msg, 31 | const std::function< nlohmann::json(nlohmann::json &) > &executeQuery); 32 | 33 | #endif // MUMBLE_JSONBRIDGE_CLI_HANDLEOPERATION_H_ 34 | -------------------------------------------------------------------------------- /json_bridge/tests/bridgeCommunication/API_mock.h: -------------------------------------------------------------------------------- 1 | #ifndef API_MOCK_H_ 2 | #define API_MOCK_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | namespace API_Mock { 10 | extern std::unordered_map< std::string, int > calledFunctions; 11 | 12 | static constexpr mumble_plugin_id_t pluginID = 42; 13 | static constexpr mumble_connection_t activeConnetion = 13; 14 | static constexpr mumble_userid_t localUserID = 5; 15 | static constexpr mumble_userid_t otherUserID = 7; 16 | static constexpr mumble_channelid_t localUserChannel = 244; 17 | static constexpr mumble_channelid_t otherUserChannel = 243; 18 | static const std::string localUserName = "Local user"; 19 | static const std::string otherUserName = "Other user"; 20 | static const std::string localUserChannelName = "Channel of local user"; 21 | static const std::string otherUserChannelName = "Channel of other user"; 22 | static const std::string localUserChannelDesc = "Channel of local user (description)"; 23 | static const std::string otherUserChannelDesc = "Channel of other user (description)"; 24 | 25 | MumbleAPI_v_1_2_x getMumbleAPI_v_1_2_x(); 26 | 27 | }; // namespace API_Mock 28 | 29 | #endif // API_MOCK_H_ 30 | -------------------------------------------------------------------------------- /cli/JSONInstruction.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #include "JSONInstruction.h" 7 | #include "JSONInterface.h" 8 | #include "handleOperation.h" 9 | 10 | #include 11 | 12 | namespace Mumble { 13 | namespace JsonBridge { 14 | namespace CLI { 15 | 16 | JSONInstruction::JSONInstruction(const nlohmann::json &msg) : m_msg(msg) {} 17 | 18 | nlohmann::json JSONInstruction::execute(const JSONInterface &jsonInterface) { 19 | MESSAGE_ASSERT_FIELD(m_msg, "message_type", string); 20 | 21 | if (m_msg["message_type"].get< std::string >() == "api_call") { 22 | return jsonInterface.process(m_msg); 23 | } else if (m_msg["message_type"].get< std::string >() == "operation") { 24 | return handleOperation(m_msg["message"], 25 | [&jsonInterface](nlohmann::json &msg) { return jsonInterface.process(msg); }); 26 | } else { 27 | throw Messages::InvalidMessageException(std::string("Unknown \"message_type\" option \"") 28 | + m_msg["message_type"].get< std::string >() + "\""); 29 | } 30 | } 31 | 32 | }; // namespace CLI 33 | }; // namespace JsonBridge 34 | }; // namespace Mumble 35 | -------------------------------------------------------------------------------- /json_bridge/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Mumble Developers. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license 3 | # that can be found in the LICENSE file at the root of the 4 | # source tree. 5 | 6 | set(CMAKE_POSITION_INDEPENDENT_CODE ON) 7 | 8 | option(tests "Build tests" OFF) 9 | 10 | add_library(json_bridge 11 | STATIC 12 | src/NamedPipe.cpp 13 | src/Bridge.cpp 14 | src/MumbleAssert.cpp 15 | src/BridgeClient.cpp 16 | src/Util.cpp 17 | src/messages/Message.cpp 18 | src/messages/Registration.cpp 19 | src/messages/APICall.cpp 20 | ) 21 | 22 | target_include_directories(json_bridge PUBLIC include/) 23 | 24 | find_package(Boost COMPONENTS thread chrono REQUIRED) 25 | target_link_libraries(json_bridge PUBLIC ${Boost_LIBRARIES}) 26 | target_include_directories(json_bridge PUBLIC ${Boost_INCLUDE_DIRS}) 27 | 28 | set(PLATFORM_DEFINES "") 29 | 30 | if(UNIX) 31 | list(APPEND PLATFORM_DEFINES "PLATFORM_UNIX") 32 | elseif(WIN32) 33 | list(APPEND PLATFORM_DEFINES "PLATFORM_WINDOWS") 34 | else() 35 | message(FATAL_ERROR "Unsupported platform") 36 | endif() 37 | 38 | target_link_libraries(json_bridge PUBLIC mumble_plugin_cpp_wrapper_api nlohmann_json::nlohmann_json) 39 | target_compile_definitions(json_bridge PUBLIC ${PLATFORM_DEFINES}) 40 | 41 | 42 | if (tests) 43 | include(GoogleTest) 44 | add_subdirectory(tests) 45 | endif() 46 | -------------------------------------------------------------------------------- /json_bridge/include/mumble/json_bridge/messages/Registration.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #ifndef MUMBLE_JSONBRIDGE_MESSAGES_REGISTRATION_H_ 7 | #define MUMBLE_JSONBRIDGE_MESSAGES_REGISTRATION_H_ 8 | 9 | #include "mumble/json_bridge/messages/Message.h" 10 | 11 | #include 12 | 13 | #include 14 | 15 | namespace Mumble { 16 | namespace JsonBridge { 17 | namespace Messages { 18 | 19 | /** 20 | * This class represents a message for registering a new client to the Mumble-JSON-Bridge 21 | */ 22 | class Registration : public Message { 23 | public: 24 | /** 25 | * The extracted path to the client's named pipe 26 | */ 27 | std::string m_pipePath; 28 | /** 29 | * The extracted secret the client has provided 30 | */ 31 | std::string m_secret; 32 | 33 | /** 34 | * Parses the given message and populates the members of this instance accordingly. If the message 35 | * doesn't fulfill the requirements, this constructor will throw an InvalidMessageException. 36 | * 37 | * @param msg The **body** of the registration message 38 | */ 39 | explicit Registration(const nlohmann::json &msg); 40 | }; 41 | }; // namespace Messages 42 | }; // namespace JsonBridge 43 | }; // namespace Mumble 44 | 45 | #endif // MUMBLE_JSONBRIDGE_MESSAGES_REGISTRATION_H_ 46 | -------------------------------------------------------------------------------- /cli/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Mumble Developers. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license 3 | # that can be found in the LICENSE file at the root of the 4 | # source tree. 5 | 6 | include(FindPython3Interpreter) 7 | 8 | set(GENERATED_OPERATIONS_FILE "${CMAKE_CURRENT_BINARY_DIR}/handleOperation.cpp") 9 | 10 | add_executable(mumble_json_bridge_cli 11 | main.cpp 12 | JSONInterface.cpp 13 | JSONInstruction.cpp 14 | "${GENERATED_OPERATIONS_FILE}" 15 | ) 16 | 17 | target_link_libraries(mumble_json_bridge_cli PUBLIC json_bridge) 18 | 19 | find_package(Boost COMPONENTS program_options REQUIRED) 20 | target_link_libraries(mumble_json_bridge_cli PUBLIC ${Boost_LIBRARIES}) 21 | target_include_directories(mumble_json_bridge_cli PUBLIC ${Boost_INCLUDE_DIRS} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") 22 | 23 | findPython3Interpreter(PYTHON_EXE) 24 | 25 | # Generate the code for handling operations in the CLI 26 | # We have to create a dummy target in order to ensure that 27 | # we re-generate this source file every time we build in order 28 | # for it to always be up-to-date. 29 | # See https://stackoverflow.com/a/31518137/3907364 30 | add_custom_target(my_custom_target_that_always_runs ALL 31 | DEPENDS "dummy.doesnt.exist" 32 | ) 33 | add_custom_command( 34 | OUTPUT "${GENERATED_OPERATIONS_FILE}" "dummy.doesnt.exist" 35 | COMMAND "${PYTHON_EXE}" "${CMAKE_SOURCE_DIR}/scripts/generate_CLI_operations.py" -i "${CMAKE_CURRENT_SOURCE_DIR}/operations" -o "${GENERATED_OPERATIONS_FILE}" 36 | ) 37 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Mumble Developers. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license 3 | # that can be found in the LICENSE file at the root of the 4 | # source tree. 5 | 6 | cmake_minimum_required(VERSION 3.10) 7 | 8 | set(version "1.0.0") 9 | 10 | project(mumble-json-bridge 11 | VERSION "${version}" 12 | DESCRIPTION "A Mumble plugin that offers a JSON API for Mumble interaction via named pipes" 13 | LANGUAGES "CXX" 14 | ) 15 | 16 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") 17 | 18 | if ("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8") 19 | message(STATUS "Building 64bit") 20 | else() 21 | message(STATUS "Building non-64bit (probably 32bit)") 22 | endif() 23 | 24 | 25 | option(cli "Build the CLI" ON) 26 | option(plugin "Build the Mumble plugin" ON) 27 | option(static "Prefer static linkage" OFF) 28 | 29 | 30 | set(CMAKE_CXX_STANDARD 17) 31 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 32 | 33 | set(JSON_BuildTests OFF CACHE INTERNAL "") 34 | set(JSON_Install OFF CACHE INTERNAL "") 35 | set(JSON_ImplicitConversions OFF CACHE INTERNAL "") 36 | 37 | if (static) 38 | set(Boost_USE_STATIC_LIBS ON) 39 | endif() 40 | 41 | 42 | # Make sure that ctest can be run from the build dir directly and not only in certain sub-dirs inside of it 43 | enable_testing() 44 | 45 | add_subdirectory(3rdParty/nlohmann-json) 46 | add_subdirectory(3rdParty/mumble-plugin-cpp) 47 | 48 | add_subdirectory(json_bridge) 49 | 50 | if (plugin) 51 | add_subdirectory(plugin) 52 | endif() 53 | 54 | if (cli) 55 | add_subdirectory(cli) 56 | endif() 57 | -------------------------------------------------------------------------------- /cli/operations/move_local_user.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - operation: move_local_user 3 | parameter: 4 | - name: channel 5 | type: string 6 | - name: password 7 | type: string 8 | default: '""' 9 | depends: 10 | - name: connectionID 11 | type: number_integer 12 | function: 13 | type: api 14 | name: getActiveServerConnection 15 | - name: userID 16 | type: number_integer 17 | function: 18 | type: api 19 | name: getLocalUserID 20 | parameter: 21 | - name: connection 22 | value: connectionID 23 | - name: channelID 24 | type: number_integer 25 | function: 26 | type: api 27 | name: findChannelByName 28 | parameter: 29 | - name: connection 30 | value: connectionID 31 | - name: channel_name 32 | value: channel 33 | executes: 34 | function: 35 | type: api 36 | name: requestUserMove 37 | parameter: 38 | - name: connection 39 | value: connectionID 40 | - name: user_id 41 | value: userID 42 | - name: channel_id 43 | value: channelID 44 | - name: password 45 | value: password 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Mumble 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /json_bridge/src/messages/APICall.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #include "mumble/json_bridge/messages/APICall.h" 7 | 8 | // define JSON serialization functions 9 | template< typename ContentType > void to_json(nlohmann::json &j, const MumbleArray< ContentType > &array) { 10 | std::vector< ContentType > vec; 11 | vec.reserve(array.size()); 12 | 13 | for (const auto ¤t : array) { 14 | vec.push_back(current); 15 | } 16 | 17 | j = vec; 18 | } 19 | 20 | namespace Mumble { 21 | namespace JsonBridge { 22 | namespace Messages { 23 | 24 | // include function implementations 25 | #include "APICall_handleImpl.cpp" 26 | 27 | APICall::APICall(const MumbleAPI &api, const nlohmann::json &msg) 28 | : Message(MessageType::API_CALL), m_api(api), m_msg(msg) { 29 | MESSAGE_ASSERT_FIELD(msg, "function", string); 30 | 31 | m_functionName = msg["function"].get< std::string >(); 32 | 33 | if (s_allFunctions.count(m_functionName) == 0) { 34 | throw InvalidMessageException(std::string("Unknown API function \"") + m_functionName + "\""); 35 | } 36 | 37 | if (s_noParamFunctions.count(m_functionName) == 0) { 38 | MESSAGE_ASSERT_FIELD(msg, "parameter", object); 39 | } 40 | } 41 | 42 | nlohmann::json APICall::execute(const std::string &bridgeSecret) const { 43 | return ::Mumble::JsonBridge::Messages::execute(m_functionName, m_api, bridgeSecret, m_msg); 44 | } 45 | 46 | }; // namespace Messages 47 | }; // namespace JsonBridge 48 | }; // namespace Mumble 49 | -------------------------------------------------------------------------------- /cli/operations/move.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | - operation: move 3 | parameter: 4 | - name: user 5 | type: string 6 | - name: channel 7 | type: string 8 | - name: password 9 | type: string 10 | default: '""' 11 | depends: 12 | - name: connectionID 13 | type: number_integer 14 | function: 15 | type: api 16 | name: getActiveServerConnection 17 | - name: userID 18 | type: number_integer 19 | function: 20 | type: api 21 | name: findUserByName 22 | parameter: 23 | - name: connection 24 | value: connectionID 25 | - name: user_name 26 | value: user 27 | - name: channelID 28 | type: number_integer 29 | function: 30 | type: api 31 | name: findChannelByName 32 | parameter: 33 | - name: connection 34 | value: connectionID 35 | - name: channel_name 36 | value: channel 37 | executes: 38 | function: 39 | type: api 40 | name: requestUserMove 41 | parameter: 42 | - name: connection 43 | value: connectionID 44 | - name: user_id 45 | value: userID 46 | - name: channel_id 47 | value: channelID 48 | - name: password 49 | value: password 50 | -------------------------------------------------------------------------------- /json_bridge/tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Mumble Developers. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license 3 | # that can be found in the LICENSE file at the root of the 4 | # source tree. 5 | 6 | # Prevent GTest from trying to install itself 7 | set(INSTALL_GTEST FALSE CACHE BOOL "" FORCE) 8 | # Force GTest to use shared CRT libraries which is what cmake defaults to for 9 | # our test-cases anyways 10 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) 11 | add_subdirectory("${CMAKE_SOURCE_DIR}/3rdParty/googletest" "${CMAKE_BINARY_DIR}/3rdParty/googletest") 12 | 13 | macro(create_test TESTNAME) 14 | # create an exectuable in which the tests will be stored 15 | add_executable(${TESTNAME} ${ARGN}) 16 | 17 | # link the Google test infrastructure, mocking library, and a default main fuction to 18 | # the test executable. Remove g_test_main if writing your own main function. 19 | target_link_libraries(${TESTNAME} PRIVATE gtest gmock gtest_main) 20 | 21 | # gtest_discover_tests replaces gtest_add_tests, 22 | # see https://cmake.org/cmake/help/v3.10/module/GoogleTest.html for more options to pass to it 23 | gtest_discover_tests(${TESTNAME} 24 | # set a working directory so your project root so that you can find test data via paths relative to the project root 25 | WORKING_DIRECTORY ${PROJECT_DIR} 26 | PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "${PROJECT_DIR}" 27 | ) 28 | 29 | # Signal the the files are built for testing purposes 30 | target_compile_definitions(${TESTNAME} PUBLIC TESTING) 31 | 32 | target_link_libraries(${TESTNAME} PRIVATE json_bridge) 33 | endmacro() 34 | 35 | add_subdirectory(pipeIO) 36 | add_subdirectory(bridgeCommunication) 37 | -------------------------------------------------------------------------------- /cli/JSONInterface.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #ifndef MUMBLE_JSONBRIDGE_CLI_INTERFACE_H_ 7 | #define MUMBLE_JSONBRIDGE_CLI_INTERFACE_H_ 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | namespace Mumble { 15 | namespace JsonBridge { 16 | namespace CLI { 17 | 18 | /** 19 | * The interface used to communicate with the Mumble JSON bridge 20 | */ 21 | class JSONInterface { 22 | private: 23 | /** 24 | * The timeout to use for read operations 25 | */ 26 | uint32_t m_readTimeout; 27 | /** 28 | * The timeout to use for write operations 29 | */ 30 | uint32_t m_writeTimeout; 31 | /** 32 | * The pipe that is being used by this interface to receive answers from the Bridge 33 | */ 34 | NamedPipe m_pipe; 35 | /** 36 | * The ID the Bridge has assigned us 37 | */ 38 | client_id_t m_id; 39 | /** 40 | * A secret key that we are using to proof our identity 41 | */ 42 | std::string m_secret; 43 | /** 44 | * The Bridge's secret 45 | */ 46 | std::string m_bridgeSecret; 47 | 48 | public: 49 | explicit JSONInterface(uint32_t readTimeout = 1000, uint32_t m_writeTimeout = 100); 50 | ~JSONInterface(); 51 | 52 | /** 53 | * Sends the given message to the Mumble JSON Bridge 54 | * 55 | * @param msg The message to be sent 56 | * @returns The Bridge's response 57 | */ 58 | nlohmann::json process(nlohmann::json msg) const; 59 | }; 60 | 61 | }; // namespace CLI 62 | }; // namespace JsonBridge 63 | }; // namespace Mumble 64 | 65 | #endif // MUMBLE_JSONBRIDGE_CLI_INTERFACE_H_ 66 | -------------------------------------------------------------------------------- /cmake/FindPython3Interpreter.cmake: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The Mumble Developers. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license 3 | # that can be found in the LICENSE file at the root of the 4 | # source tree. 5 | 6 | # This function will attempt to find a Python3 interpreter. In constrast to the approach taken by 7 | # find_package(Python3) this function will first try to get a suitable executable from the current 8 | # PATH and does not try to find the most recent version of Python installed on this computer. 9 | # This is so that ideally this function finds the same binary that is also used if the user typed 10 | # e.g. "python3" in their command-line. 11 | # If it fails to get the executable from the PATH, it falls back to using find_package after all. 12 | function(findPython3Interpreter outVar) 13 | # Start by trying to search for "python3" executable in PATH 14 | find_program(PYTHON3_EXE NAMES "python3") 15 | 16 | if (NOT PYTHON3_EXE) 17 | # Try to search for "python" executable in PATH 18 | find_program(PYTHON3_EXE NAMES "python") 19 | 20 | if (PYTHON3_EXE) 21 | # Check that this is actually Python3 and not Python2 22 | execute_process(COMMAND "${PYTHON3_EXE}" "--version" OUTPUT_VARIABLE PYTHON3_VERSION) 23 | # Remove "Python" from version specifier 24 | string(REPLACE "Python" "" PYTHON3_VERSION "${PYTHON3_VERSION}") 25 | string(STRIP "${PYTHON3_VERSION}" PYTHON3_VERSION) 26 | 27 | if (NOT "${PYTHON3_VERSION}" MATCHES "^3") 28 | # this is not Python3 -> clear the vars 29 | set(PYTHON3_EXE "NOTFOUND") 30 | endif() 31 | 32 | set(PYTHON3_VERSION "NOTFOUND") 33 | endif() 34 | endif() 35 | 36 | if (NOT PYTHON3_EXE) 37 | message(WARNING "Can't find Python3 in PATH -> Falling back to find_package") 38 | find_package(Python3 COMPONENTS Interpreter QUIET) 39 | set(PYTHON3_EXE "${Python3_EXECUTABLE}") 40 | endif() 41 | 42 | if (NOT PYTHON3_EXE) 43 | message(FATAL_ERROR "Unable to find Python3 interpreter") 44 | else() 45 | message(STATUS "Found Python3: ${PYTHON3_EXE}") 46 | set(${outVar} "${PYTHON3_EXE}" PARENT_SCOPE) 47 | endif() 48 | endfunction() 49 | -------------------------------------------------------------------------------- /json_bridge/src/messages/Message.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #include "mumble/json_bridge/messages/Message.h" 7 | 8 | #include 9 | 10 | namespace Mumble { 11 | namespace JsonBridge { 12 | namespace Messages { 13 | 14 | std::string to_string(MessageType type) { 15 | switch (type) { 16 | case MessageType::REGISTRATION: 17 | return "Registration"; 18 | case MessageType::API_CALL: 19 | return "api_call"; 20 | case MessageType::DISCONNECT: 21 | return "disconnect"; 22 | } 23 | 24 | throw std::invalid_argument(std::string("Unknown message type \"") 25 | + std::to_string(static_cast< int >(type)) + "\""); 26 | } 27 | 28 | MessageType type_from_string(const std::string &type) { 29 | if (boost::iequals(type, "registration")) { 30 | return MessageType::REGISTRATION; 31 | } else if (boost::iequals(type, "api_call")) { 32 | return MessageType::API_CALL; 33 | } else if (boost::iequals(type, "disconnect")) { 34 | return MessageType::DISCONNECT; 35 | } else { 36 | throw std::invalid_argument(std::string("Unknown message type \"") + type + "\""); 37 | } 38 | } 39 | 40 | MessageType parseBasicFormat(const nlohmann::json &msg) { 41 | if (!msg.is_object()) { 42 | throw InvalidMessageException("The given message is not a JSON object"); 43 | } 44 | 45 | MESSAGE_ASSERT_FIELD(msg, "message_type", string); 46 | 47 | MessageType type; 48 | try { 49 | type = type_from_string(msg["message_type"].get< std::string >()); 50 | } catch (const std::invalid_argument &) { 51 | throw InvalidMessageException(std::string("The given message_type \"") 52 | + msg["message_type"].get< std::string >() + "\" is unknown"); 53 | } 54 | 55 | if (type != MessageType::DISCONNECT) { 56 | // The disconnect message doesn't require a message body 57 | MESSAGE_ASSERT_FIELD(msg, "message", object); 58 | } 59 | 60 | return type; 61 | } 62 | 63 | Message::Message(MessageType type) : m_type(type) {} 64 | 65 | Message::~Message() {} 66 | 67 | MessageType Message::getType() const noexcept { return m_type; } 68 | }; // namespace Messages 69 | }; // namespace JsonBridge 70 | }; // namespace Mumble 71 | -------------------------------------------------------------------------------- /json_bridge/include/mumble/json_bridge/messages/APICall.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #ifndef MUMBLE_JSONBRIDGE_MESSAGES_APICALL_H_ 7 | #define MUMBLE_JSONBRIDGE_MESSAGES_APICALL_H_ 8 | 9 | #include "mumble/json_bridge/messages/Message.h" 10 | 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | namespace Mumble { 19 | namespace JsonBridge { 20 | namespace Messages { 21 | 22 | /** 23 | * This class represents a message that requests the Bridge to call a specific Mumble API function 24 | */ 25 | class APICall : public Message { 26 | private: 27 | /** 28 | * The name of the API function that should be called 29 | */ 30 | std::string m_functionName; 31 | /** 32 | * A reference to a MumbleAPI 33 | */ 34 | const MumbleAPI &m_api; 35 | /** 36 | * The **body** of the API-call request message 37 | */ 38 | nlohmann::json m_msg; 39 | 40 | /** 41 | * A set of all available API function names 42 | */ 43 | static const std::unordered_set< std::string > s_allFunctions; 44 | /** 45 | * A set of the names of all API functions that don't take any parameter 46 | */ 47 | static const std::unordered_set< std::string > s_noParamFunctions; 48 | 49 | public: 50 | /** 51 | * Creates an instance of this Message. If the provided message doesn't fulfill 52 | * the requirements of this class, this constructor will throw an InvalidMessageException. 53 | * 54 | * @param api A **reference** to a MumbleAPI. The lifetime of this API must not be shorter than the one of 55 | * this instance 56 | * @param msg The **body** of the API-call request message 57 | */ 58 | explicit APICall(const MumbleAPI &api, const nlohmann::json &msg); 59 | 60 | /** 61 | * Executes the requested API function 62 | * 63 | * @returns JSON representation of the message describing the status of the invocation (including potential 64 | * return values) 65 | */ 66 | nlohmann::json execute(const std::string &bridgeSecret) const; 67 | }; 68 | }; // namespace Messages 69 | }; // namespace JsonBridge 70 | }; // namespace Mumble 71 | 72 | #endif // MUMBLE_JSONBRIDGE_MESSAGES_APICALL_H_ 73 | -------------------------------------------------------------------------------- /.github/workflows/pr-checks.yml: -------------------------------------------------------------------------------- 1 | name: PR-Checks 2 | 3 | on: [pull_request] 4 | 5 | env: 6 | buildDir: ${{ github.workspace }}/build/ 7 | 8 | jobs: 9 | pr-checks: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | 14 | - uses: actions/checkout@v2 15 | with: 16 | submodules: 'recursive' 17 | 18 | - name: Check line endings 19 | uses: erclu/check-crlf@v1 20 | 21 | - uses: BSFishy/pip-action@v1 22 | with: 23 | packages: PyYAML 24 | 25 | - uses: lukka/get-cmake@latest 26 | 27 | # Restore from cache the previously built ports. If a "cache miss" 28 | # occurs, then vcpkg is bootstrapped. Since a the vcpkg.json is 29 | # being used later on to install the packages when run-cmake runs, 30 | # no packages are installed at this time and the input 'setupOnly:true 31 | # is mandatory. 32 | - name: Restore artifacts / setup vcpkg 33 | uses: lukka/run-vcpkg@v7 34 | with: 35 | # Just install vcpkg for now, do not install any ports 36 | # in this step yet. 37 | setupOnly: true 38 | vcpkgGitCommitId: 3166bcc15b156b57667d9e573fba9775ceef3eb1 39 | # Since the cache must be invalidated when content of the 40 | # vcpkg.json file changes, let's compute its hash and append 41 | # this to the computed cache's key. 42 | appendedCacheKey: ${{ hashFiles( '**/vcpkg.json' ) }} 43 | vcpkgTriplet: ${{ matrix.triplet }} 44 | # Ensure the vcpkg artifacts are cached, they are generated in the 45 | # 'CMAKE_BINARY_DIR/vcpkg_installed' directory. 46 | additionalCachedPaths: ${{ env.buildDir }}/vcpkg_installed 47 | 48 | - name: Configure 49 | uses: lukka/run-cmake@v3 50 | with: 51 | cmakeListsOrSettingsJson: CMakeListsTxtAdvanced 52 | cmakeListsTxtPath: '${{ github.workspace }}/CMakeLists.txt' 53 | buildDirectory: ${{ env.buildDir }} 54 | # This input tells run-cmake to consume the vcpkg.cmake toolchain 55 | # file set by run-vcpkg. 56 | useVcpkgToolchainFile: true 57 | buildWithCMake: false 58 | cmakeAppendedArgs: -DCMAKE_EXPORT_COMPILE_COMMANDS=ON 59 | 60 | - name: Link compile-command DB 61 | run: ln -s "${{ env.buildDir }}/compile_commands.json" . 62 | shell: bash 63 | 64 | - name: Check code formatting 65 | uses: jidicula/clang-format-action@v4.11.0 66 | with: 67 | clang-format-version: '10' 68 | check-path: '.' 69 | exclude-regex: '(3rdParty/*|build/*|vcpkg/*)' 70 | -------------------------------------------------------------------------------- /cli/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #include "Instruction.h" 7 | #include "JSONInstruction.h" 8 | #include "JSONInterface.h" 9 | #include "handleOperation.h" 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | int main(int argc, char **argv) { 21 | try { 22 | boost::program_options::options_description desc("Command-line interface for the Mumble-JSON-Bridge"); 23 | 24 | uint32_t readTimeout; 25 | uint32_t writeTimeout; 26 | 27 | desc.add_options()("help,h", "Produces this help message")("json,j", 28 | boost::program_options::value< std::string >(), 29 | "Specifies the JSON message to be sent to Mumble")( 30 | "read-timeout,r", boost::program_options::value< uint32_t >(&readTimeout)->default_value(1000), 31 | "The timeout for read-operations (in ms)")( 32 | "write-timeout,w", boost::program_options::value< uint32_t >(&writeTimeout)->default_value(100), 33 | "The timeout for write-operations (in ms)"); 34 | 35 | boost::program_options::variables_map vm; 36 | boost::program_options::store(boost::program_options::parse_command_line(argc, argv, desc), vm); 37 | boost::program_options::notify(vm); 38 | 39 | if (vm.count("help")) { 40 | std::cout << desc << std::endl; 41 | return 0; 42 | } 43 | 44 | nlohmann::json json; 45 | if (vm.count("json")) { 46 | json = nlohmann::json::parse(vm["json"].as< std::string >()); 47 | } else { 48 | // Read all content from stdin 49 | std::string content(std::istreambuf_iterator< char >(std::cin), {}); 50 | boost::trim(content); 51 | 52 | json = nlohmann::json::parse(content); 53 | } 54 | 55 | Mumble::JsonBridge::CLI::JSONInstruction instruction(json); 56 | 57 | Mumble::JsonBridge::CLI::JSONInterface jsonInterface(readTimeout, writeTimeout); 58 | 59 | std::cout << instruction.execute(jsonInterface).dump(2) << std::endl; 60 | } catch (const Mumble::JsonBridge::TimeoutException &) { 61 | std::cerr << "[ERROR]: The operation timed out (Are you sure the JSON Bridge is running?)" << std::endl; 62 | return 2; 63 | } catch (const OperationException &e) { 64 | std::cerr << "[ERROR]: Operation failed: " << e.what() << std::endl; 65 | return 3; 66 | } catch (const std::exception &e) { 67 | std::cerr << "[ERROR]: " << e.what() << std::endl; 68 | return 4; 69 | } 70 | 71 | return 0; 72 | } 73 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | buildDir: ${{ github.workspace }}/build/ 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | include: 14 | - os: ubuntu-24.04 15 | triplet: x64-linux 16 | - os: windows-2022 17 | triplet: x64-windows 18 | - os: macos-14 19 | triplet: arm64-osx 20 | 21 | runs-on: ${{ matrix.os }} 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | submodules: 'recursive' 27 | 28 | - uses: actions/setup-python@v2 29 | with: 30 | python-version: "3.x" 31 | 32 | - uses: BSFishy/pip-action@v1 33 | with: 34 | packages: PyYAML 35 | 36 | - uses: lukka/get-cmake@latest 37 | 38 | # Restore from cache the previously built ports. If a "cache miss" 39 | # occurs, then vcpkg is bootstrapped. Since a the vcpkg.json is 40 | # being used later on to install the packages when run-cmake runs, 41 | # no packages are installed at this time and the input 'setupOnly:true 42 | # is mandatory. 43 | - name: Restore artifacts / setup vcpkg 44 | uses: lukka/run-vcpkg@v7 45 | with: 46 | # Just install vcpkg for now, do not install any ports 47 | # in this step yet. 48 | setupOnly: true 49 | vcpkgGitCommitId: 401edd4e93917851f003f88c0fed03d20eba47bc 50 | # Since the cache must be invalidated when content of the 51 | # vcpkg.json file changes, let's compute its hash and append 52 | # this to the computed cache's key. 53 | appendedCacheKey: ${{ hashFiles( '**/vcpkg.json' ) }} 54 | vcpkgTriplet: ${{ matrix.triplet }} 55 | # Ensure the vcpkg artifacts are cached, they are generated in the 56 | # 'CMAKE_BINARY_DIR/vcpkg_installed' directory. 57 | additionalCachedPaths: ${{ env.buildDir }}/vcpkg_installed 58 | 59 | - name: Build 60 | uses: lukka/run-cmake@v3 61 | with: 62 | cmakeListsOrSettingsJson: CMakeListsTxtAdvanced 63 | cmakeListsTxtPath: '${{ github.workspace }}/CMakeLists.txt' 64 | buildDirectory: ${{ env.buildDir }} 65 | # This input tells run-cmake to consume the vcpkg.cmake toolchain 66 | # file set by run-vcpkg. 67 | useVcpkgToolchainFile: true 68 | buildWithCMake: true 69 | cmakeAppendedArgs: -Dtests=ON 70 | 71 | - name: Test 72 | run: cd "${{ env.buildDir }}"; ctest --output-on-failure --timeout 10 73 | shell: bash 74 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | AccessModifierOffset: -4 4 | AlignAfterOpenBracket: Align 5 | AlignConsecutiveAssignments: true 6 | # Setting AlignConsecutiveDeclarations to true would be nice but it doesn't work right with PointerAlignment=Right 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlines: Left 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllParametersOfDeclarationOnNextLine: true 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: InlineOnly 15 | AllowShortIfStatementsOnASingleLine: false 16 | AllowShortLoopsOnASingleLine: false 17 | AlwaysBreakAfterReturnType: None 18 | AlwaysBreakBeforeMultilineStrings: false 19 | AlwaysBreakTemplateDeclarations: false 20 | BinPackArguments: true 21 | BinPackParameters: true 22 | BreakBeforeBinaryOperators: NonAssignment 23 | BreakBeforeBraces: Attach 24 | BreakBeforeTernaryOperators: true 25 | BreakConstructorInitializers: BeforeColon 26 | BreakStringLiterals: true 27 | ColumnLimit: 120 28 | CompactNamespaces: false 29 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 30 | ConstructorInitializerIndentWidth: 4 31 | ContinuationIndentWidth: 4 32 | Cpp11BracedListStyle: false 33 | DerivePointerAlignment: false 34 | DisableFormat: false 35 | FixNamespaceComments: true 36 | ForEachMacros: 37 | - foreach 38 | - Q_FOREACH 39 | - BOOST_FOREACH 40 | IncludeBlocks: Preserve 41 | IncludeCategories: 42 | # Global.h must always be included last 43 | - Regex: 'Global.h' 44 | Priority: 10000 45 | # Since a lot of windows-headers rely on windows.h, it has to be included first 46 | - Regex: 'windows.h' 47 | Priority: 1 48 | # Assign a priority < INT_MAX to all other includes in order to be able to force headers 49 | # to come after them 50 | - Regex: '.*' 51 | Priority: 10 52 | IndentCaseLabels: true 53 | IndentPPDirectives: AfterHash 54 | IndentWidth: 4 55 | IndentWrappedFunctionNames: true 56 | KeepEmptyLinesAtTheStartOfBlocks: false 57 | MacroBlockBegin: '' 58 | MacroBlockEnd: '' 59 | MaxEmptyLinesToKeep: 3 60 | NamespaceIndentation: Inner 61 | PenaltyBreakAssignment: 2 62 | PenaltyBreakBeforeFirstCallParameter: 19 63 | PenaltyBreakComment: 300 64 | PenaltyBreakFirstLessLess: 120 65 | PenaltyBreakString: 1000 66 | PenaltyExcessCharacter: 1000000 67 | PenaltyReturnTypeOnItsOwnLine: 60 68 | PointerAlignment: Right 69 | ReflowComments: true 70 | SortIncludes: true 71 | SortUsingDeclarations: true 72 | SpaceAfterCStyleCast: true 73 | SpaceAfterTemplateKeyword: false 74 | SpaceBeforeAssignmentOperators: true 75 | SpaceBeforeParens: ControlStatements 76 | SpaceInEmptyParentheses: false 77 | SpacesBeforeTrailingComments: 1 78 | SpacesInAngles: true 79 | SpacesInContainerLiterals: true 80 | SpacesInParentheses: false 81 | SpacesInSquareBrackets: false 82 | Standard: Cpp11 83 | TabWidth: 4 84 | UseTab: ForContinuationAndIndentation 85 | ... 86 | 87 | -------------------------------------------------------------------------------- /cli/JSONInterface.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #include "JSONInterface.h" 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | namespace Mumble { 15 | namespace JsonBridge { 16 | namespace CLI { 17 | 18 | JSONInterface::JSONInterface(uint32_t readTimeout, uint32_t writeTimeout) 19 | : m_readTimeout(readTimeout), m_writeTimeout(writeTimeout) { 20 | std::filesystem::path pipePath; 21 | #ifdef PLATFORM_WINDOWS 22 | pipePath = "\\\\.\\pipe\\"; 23 | #else 24 | pipePath = "/tmp/"; 25 | #endif // PLATFORM_WINDOWS 26 | 27 | pipePath = pipePath / ".mumble-json-bridge-cli"; 28 | 29 | m_pipe = NamedPipe::create(pipePath); 30 | 31 | m_secret = Util::generateRandomString(12); 32 | 33 | // clang-format off 34 | nlohmann::json registration = { 35 | {"message_type", "registration"}, 36 | {"message", 37 | { 38 | {"pipe_path", pipePath.string()}, 39 | {"secret", m_secret} 40 | } 41 | } 42 | }; 43 | // clang-format off 44 | 45 | NamedPipe::write(Bridge::s_pipePath, registration.dump(), m_writeTimeout); 46 | 47 | nlohmann::json response = nlohmann::json::parse(m_pipe.read_blocking(m_readTimeout)); 48 | 49 | m_bridgeSecret = response["secret"].get(); 50 | m_id = response["response"]["client_id"].get(); 51 | } 52 | 53 | JSONInterface::~JSONInterface() { 54 | // clang-format off 55 | nlohmann::json message = { 56 | { "message_type", "disconnect" }, 57 | { "client_id", m_id }, 58 | { "secret", m_secret } 59 | }; 60 | // clang-format on 61 | 62 | try { 63 | NamedPipe::write(Bridge::s_pipePath, message.dump(), m_writeTimeout); 64 | // We patiently wait for the Bridge's reply, even though we don't care about it. This is in 65 | // order for the Bridge's operation to not error due to timeout. 66 | std::string answer = m_pipe.read_blocking(m_readTimeout); 67 | } catch (...) { 68 | // Ignore any exceptions that this might cause. If it does throw then this client might not 69 | // be disconnected from the Bridge, which isn't that bad. Besides: We probably can't do anything 70 | // about it anyways, but we certainly don't want our program to terminate because of it. 71 | } 72 | } 73 | 74 | nlohmann::json JSONInterface::process(nlohmann::json msg) const { 75 | msg["secret"] = m_secret; 76 | msg["client_id"] = m_id; 77 | 78 | NamedPipe::write(Bridge::s_pipePath, msg.dump(), m_writeTimeout); 79 | 80 | nlohmann::json response = nlohmann::json::parse(m_pipe.read_blocking(m_readTimeout)); 81 | 82 | if (response["secret"].get< std::string >() != m_bridgeSecret) { 83 | std::cerr << "[ERROR]: Bridge secret doesn't match" << std::endl; 84 | return {}; 85 | } 86 | 87 | // Remove the secret field as it has already been validated here 88 | response.erase("secret"); 89 | 90 | return response; 91 | } 92 | }; // namespace CLI 93 | }; // namespace JsonBridge 94 | }; // namespace Mumble 95 | -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | The CLI is the part that allows external applications to easily talk to Mumble's JSON bridge without having to 4 | worry about the communication details (i.e. the process does not have to worry about named pipes and how 5 | and which to use). 6 | 7 | All requests are made using JSON syntax. The basic message format is 8 | ``` 9 | { 10 | "message_type": "", 11 | "message": { 12 | 13 | } 14 | } 15 | ``` 16 | 17 | Available choices for `` are: 18 | 19 | | **Type** | **Description** | 20 | | -------- | --------------- | 21 | | `api_call` | A direct call to one of Mumble's API functions (plugin API) | 22 | | `operation` | A more high-level operation | 23 | 24 | ## api_call 25 | 26 | In order to directly make an API call, the `` has to follow the form 27 | ``` 28 | "function": "`, 29 | "parameter": { 30 | 31 | } 32 | ``` 33 | 34 | `` is a list of key-value-pairs providing the parameters for the function that is 35 | going to be called. The necessary parameters (names and type) as well as the function name itself 36 | can be obtained from the official 37 | [C++ plugin API wrapper](https://github.com/mumble-voip/mumble-plugin-cpp/blob/7e1256a958d8e452ddb5273a20c30b0a26d6c4dc/include/mumble/plugin/MumbleAPI.h). 38 | Note however that the parameter names have to be converted to snake_case first (e.g. `useID` transforms 39 | to `user_id`). 40 | 41 | If a function does not take any parameter, the entire `parameter` entry **must not be present** in 42 | the sent message. 43 | 44 | ### Examples 45 | 46 | ``` 47 | { 48 | "message_type": "api_call", 49 | "message": { 50 | "function": "getActiveServerConnection" 51 | } 52 | } 53 | ``` 54 | 55 | ``` 56 | { 57 | "message_type": "api_call", 58 | "message": { 59 | "function": "getChannelName", 60 | "parameter": { 61 | "connection": 0, 62 | "channel_id": 5 63 | } 64 | } 65 | } 66 | ``` 67 | 68 | 69 | ## operation 70 | 71 | Usually one needs to perform multiple successive API calls in order to get what one wants. This 72 | is exactly where operations come into play. An operation is a specific API call combination that 73 | is automatically processed by the CLI for you (including error handling). If for instance you want 74 | to obtain the local user's name, this involves 3 API calls aready: 75 | 1. Obtain the ID of the current connection 76 | 2. Obtain local user's ID 77 | 3. Obtain the name associated with that ID 78 | 79 | Thankfully there is an operation for this: 80 | ``` 81 | { 82 | "message_type": "operation", 83 | "message": { 84 | "operation": "get_local_user_name" 85 | } 86 | } 87 | ``` 88 | 89 | Operations can also take parameter, which are handled in the same way as parameter for API calls: 90 | ``` 91 | { 92 | "message_type": "operation", 93 | "message": { 94 | "operation": "move", 95 | "parameter": { 96 | "user": "SomeUser", 97 | "channel": "MyChannel" 98 | } 99 | } 100 | } 101 | ``` 102 | 103 | All operations are defined in the [operations](operations/) directory in form of a YAML file. It 104 | contains the definition of the operation and also what parameter it takes (parameters that have 105 | default values defined are optional). 106 | 107 | -------------------------------------------------------------------------------- /json_bridge/include/mumble/json_bridge/messages/Message.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #ifndef MUMBLE_JSONBRIDGE_MESSAGES_MESSAGE_H_ 7 | #define MUMBLE_JSONBRIDGE_MESSAGES_MESSAGE_H_ 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #define MESSAGE_ASSERT_FIELD(msg, name, type) \ 15 | if (!msg.contains(name)) { \ 16 | throw ::Mumble::JsonBridge::Messages::InvalidMessageException("The given message does not specify a \"" name \ 17 | "\" field"); \ 18 | } \ 19 | if (!msg[name].is_##type()) { \ 20 | throw ::Mumble::JsonBridge::Messages::InvalidMessageException("The \"" name \ 21 | "\" field is expected to be of type " #type); \ 22 | } 23 | 24 | namespace Mumble { 25 | namespace JsonBridge { 26 | namespace Messages { 27 | 28 | /** 29 | * An exception thrown if a message doesn't fulfill the requirements 30 | */ 31 | class InvalidMessageException : public std::logic_error { 32 | public: 33 | using std::logic_error::logic_error; 34 | }; 35 | 36 | /** 37 | * An enum holding the possible message types 38 | */ 39 | enum class MessageType { REGISTRATION, API_CALL, DISCONNECT }; 40 | 41 | /** 42 | * @return A unique string representation of the give MessageType. If no such 43 | * representation can be found, an exception is thrown. 44 | * 45 | * @param type The type to convert to string 46 | * 47 | * @see Mumble::JsonBridge::Messages::MessageType 48 | */ 49 | std::string to_string(MessageType type); 50 | /** 51 | * @return The MessageType corresponding to the provided string representation. If the provided string 52 | * is not a valid representation of a MessageType, this function will throw an exception. 53 | * 54 | * @param type The type's string representation 55 | * 56 | * @see Mumble::JsonBridge::Messages::MessageType 57 | */ 58 | MessageType type_from_string(const std::string &type); 59 | 60 | /** 61 | * Verifies that the message represented by the given JSON object fulfills the basic requirements 62 | * of a message sent to the Mumble-JSON-Bridge. 63 | * 64 | * @param msg The JSON representation of the message 65 | * @returns The MessageType of the provided message 66 | * 67 | * @see Mumble::JsonBridge::Messages::MessageType 68 | */ 69 | MessageType parseBasicFormat(const nlohmann::json &msg); 70 | 71 | /** 72 | * This class represents a message received by the Mumble-JSON-Bridge 73 | */ 74 | class Message { 75 | protected: 76 | /** 77 | * The type of this message 78 | */ 79 | MessageType m_type; 80 | 81 | Message(MessageType type); 82 | 83 | public: 84 | virtual ~Message(); 85 | 86 | /** 87 | * @return The MessageType of this message 88 | * 89 | * @see Mumble::JsonBridge::Messages::MessageType 90 | */ 91 | MessageType getType() const noexcept; 92 | }; 93 | 94 | }; // namespace Messages 95 | }; // namespace JsonBridge 96 | }; // namespace Mumble 97 | 98 | #endif // MUMBLE_JSONBRIDGE_MESSAGES_MESSAGE_H_ 99 | -------------------------------------------------------------------------------- /json_bridge/include/mumble/json_bridge/BridgeClient.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #ifndef MUMBLE_JSONBRIDGE_BRIDGECLIENT_H_ 7 | #define MUMBLE_JSONBRIDGE_BRIDGECLIENT_H_ 8 | 9 | #include "mumble/json_bridge/NonCopyable.h" 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | namespace Mumble { 16 | namespace JsonBridge { 17 | 18 | /** 19 | * The type used for representing client IDs 20 | * 21 | * @see Mumble::JsonBridge::INVALID_CLIENT_ID 22 | */ 23 | using client_id_t = unsigned int; 24 | /** 25 | * The value representing an invalid client ID. This can be used to represent uninitialized IDs. 26 | */ 27 | constexpr client_id_t INVALID_CLIENT_ID = (std::numeric_limits< client_id_t >::max)(); 28 | 29 | /** 30 | * This class represents a client that is currently connected to the Bridge. In particular it wraps functionality 31 | * like verifying a client's secret and writing messages to it. 32 | * 33 | * @see Mumble::JsonBridge::Bridge 34 | */ 35 | class BridgeClient : NonCopyable { 36 | private: 37 | /** 38 | * The client's ID 39 | */ 40 | client_id_t m_id = INVALID_CLIENT_ID; 41 | /** 42 | * The path to the client's named pipe. This is where messages are being written to. 43 | */ 44 | std::filesystem::path m_pipePath; 45 | /** 46 | * The client's secret that it provided during registration. If a message is received claiming to 47 | * to come from this client it has to be verified that the provided secret matches this one. Otherwise 48 | * the identity of the client can't be verified. 49 | * 50 | * @see Mumble::JsonBridge::BridgeClient::secretMatches() 51 | */ 52 | std::string m_secret; 53 | 54 | public: 55 | /** 56 | * Creates an **invalid** instance 57 | */ 58 | explicit BridgeClient() = default; 59 | /** 60 | * Creates an instance of a client with the specified data. 61 | * 62 | * @param path The path to the client's named pipe 63 | * @param secret The secret the client has provided (used for identity verification) 64 | * @param id The ID that is assigned to this client. If not given, the ID of this client is set to be invalid. 65 | */ 66 | explicit BridgeClient(const std::filesystem::path &pipePath, const std::string &secret, 67 | client_id_t id = INVALID_CLIENT_ID); 68 | ~BridgeClient(); 69 | 70 | BridgeClient(BridgeClient &&) = default; 71 | BridgeClient &operator=(BridgeClient &&) = default; 72 | 73 | /** 74 | * Writes the given message to this client's named pipe 75 | * 76 | * @param message The message to write 77 | */ 78 | void write(const std::string &message) const; 79 | 80 | /** 81 | * @returns The ID of this client 82 | */ 83 | client_id_t getID() const noexcept; 84 | /** 85 | * @returns The path to this client's named pipe 86 | */ 87 | const std::filesystem::path &getPipePath() const noexcept; 88 | 89 | /** 90 | * Checks whether the provided secret matches with the one provided by this client. 91 | * 92 | * @param secret The secret to verify 93 | * @returns Whether the provided secret matches 94 | */ 95 | bool secretMatches(const std::string &secret) const noexcept; 96 | 97 | /** 98 | * @returns Whether this client is currently in a valid state 99 | */ 100 | operator bool() const noexcept; 101 | }; 102 | 103 | }; // namespace JsonBridge 104 | }; // namespace Mumble 105 | 106 | #endif // MUMBLE_JSONBRIDGE_BRIDGECLIENT_H_ 107 | -------------------------------------------------------------------------------- /json_bridge/include/mumble/json_bridge/Bridge.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #ifndef MUMBLE_JSONBRIDGE_BRIDGE_H_ 7 | #define MUMBLE_JSONBRIDGE_BRIDGE_H_ 8 | 9 | #include "mumble/json_bridge/BridgeClient.h" 10 | #include "mumble/json_bridge/NamedPipe.h" 11 | 12 | #include "mumble/json_bridge/messages/APICall.h" 13 | #include "mumble/json_bridge/messages/Registration.h" 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | 21 | #include 22 | 23 | #include 24 | 25 | namespace Mumble { 26 | namespace JsonBridge { 27 | 28 | /** 29 | * Tbis class represents the heart of the Mumble-JSON-Bridge. It is responsible for creating a new thread in which 30 | * it'll create the named pipe used for communication. It will enter a loop of polling for messages until the bridge 31 | * is stopped again. 32 | */ 33 | class Bridge { 34 | private: 35 | /** 36 | * Little helper mutex that is needed in order to ensure that m_workerThread has been assigned properly before 37 | * being accessed for the first time in the new thread. 38 | */ 39 | std::mutex m_startMutex; 40 | /** 41 | * The worker thread of this class in which basically all operations of this class happen. 42 | */ 43 | boost::thread m_workerThread; 44 | /** 45 | * The pipe-instance that is used for communication 46 | */ 47 | NamedPipe m_pipe; 48 | /** 49 | * A map of currently registered clients 50 | */ 51 | std::unordered_map< client_id_t, BridgeClient > m_clients; 52 | /** 53 | * The bridge's secret used to identify itself when talking to clients 54 | */ 55 | std::string m_secret; 56 | /** 57 | * A **reference** to the MumbleAPI. API-call requests will be forwarded to and processed by it. 58 | */ 59 | const MumbleAPI &m_api; 60 | 61 | /** 62 | * A continuous counter for assigning unique IDs to new clients. This variable must not be accessed 63 | * outside of m_workerThread. 64 | * 65 | * @see Mumble::JsonBridge::Bridge::m_workerThread 66 | */ 67 | static client_id_t s_nextClientID; 68 | 69 | /** 70 | * Internal start-method that must not be called outside of m_workerThread 71 | * 72 | * @see Mumble::JsonBridge::Bridge::m_workerThread 73 | */ 74 | void doStart(); 75 | /** 76 | * Method used to process received messages 77 | * 78 | * @msg The JSON representation of the respective message 79 | */ 80 | void processMessage(const nlohmann::json &msg); 81 | 82 | /** 83 | * Used to handle registration messages. 84 | * 85 | * @param msg The message to process 86 | */ 87 | void handleRegistration(const Messages::Registration &msg); 88 | /** 89 | * Used to handle API-call request messages. 90 | * 91 | * @param msg The message to process 92 | */ 93 | void handleAPICall(const BridgeClient &client, const Messages::APICall &msg) const; 94 | /** 95 | * Used to handle disconnect messages 96 | * 97 | * @param msg The JSON representation of the message to process 98 | */ 99 | void handleDisconnect(const nlohmann::json &msg); 100 | 101 | public: 102 | /** 103 | * The path at which the Bridge's named pipe will be made available. If it doesn't exist, this means that 104 | * the Bridge has not started (yet). 105 | */ 106 | static const std::filesystem::path s_pipePath; 107 | 108 | /** 109 | * Creates a new instance of this Bridge 110 | * 111 | * @param api A **reference** to the MumbleAPI. The lifetime of the referenced API object must not be shorter 112 | * than the lifetime of this Bridge object! 113 | */ 114 | Bridge(const MumbleAPI &api); 115 | 116 | /** 117 | * Start the bridge. Note that this will be done in a different thread, so this function is will return 118 | * immediately. 119 | */ 120 | void start(); 121 | /** 122 | * Stops the bridge. 123 | * 124 | * @param join Whether this function should wait on the worker-thread to terminate. Otherwise this function will 125 | * return immediately. 126 | */ 127 | void stop(bool join); 128 | }; 129 | 130 | }; // namespace JsonBridge 131 | }; // namespace Mumble 132 | 133 | #endif // MUMBLE_JSONBRIDGE_BRIDGE_H_ 134 | -------------------------------------------------------------------------------- /json_bridge/tests/pipeIO/test_pipeIO.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #include "gtest/gtest.h" 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | #define PIPENAME "testPipe" 20 | 21 | #ifdef PLATFORM_UNIX 22 | # define PIPEDIR "." 23 | #else 24 | # define PIPEDIR "\\\\.\\pipe\\" 25 | #endif 26 | 27 | #define TEST_STRING \ 28 | "This is a test-string that should exceed the default pipe-buffer and should therefore require multiple " \ 29 | "iterations for reading" 30 | 31 | #define TEST_STRING_L32 "This is a string with 32 chars. " 32 | 33 | constexpr unsigned int READ_TIMOUT = 10 * 1000; 34 | 35 | using namespace Mumble::JsonBridge; 36 | 37 | const std::filesystem::path pipePath = std::filesystem::path(PIPEDIR) / PIPENAME; 38 | 39 | 40 | class PipeIOTest : public ::testing::Test { 41 | protected: 42 | NamedPipe m_testPipe; 43 | std::atomic< bool > m_failed = std::atomic< bool >(false); 44 | boost::thread m_readerThread; 45 | 46 | PipeIOTest() { 47 | std::error_code errorCode; 48 | EXPECT_FALSE(std::filesystem::exists(pipePath, errorCode)) << "Pipe already exists"; 49 | EXPECT_FALSE(errorCode) << "Checking for pipe-existance failed"; 50 | } 51 | 52 | ~PipeIOTest() { 53 | // Explicitly destroy the pipe in order to be able to check its effects 54 | // Note that this will not cause any problems with the object's actual destruction 55 | m_testPipe.destroy(); 56 | 57 | std::error_code errorCode; 58 | EXPECT_FALSE(std::filesystem::exists(pipePath, errorCode) && !errorCode) 59 | << "NamedPipe's destructor didn't destroy the pipe"; 60 | } 61 | 62 | void setupPipeReader() { 63 | try { 64 | m_testPipe = NamedPipe::create(pipePath); 65 | 66 | ASSERT_EQ(m_testPipe.getPath(), pipePath); 67 | 68 | test_read(); 69 | } catch (const boost::thread_interrupted &) { 70 | std::cout << "Pipe-thread was interrupted" << std::endl; 71 | } catch (const std::exception &e) { 72 | m_failed.store(true); 73 | FAIL() << "Exception: " << e.what() << std::endl; 74 | } catch (...) { 75 | m_failed.store(true); 76 | FAIL() << "Unhandled exception" << std::endl; 77 | } 78 | } 79 | 80 | virtual void test_read() const { ASSERT_EQ(m_testPipe.read_blocking(READ_TIMOUT), TEST_STRING); } 81 | 82 | void SetUp() override { m_readerThread = boost::thread(&PipeIOTest::setupPipeReader, this); } 83 | 84 | void TearDown() override { m_readerThread.join(); } 85 | }; 86 | 87 | class PipeIOTest2 : public PipeIOTest { 88 | void test_read() const override { ASSERT_EQ(m_testPipe.read_blocking(READ_TIMOUT), TEST_STRING_L32); } 89 | }; 90 | 91 | class PipeIOTest3 : public PipeIOTest { 92 | void test_read() const override { 93 | std::string content; 94 | ASSERT_THROW(content = m_testPipe.read_blocking(100), TimeoutException); 95 | } 96 | }; 97 | 98 | 99 | void waitUntilPipeExists(const std::atomic< bool > &errorFlag) { 100 | // Busy waiting with sleep until the new thread spun up and the pipe is ready 101 | while (!std::filesystem::exists(pipePath) && !errorFlag.load()) { 102 | boost::this_thread::sleep_for(boost::chrono::milliseconds(5)); 103 | } 104 | } 105 | 106 | TEST_F(PipeIOTest, basicIO) { 107 | waitUntilPipeExists(m_failed); 108 | 109 | if (m_failed.load()) { 110 | std::cerr << "Test aborted" << std::endl; 111 | return; 112 | } 113 | 114 | NamedPipe::write(pipePath, TEST_STRING); 115 | } 116 | 117 | TEST_F(PipeIOTest2, contentMatchesBufferSize) { 118 | waitUntilPipeExists(m_failed); 119 | 120 | ASSERT_TRUE(strlen(TEST_STRING_L32) == 32); 121 | 122 | if (m_failed.load()) { 123 | std::cerr << "Test aborted" << std::endl; 124 | return; 125 | } 126 | 127 | NamedPipe::write(pipePath, TEST_STRING_L32); 128 | } 129 | 130 | TEST_F(PipeIOTest, interruptable) { 131 | boost::this_thread::sleep_for(boost::chrono::seconds(3)); 132 | 133 | m_readerThread.interrupt(); 134 | 135 | m_readerThread.join(); 136 | 137 | SUCCEED(); 138 | } 139 | 140 | TEST_F(PipeIOTest3, read_timeout) { 141 | // We don't write anything to the pipe and therefore expect the pipe's read function to throw a TimeoutException 142 | m_readerThread.join(); 143 | 144 | SUCCEED(); 145 | } 146 | 147 | TEST(PipeIOTest4, write_timeout_nonExistantTarget) { 148 | std::filesystem::path dummyPipePath(std::filesystem::path(PIPEDIR) / "myDummyPipe"); 149 | ASSERT_THROW(NamedPipe::write(dummyPipePath, "dummyMsg", 100), TimeoutException); 150 | } 151 | 152 | TEST(PipeIOTest4, write_timeout_pipeNotDrained) { 153 | NamedPipe pipe = NamedPipe::create(std::filesystem::path(PIPEDIR) / "undrainedPipe"); 154 | 155 | ASSERT_THROW(pipe.write("dummyMsg", 100), TimeoutException); 156 | } 157 | 158 | TEST(PipeIOTest4, check_existence) { 159 | ASSERT_FALSE(NamedPipe::exists("I certainly don't exist on neither OS")); 160 | 161 | #ifdef PLATFORM_UNIX 162 | ASSERT_FALSE(NamedPipe::exists("/This/Is/Not/A/valid/Path/To/a/pipe")); 163 | #else 164 | ASSERT_FALSE(NamedPipe::exists("\\\\.\\pipe\\ICertainlyDontExistEvenThoughICouldBeValid")); 165 | #endif 166 | 167 | NamedPipe dummyPipe = NamedPipe::create(std::filesystem::path(PIPEDIR) / "dummyPipe"); 168 | 169 | ASSERT_TRUE(NamedPipe::exists(dummyPipe.getPath())); 170 | } 171 | 172 | TEST(PipeIOTest4, create_destroy_check_existence) { 173 | std::filesystem::path pipePath = std::filesystem::path(PIPEDIR) / "myPipe"; 174 | 175 | ASSERT_FALSE(NamedPipe::exists(pipePath)); 176 | 177 | NamedPipe pipe = NamedPipe::create(pipePath); 178 | 179 | ASSERT_TRUE(NamedPipe::exists(pipePath)); 180 | 181 | pipe.destroy(); 182 | 183 | ASSERT_FALSE(NamedPipe::exists(pipePath)); 184 | } 185 | -------------------------------------------------------------------------------- /json_bridge/include/mumble/json_bridge/NamedPipe.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #ifndef MUMBLE_JSONBRIDGE_NAMEDPIPE_H_ 7 | #define MUMBLE_JSONBRIDGE_NAMEDPIPE_H_ 8 | 9 | #include "mumble/json_bridge/NonCopyable.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #ifdef PLATFORM_WINDOWS 17 | # include 18 | #endif 19 | 20 | namespace Mumble { 21 | namespace JsonBridge { 22 | /** 23 | * An exception thrown by NamedPipe operations if something didn't go as intended 24 | * 25 | * @tparam error_code_t The type of the underlying error code 26 | */ 27 | template< typename error_code_t > class PipeException : public std::exception { 28 | private: 29 | /** 30 | * The error code associated with the encountered error 31 | */ 32 | error_code_t m_errorCode; 33 | /** 34 | * A message for displaying purpose that explains what happened 35 | */ 36 | std::string m_message; 37 | 38 | public: 39 | /** 40 | * @param errorCode The encountered error code 41 | * @param context The context in which this error code has been encountered. This will be embedded in this 42 | * exception's message 43 | */ 44 | PipeException(error_code_t errorCode, const std::string &context) : m_errorCode(errorCode) { 45 | m_message = 46 | std::string("Pipe action \"") + context + "\" returned error code " + std::to_string(m_errorCode); 47 | } 48 | 49 | const char *what() const noexcept { return m_message.c_str(); } 50 | }; 51 | 52 | /** 53 | * An exception thrown when an operation takes longer than allowed 54 | */ 55 | class TimeoutException : public std::exception { 56 | public: 57 | const char *what() const noexcept { return "TimeoutException"; } 58 | }; 59 | 60 | /** 61 | * Wrapper class around working with NamedPipes. Its main purpose is to abstract away the implementation differences 62 | * between different platforms (e.g. Windows vs Posix-compliant systems). 63 | * At the same time it serves as a RAII wrapper. 64 | */ 65 | class NamedPipe : NonCopyable { 66 | private: 67 | /** 68 | * The path to the wrapped pipe 69 | */ 70 | std::filesystem::path m_pipePath; 71 | 72 | #ifdef PLATFORM_WINDOWS 73 | /** 74 | * On Windows this holds the handle to the pipe. On other platforms this variable doesn't exist. 75 | */ 76 | HANDLE m_handle = INVALID_HANDLE_VALUE; 77 | #endif 78 | 79 | /** 80 | * Instantiates this wrapper. On Windows the m_handle member variable has to be set 81 | * explicitly after having constructed this object. 82 | * 83 | * @param The path to the pipe that should be wrapped by this object 84 | */ 85 | explicit NamedPipe(const std::filesystem::path &path); 86 | 87 | public: 88 | /** 89 | * Creates an empty (invalid) instance 90 | */ 91 | NamedPipe() = default; 92 | ~NamedPipe(); 93 | 94 | NamedPipe(NamedPipe &&other); 95 | NamedPipe &operator=(NamedPipe &&other); 96 | 97 | /** 98 | * Creates a new named pipe at the specified location. If such a pipe (or other file) already exists at the 99 | * given location, this function will fail. 100 | * 101 | * @param pipePath The path at which the pipe shall be created 102 | * @returns A NamedPipe object wrapping the newly created pipe 103 | */ 104 | [[nodiscard]] static NamedPipe create(const std::filesystem::path &pipePath); 105 | /** 106 | * Writes a message to the named pipe at the given location 107 | * 108 | * @param pipePath The path at which the pipe is expected to exist. If the pipe does not exist, the function 109 | * will poll for its existance until it times out. 110 | * @param content The messages that should be written to the named pipe 111 | * @param timeout How long this function is allowed to take in milliseconds. Note that the timeout is only 112 | * respected very roughly (especially on Windows) and should therefore rather be used to specify the general 113 | * order of magnitude of the timeout instead of the exact timeout-interval. 114 | */ 115 | static void write(const std::filesystem::path &pipePath, const std::string &content, 116 | unsigned int timeout = 1000); 117 | 118 | /** 119 | * @returns Whether a named pipe at the given path currently exists 120 | */ 121 | static bool exists(const std::filesystem::path &pipePath); 122 | 123 | /** 124 | * Writes to the named pipe wrapped by this object by calling NamedPipe::write 125 | * @content The message that should be written to the named pipe 126 | * @content How long this function is allowed to take in milliseconds. The remarks from NamedPipe::write apply. 127 | * 128 | * @see Mumble::JsonBridge::NamedPipe::write() 129 | */ 130 | void write(const std::string &content, unsigned int timeout = 1000) const; 131 | 132 | /** 133 | * Reads content from the wrapped named pipe. This function will block until there is content available or the 134 | * timeout is over. Once started this function will read all available content until EOF in a single block. 135 | * 136 | * @param timeout How long this function may wait for content. Note that this will not be respected precisely. 137 | * Rather this specifies the general order of magnitude of the timeout. 138 | * @returns The read content 139 | */ 140 | [[nodiscard]] std::string 141 | read_blocking(unsigned int timeout = (std::numeric_limits< unsigned int >::max)()) const; 142 | 143 | /** 144 | * @returns The path of the wrapped named pipe 145 | */ 146 | [[nodiscard]] std::filesystem::path getPath() const noexcept; 147 | 148 | /** 149 | * Destroys the wrapped named pipe. After having called this function the pipe does no 150 | * longer exist in the OS's filesystem and this wrapper will become unusable. 151 | * 152 | * @note This function is called automatically by the object's destructor 153 | * @note Calling this function multiple times is allowed. All but the first invocation are turned into no-opts. 154 | */ 155 | void destroy(); 156 | 157 | /** 158 | * @returns Whether this wrapper is currently in a valid state 159 | */ 160 | operator bool() const noexcept; 161 | }; 162 | 163 | }; // namespace JsonBridge 164 | }; // namespace Mumble 165 | 166 | #endif // MUMBLE_JSONBRIDGE_NAMEDPIPE_H_ 167 | -------------------------------------------------------------------------------- /json_bridge/src/Bridge.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #include "mumble/json_bridge/Bridge.h" 7 | #include "mumble/json_bridge/NamedPipe.h" 8 | #include "mumble/json_bridge/Util.h" 9 | 10 | #include "mumble/json_bridge/messages/Message.h" 11 | #include "mumble/json_bridge/messages/Registration.h" 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | 19 | #include 20 | 21 | #define CHECK_THREAD assert(m_workerThread.get_id() == boost::this_thread::get_id()) 22 | 23 | 24 | #ifdef PLATFORM_WINDOWS 25 | # define PIPE_DIR "\\\\.\\pipe\\" 26 | #else 27 | # define PIPE_DIR "/tmp/" 28 | #endif 29 | 30 | namespace Mumble { 31 | namespace JsonBridge { 32 | 33 | client_id_t Bridge::s_nextClientID = 0; 34 | const std::filesystem::path Bridge::s_pipePath(std::filesystem::path(PIPE_DIR) / ".mumble-json-bridge"); 35 | 36 | Bridge::Bridge(const MumbleAPI &api) : m_api(api) {} 37 | 38 | void Bridge::doStart() { 39 | { 40 | // Make sure that m_workerThread has been assigned properly before accessing it 41 | std::lock_guard< std::mutex > guard(m_startMutex); 42 | CHECK_THREAD; 43 | } 44 | 45 | // Generate a secret that we are going to use 46 | m_secret = Util::generateRandomString(12); 47 | 48 | try { 49 | m_pipe = NamedPipe::create(s_pipePath); 50 | 51 | if (!m_pipe) { 52 | std::cerr << "Error creating pipe" << std::endl; 53 | return; 54 | } 55 | 56 | std::string content; 57 | // Loop until the thread is interrupted 58 | while (true) { 59 | content = m_pipe.read_blocking(); 60 | 61 | try { 62 | nlohmann::json message = nlohmann::json::parse(content); 63 | 64 | processMessage(message); 65 | } catch (const nlohmann::json::parse_error &e) { 66 | std::cerr << "Mumble-JSON-Bridge: Can't parse message: " << e.what() << std::endl; 67 | } catch (const TimeoutException &) { 68 | std::cerr << "Mumble-JSON-Bridge: NamedPipe IO timed out" << std::endl; 69 | } 70 | }; 71 | 72 | std::cout << "Stopping pipe-query" << std::endl; 73 | } catch (const boost::thread_interrupted &) { 74 | // Destroy the client-pipe 75 | m_pipe.destroy(); 76 | 77 | // rethrow 78 | throw; 79 | } catch (const std::exception &e) { 80 | // Destroy the client-pipe 81 | m_pipe.destroy(); 82 | 83 | std::cerr << "Mumble-JSON-Bridge failed: " << e.what() << std::endl; 84 | 85 | // Exit thread 86 | return; 87 | } 88 | } 89 | 90 | void Bridge::processMessage(const nlohmann::json &msg) { 91 | CHECK_THREAD; 92 | 93 | client_id_t id = INVALID_CLIENT_ID; 94 | 95 | try { 96 | Messages::MessageType type; 97 | try { 98 | type = Messages::parseBasicFormat(msg); 99 | } catch (const Messages::InvalidMessageException &) { 100 | // See if the message contains a client_id field as this would allow us to actually return 101 | // an error to the respective client instead of simply writing something to cerr (which the 102 | // client won't see). 103 | if (msg.contains("client_id")) { 104 | id = msg["client_id"].get< client_id_t >(); 105 | } 106 | 107 | // Rethrow original exception 108 | throw; 109 | } 110 | 111 | if (type != Messages::MessageType::REGISTRATION) { 112 | MESSAGE_ASSERT_FIELD(msg, "client_id", number_integer); 113 | 114 | id = msg["client_id"].get< client_id_t >(); 115 | 116 | MESSAGE_ASSERT_FIELD(msg, "secret", string); 117 | 118 | auto it = m_clients.find(id); 119 | 120 | if (it == m_clients.end()) { 121 | throw Messages::InvalidMessageException("Invalid client ID"); 122 | } 123 | 124 | if (!it->second.secretMatches(msg["secret"].get< std::string >())) { 125 | throw Messages::InvalidMessageException("Permission denied (invalid secret)"); 126 | } 127 | } 128 | 129 | switch (type) { 130 | case Messages::MessageType::REGISTRATION: 131 | handleRegistration(Messages::Registration(msg["message"])); 132 | break; 133 | case Messages::MessageType::API_CALL: 134 | handleAPICall(m_clients[id], Messages::APICall(m_api, msg["message"])); 135 | break; 136 | case Messages::MessageType::DISCONNECT: 137 | handleDisconnect(msg); 138 | break; 139 | } 140 | } catch (const Messages::InvalidMessageException &e) { 141 | if (id != INVALID_CLIENT_ID && m_clients.find(id) != m_clients.end()) { 142 | const BridgeClient &client = m_clients[id]; 143 | 144 | // clang-format off 145 | nlohmann::json errorMsg = { 146 | { "response_type", "error" }, 147 | { "secret", m_secret }, 148 | { "response", 149 | { 150 | { "error_message", std::string(e.what()) } 151 | } 152 | } 153 | }; 154 | // clang-format on 155 | 156 | client.write(errorMsg.dump()); 157 | } else { 158 | std::cerr << "Mumble-JSON-Bridge: Got error for unknown client: " << e.what() << std::endl; 159 | } 160 | } 161 | } 162 | 163 | void Bridge::handleRegistration(const Messages::Registration &msg) { 164 | CHECK_THREAD; 165 | 166 | std::error_code errorCode; 167 | if (std::filesystem::exists(msg.m_pipePath, errorCode)) { 168 | client_id_t id = s_nextClientID; 169 | s_nextClientID++; 170 | 171 | m_clients[id] = BridgeClient(msg.m_pipePath, msg.m_secret, id); 172 | 173 | // Tell the client about its assigned ID 174 | // clang-format off 175 | nlohmann::json response = { 176 | { "response_type", "registration" }, 177 | { "secret", m_secret }, 178 | { "response", 179 | { 180 | { "client_id", id } 181 | } 182 | } 183 | }; 184 | // clang-format on 185 | 186 | m_clients[id].write(response.dump()); 187 | } 188 | } 189 | 190 | void Bridge::handleAPICall(const BridgeClient &client, const Messages::APICall &msg) const { 191 | nlohmann::json response = msg.execute(m_secret); 192 | 193 | client.write(response.dump()); 194 | } 195 | 196 | void Bridge::handleDisconnect(const nlohmann::json &msg) { 197 | client_id_t id = msg["client_id"].get< client_id_t >(); 198 | // Move the client out of the list of known clients 199 | BridgeClient client = std::move(m_clients[id]); 200 | m_clients.erase(id); 201 | 202 | 203 | // clang-format off 204 | nlohmann::json response = { 205 | { "response_type", "disconnect" }, 206 | { "secret", m_secret }, 207 | }; 208 | // clang-format on 209 | 210 | client.write(response.dump()); 211 | } 212 | 213 | void Bridge::start() { 214 | // We need a mutex here in order to make sure that doStart won't start using m_workerThread before 215 | // it has been initialized properly by below statement. 216 | std::lock_guard< std::mutex > guard(m_startMutex); 217 | m_workerThread = boost::thread(&Bridge::doStart, this); 218 | } 219 | 220 | void Bridge::stop(bool join) { 221 | m_workerThread.interrupt(); 222 | 223 | if (join) { 224 | m_workerThread.join(); 225 | } 226 | } 227 | 228 | }; // namespace JsonBridge 229 | }; // namespace Mumble 230 | -------------------------------------------------------------------------------- /scripts/generate_CLI_operations.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright 2020 The Mumble Developers. All rights reserved. 4 | # Use of this source code is governed by a BSD-style license 5 | # that can be found in the LICENSE file at the root of the 6 | # Mumble source tree or at . 7 | 8 | import yaml 9 | import argparse 10 | import os 11 | from datetime import datetime 12 | 13 | def generateLicenseHeader(): 14 | currentYear = datetime.today().strftime("%Y") 15 | 16 | licenseHeader = "// Copyright " + (currentYear + "-" if not currentYear == "2020" else "") + "2020 The Mumble Developers. All rights reserved.\n" 17 | licenseHeader += "// Use of this source code is governed by a BSD-style license\n" 18 | licenseHeader += "// that can be found in the LICENSE file at the root of the\n" 19 | licenseHeader += "// source tree.\n" 20 | 21 | return licenseHeader 22 | 23 | 24 | def jsonTypeToCppType(jsonType): 25 | if jsonType == "string": 26 | return "std::string" 27 | elif jsonType == "number": 28 | return "double" 29 | elif jsonType == "number_integer": 30 | return "int" 31 | elif jsonType == "number_unsigned": 32 | return "uint64_t" 33 | elif jsonType == "number_float": 34 | return "double" 35 | elif jsonType == "boolean": 36 | return "bool" 37 | 38 | raise RuntimeError("Unable to convert \"%s\" from JSON to cpp" % jsonType) 39 | 40 | 41 | def generateAPIResponseCheck(): 42 | generatedCode = "void checkAPIResponse(const nlohmann::json &response) {\n" 43 | generatedCode += "\tif (response.contains(\"response_type\") && response[\"response_type\"].get() == \"api_call\") {\n" 44 | generatedCode += "\t\t// All good\n" 45 | generatedCode += "\t\treturn;\n" 46 | generatedCode += "\t}\n" 47 | generatedCode += "\t\n" 48 | generatedCode += "\t// There seems to have been an error\n" 49 | generatedCode += "\tif (!response.contains(\"response_type\") || !response.contains(\"response\")) {\n" 50 | generatedCode += "\t\t// We can't process this response - seems invalid\n" 51 | generatedCode += "\t\tthrow OperationException(\"Got invalid response from Mumble-JSON-Bridge.\");\n" 52 | generatedCode += "\t}\n" 53 | generatedCode += "\t\n" 54 | generatedCode += "\tif (response[\"response_type\"].get() == \"api_error\"\n" 55 | generatedCode += "\t\t|| response[\"response_type\"].get() == \"api_error_optional\"\n" 56 | generatedCode += "\t\t|| response[\"response_type\"].get() == \"error\") {\n" 57 | generatedCode += "\t\tthrow OperationException(response[\"response\"][\"error_message\"].get());\n" 58 | generatedCode += "\t} else {\n" 59 | generatedCode += "\t\tthrow OperationException(\"Generic API error ecountered\");\n" 60 | generatedCode += "\t}\n" 61 | generatedCode += "}\n" 62 | 63 | return generatedCode 64 | 65 | 66 | 67 | def generateParameterProcessing(parameter, messageName, operationName): 68 | generatedCode = "// Validate and extract parameter\n" 69 | generatedCode += "\n" 70 | 71 | requiredParamCount = 0 72 | for currentParam in parameter: 73 | if not "default" in currentParam: 74 | requiredParamCount += 1 75 | 76 | # verify that the correct amount of parameter is provided 77 | generatedCode += "MESSAGE_ASSERT_FIELD(" + messageName + ", \"parameter\", object);\n" 78 | generatedCode += "\n" 79 | generatedCode += "const nlohmann::json &operationParams = " + messageName + "[\"parameter\"];\n" 80 | generatedCode += "\n" 81 | generatedCode += "if (operationParams.size() < " + str(requiredParamCount) + ") {\n" 82 | generatedCode += "\tthrow ::Mumble::JsonBridge::Messages::InvalidMessageException(std::string(\"Operation \\\"" + operationName \ 83 | + "\\\" expects at least " + str(requiredParamCount) + " parameter, but was provided with \") + std::to_string(operationParams.size()));\n" 84 | generatedCode += "}\n" 85 | generatedCode += "\n" 86 | 87 | # Check and extract actual parameter 88 | for currentParam in parameter: 89 | paramName = currentParam["name"] 90 | paramType = currentParam["type"] 91 | 92 | if "default" in currentParam: 93 | generatedCode += jsonTypeToCppType(paramType) + " " + paramName + " = " + currentParam["default"] + ";\n" 94 | generatedCode += "if (operationParams.contains(\"" + paramName + "\")) {\n" 95 | generatedCode += "\t// Validate the param type\n" 96 | generatedCode += "\tMESSAGE_ASSERT_FIELD(operationParams, \"" + paramName + "\", " + paramType + ");\n" 97 | generatedCode += "\t" + jsonTypeToCppType(paramType) + " " + paramName + " = operationParams[\"" + paramName + "\"].get<"\ 98 | + jsonTypeToCppType(paramType) + ">();\n" 99 | generatedCode += "}\n" 100 | else: 101 | generatedCode += "MESSAGE_ASSERT_FIELD(operationParams, \"" + paramName + "\", " + paramType + ");\n" 102 | generatedCode += jsonTypeToCppType(paramType) + " " + paramName + " = operationParams[\"" + paramName + "\"].get<"\ 103 | + jsonTypeToCppType(paramType) + ">();\n" 104 | generatedCode += "\n" 105 | 106 | return generatedCode 107 | 108 | 109 | def generateAPIFunctionCall(queryName, responseName, functionName, parameter): 110 | generatedCode = "// clang-format off\n" 111 | generatedCode += "nlohmann::json " + queryName + " = {\n" 112 | generatedCode += "\t{ \"message_type\", \"api_call\" },\n" 113 | generatedCode += "\t{ \"message\",\n" 114 | generatedCode += "\t\t{\n" 115 | generatedCode += "\t\t\t{ \"function\", \"" + functionName + "\" }" 116 | 117 | if len(parameter) > 0: 118 | generatedCode += ",\n" 119 | generatedCode += "\t\t\t{ \"parameter\",\n" 120 | generatedCode += "\t\t\t\t{\n" 121 | 122 | for currentParam in parameter: 123 | value = currentParam["value"] 124 | if not value: 125 | # convert empty to empty string 126 | value = "\"\"" 127 | 128 | if type(value) == bool: 129 | # Make sure that booleans are used in a c-compatible way 130 | value = "true" if value else "false" 131 | 132 | assert type(value) == type("") 133 | generatedCode += "\t\t\t\t\t{ \"" + currentParam["name"] + "\", " + value + " },\n" 134 | 135 | # remove trailing ",\n" 136 | generatedCode = generatedCode[ : -2] 137 | generatedCode += "\n" 138 | generatedCode += "\t\t\t\t}\n" 139 | 140 | generatedCode += "\t\t\t}\n" 141 | else: 142 | generatedCode += "\n" 143 | 144 | generatedCode += "\t\t}\n" 145 | generatedCode += "\t}\n" 146 | generatedCode += "};\n" 147 | generatedCode += "// clang-format on\n" 148 | generatedCode += "\n" 149 | generatedCode += "nlohmann::json " + responseName + " = executeQuery(" + queryName + ");\n" 150 | 151 | return generatedCode 152 | 153 | 154 | def generateFunctionAssignment(varName, varType, functionSpec, counter): 155 | generatedCode = "" 156 | 157 | if functionSpec["type"] == "api": 158 | generatedCode += generateAPIFunctionCall("query" + str(counter), "response" + str(counter), functionSpec["name"], \ 159 | functionSpec["parameter"] if "parameter" in functionSpec else []) 160 | generatedCode += "\n" 161 | generatedCode += "checkAPIResponse(response" + str(counter) + ");\n" 162 | generatedCode += jsonTypeToCppType(varType) + " " + varName + " = response" + str(counter) + "[\"response\"][\"return_value\"].get<" \ 163 | + jsonTypeToCppType(varType) + ">();\n" 164 | elif functionSpec["type"] == "regular": 165 | generatedCode += jsonTypeToCppType(varType) + " " + varName + " = " + functionSpec["name"] + "(" 166 | 167 | if "parameter" in functionSpec: 168 | for currentParam in functionSpec["parameter"]: 169 | generatedCode += currentParam["value"] + ", " 170 | 171 | # Remove trailing ", " 172 | generatedCode = generatedCode[ : -2] 173 | 174 | generatedCode += ");\n" 175 | else: 176 | raise RuntimeError("Unknown function type %s" % functionSpec["type"]) 177 | 178 | return generatedCode 179 | 180 | 181 | def generateDepencyProcessing(dependencies, operationName): 182 | generatedCode = "// Obtain all needed values\n" 183 | generatedCode += "\n" 184 | 185 | # Handle dependencies one at a time 186 | counter = 1 187 | for currentDep in dependencies: 188 | depName = currentDep["name"] 189 | depType = currentDep["type"] 190 | 191 | generatedCode += generateFunctionAssignment(depName, depType, currentDep["function"], counter).strip() + "\n" 192 | generatedCode += "\n" 193 | counter += 1 194 | 195 | return generatedCode 196 | 197 | 198 | def generateExecuteStatement(execSpec): 199 | if not "function" in execSpec: 200 | raise RuntimeError("Missing \"function\" spec in \"executes\" statement") 201 | 202 | functionSpec = execSpec["function"] 203 | 204 | if not functionSpec["type"] == "api": 205 | raise RuntimeError("Currently only API function calls are supported for operation-executes statements") 206 | 207 | generatedCode = generateAPIFunctionCall("operationQuery", "result", \ 208 | functionSpec["name"], functionSpec["parameter"] if "parameter" in functionSpec else []).strip() + "\n" 209 | generatedCode += "\n" 210 | generatedCode += "return result;" 211 | 212 | return generatedCode 213 | 214 | 215 | def generatedDelegateFunction(operations): 216 | generatedCode = "nlohmann::json handleOperation(const nlohmann::json &msg,\n" 217 | generatedCode += "\tconst std::function &executeQuery) {\n" 218 | generatedCode += "\tif (!msg.contains(\"operation\") || !msg[\"operation\"].is_string()) {\n" 219 | generatedCode += "\t\tthrow OperationException(\"Missing \\\"operation\\\" field (required to be of type string)\");\n" 220 | generatedCode += "\t}\n" 221 | generatedCode += "\n" 222 | 223 | for i in range(len(operations)): 224 | if i == 0: 225 | generatedCode += "\tif " 226 | else: 227 | generatedCode += " else if " 228 | 229 | generatedCode += "(msg[\"operation\"].get() == \"" + operations[i] + "\") {\n" 230 | generatedCode += "\t\treturn handle_" + operations[i] + "_operation(msg, executeQuery);\n" 231 | generatedCode += "\t}" 232 | 233 | generatedCode += " else {\n" 234 | generatedCode += "\t\tthrow OperationException(std::string(\"Unknown operation \\\"\") + msg[\"operation\"].get() + \"\\\"\");\n" 235 | generatedCode += "\t}\n" 236 | generatedCode += "}\n" 237 | 238 | return generatedCode 239 | 240 | 241 | def main(): 242 | parser = argparse.ArgumentParser(description="Generates the implementation for the handle_*_operation functions") 243 | parser.add_argument("-i", "--input-dir", help="The path to directory containing the operation YAML files") 244 | parser.add_argument("-o", "--output-file", help="Path to which the generated source code shall be written") 245 | 246 | args = parser.parse_args() 247 | 248 | generatedCode = generateLicenseHeader() 249 | generatedCode += "\n" 250 | generatedCode += "// This file was auto-generated by scripts/generate_CLI_operations.py. DO NOT EDIT MANUALLY!\n" 251 | generatedCode += "\n" 252 | 253 | generatedCode += "#include \"handleOperation.h\"\n" 254 | generatedCode += "\n" 255 | generatedCode += "#include \n" 256 | generatedCode += "\n" 257 | 258 | generatedCode += generateAPIResponseCheck() 259 | generatedCode += "\n" 260 | 261 | operations = [] 262 | 263 | if args.input_dir is None: 264 | print("[ERROR]: No input-dir given") 265 | return 266 | 267 | for currentFile in os.listdir(args.input_dir): 268 | fileName = os.fsdecode(currentFile) 269 | 270 | if not fileName.endswith(".yaml"): 271 | print("Skipping \"%s\"" % fileName) 272 | continue 273 | 274 | with open(os.path.join(args.input_dir, fileName), "r") as definitionFile: 275 | documents = yaml.full_load(definitionFile) 276 | 277 | for currentOp in documents["operations"]: 278 | operationName = currentOp["operation"] 279 | operations.append(operationName) 280 | 281 | generatedCode += "nlohmann::json handle_" + operationName + "_operation(const nlohmann::json &msg,\n" 282 | generatedCode += "\tconst std::function &executeQuery) {\n" 283 | 284 | if "parameter" in currentOp: 285 | # Handle the parameter that the JSON message might contain 286 | generatedCode += "\t" + generateParameterProcessing(currentOp["parameter"], "msg", operationName).replace("\n", "\n\t").strip() + "\n" 287 | else: 288 | # make sure there are no parameter given as none are expected 289 | generatedCode += "\tif (msg.contains(\"parameter\")) {\n" 290 | generatedCode += "\t\t::Mumble::JsonBridge::Messages::InvalidMessageException(\"Operation \\\"" + operationName \ 291 | + "\\\" does not take any parameter\");\n" 292 | generatedCode += "\t}\n" 293 | 294 | generatedCode += "\n" 295 | 296 | if "depends" in currentOp: 297 | generatedCode += "\t" + generateDepencyProcessing(currentOp["depends"], operationName).replace("\n", "\n\t").strip() + "\n" 298 | 299 | generatedCode += "\n" 300 | generatedCode += "\n" 301 | 302 | generatedCode += "\t// Now actually execute the operation\n" 303 | generatedCode += "\t" + generateExecuteStatement(currentOp["executes"]).replace("\n", "\n\t").strip() + "\n" 304 | 305 | generatedCode += "}\n" 306 | generatedCode += "\n" 307 | 308 | # Generate function for delegating 309 | generatedCode += generatedDelegateFunction(operations) 310 | 311 | 312 | if not args.output_file is None: 313 | outFile = open(args.output_file, "w") 314 | outFile.write(generatedCode) 315 | else: 316 | print(generatedCode) 317 | 318 | 319 | if __name__ == "__main__": 320 | main() 321 | -------------------------------------------------------------------------------- /scripts/generate_APICall_implementation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright 2020 The Mumble Developers. All rights reserved. 4 | # Use of this source code is governed by a BSD-style license 5 | # that can be found in the LICENSE file at the root of the 6 | # Mumble source tree or at . 7 | 8 | import re 9 | import argparse 10 | from datetime import datetime 11 | 12 | class Parameter: 13 | def __init__(self, name, paramType): 14 | self.m_name = name 15 | self.m_type = paramType 16 | 17 | def camel_to_snake(name): 18 | converted = "" 19 | prevConverted = False 20 | for c in name: 21 | if c.isupper(): 22 | if prevConverted: 23 | converted += c.lower() 24 | else: 25 | prevConverted = True 26 | converted += "_" + c.lower() 27 | else: 28 | prevConverted = False 29 | converted += c 30 | 31 | return converted.lstrip("_") 32 | 33 | def getJsonType(cppType): 34 | if cppType == "const char *" or cppType == "std::string" or cppType == "MumbleString": 35 | return "string" 36 | elif cppType == "int": 37 | return "number_integer" 38 | elif cppType == "unsigned int" or cppType == "uint8_t" or cppType == "uint16_t" or cppType == "uint32_t": 39 | return "number_unsigned" 40 | elif cppType == "double" or cppType == "float": 41 | return "number_float" 42 | elif cppType == "mumble_error_t": 43 | return "number_unsigned" 44 | elif cppType == "bool": 45 | return "boolean" 46 | elif cppType == "mumble_connection_t": 47 | return "number_integer" 48 | elif cppType == "mumble_userid_t": 49 | return "number_unsigned" 50 | elif cppType == "mumble_channelid_t": 51 | return "number_integer" 52 | elif cppType == "mumble_transmission_mode_t": 53 | return "string" 54 | elif cppType == "mumble_settings_key_t": 55 | return "string" 56 | elif "std::vector" in cppType: 57 | return "array" 58 | elif cppType.endswith("*"): 59 | return "number_unsigned" 60 | 61 | raise RuntimeError("Unable to convert cpp-type " + cppType) 62 | 63 | def generateLicenseHeader(): 64 | currentYear = datetime.today().strftime("%Y") 65 | 66 | licenseHeader = "// Copyright " + (currentYear + "-" if not currentYear == "2020" else "") + "2020 The Mumble Developers. All rights reserved.\n" 67 | licenseHeader += "// Use of this source code is governed by a BSD-style license\n" 68 | licenseHeader += "// that can be found in the LICENSE file at the root of the\n" 69 | licenseHeader += "// source tree.\n" 70 | 71 | return licenseHeader 72 | 73 | def generateSetInit(name, elements): 74 | init = "const std::unordered_set APICall::" + name + " = {\n" 75 | for current in elements: 76 | init += "\t\"" + current + "\",\n" 77 | 78 | # remove last ",\n" 79 | init = init[0 : -2] 80 | init += "\n};" 81 | 82 | return init 83 | 84 | def generateExecuteFunction(functionNames, functionsWithoutParameters): 85 | func = "nlohmann::json execute(const std::string &functionName, const MumbleAPI &api, const std::string &bridgeSecret, "\ 86 | + "const nlohmann::json &msg) {\n" 87 | 88 | for i in range(len(functionNames)): 89 | if i == 0: 90 | func += "\tif (" 91 | else: 92 | func += " else if (" 93 | 94 | func += "functionName == \"" + functionNames[i] + "\") {\n" 95 | func += "\t\treturn handle_" + functionNames[i] + "(api, bridgeSecret" 96 | if not functionNames[i] in functionsWithoutParameters: 97 | func += ", msg[\"parameter\"]" 98 | func += ");\n" 99 | func += "\t}" 100 | 101 | func += "\n\n" 102 | func += "\tthrow std::invalid_argument(std::string(\"Unknown API function \\\"\") + functionName + \"\\\"\");\n" 103 | func += "}" 104 | 105 | return func 106 | 107 | def main(): 108 | parser = argparse.ArgumentParser(description="Generates the implementation for the handle_* functions of the APICall class") 109 | parser.add_argument("-i", "--api-header", help="The path to the C++ API-wrapper header-file") 110 | parser.add_argument("-o", "--output-file", help="Path to which the generated source code shall be written") 111 | 112 | args = parser.parse_args() 113 | 114 | # read header and trim to relevant content 115 | apiHeader = open(args.api_header, "r").read() 116 | apiHeader = apiHeader[apiHeader.find("~MumbleAPI();") + 13 : apiHeader.rfind("};")] 117 | 118 | generatedImpl = generateLicenseHeader() 119 | 120 | generatedImpl += "\n" 121 | 122 | generatedImpl += "// This file was auto-generated by scripts/generate_APICall_implementation.py. DO NOT EDIT MANUALLY!\n" 123 | 124 | generatedImpl += "\n" 125 | 126 | # Add place-holder for init of static member variables containing function names 127 | generatedImpl += "%s\n\n" 128 | 129 | 130 | functionNames = [] 131 | noParamFunctionNames = [] 132 | 133 | functionPattern = re.compile("((?:\w|:|\<[^>]*\>)+)\s*(\w+)\s*\(([^)]*)\)\s*(.*)") 134 | functions = apiHeader.split(";") 135 | for currentFunction in functions: 136 | currentFunction = currentFunction.strip().replace("\n", " ").replace("\t", " ") 137 | # Trim multiple consecutive spaces 138 | currentFunction = re.sub(" +", " ", currentFunction) 139 | 140 | if not currentFunction: 141 | continue 142 | 143 | match = functionPattern.match(currentFunction) 144 | if not match: 145 | raise RuntimeError("Function RegEx doesn't work") 146 | 147 | returnType = match.group(1) 148 | functionName = match.group(2) 149 | parameterList = match.group(3).split(",") 150 | modifiers = match.group(4) 151 | 152 | parameter = [] 153 | 154 | for currentParam in parameterList: 155 | currentParam = currentParam.strip() 156 | 157 | if not currentParam: 158 | continue 159 | 160 | # Strip defaul arguments 161 | if "=" in currentParam: 162 | currentParam = currentParam[0 : currentParam.find("=")].strip() 163 | 164 | match = re.match(".*?(\w+)$", currentParam) 165 | if not match: 166 | raise RuntimeError("Parameter RegEx not working") 167 | 168 | paramName = match.group(1) 169 | paramType = currentParam[0 : -len(paramName)].strip() 170 | 171 | # convert param name to snake_case in order to fit in the JSON naming scheme 172 | paramName = camel_to_snake(paramName) 173 | 174 | parameter.append(Parameter(paramName, paramType)) 175 | 176 | functionNames.append(functionName) 177 | if len(parameter) == 0: 178 | noParamFunctionNames.append(functionName); 179 | 180 | generatedFunction = "nlohmann::json handle_" + functionName + "(const MumbleAPI &api, const std::string &bridgeSecret" 181 | 182 | if len(parameter) > 0: 183 | generatedFunction += ", const nlohmann::json ¶meter" 184 | 185 | generatedFunction += ") {\n" 186 | 187 | # Generate verification code 188 | if len(parameter) > 0: 189 | generatedFunction += "\t// Validate specified parameter\n" 190 | generatedFunction += "\tif (parameter.size() != " + str(len(parameter)) + ") {\n" 191 | generatedFunction += "\t\tthrow InvalidMessageException(std::string(\"API function \\\"" + functionName + "\\\" expects " \ 192 | + str(len(parameter)) + " parameter(s) but got \") + std::to_string(parameter.size()));\n" 193 | generatedFunction += "\t}\n" 194 | 195 | for currentParam in parameter: 196 | generatedFunction += "\tMESSAGE_ASSERT_FIELD(parameter, \"" + currentParam.m_name + "\", " + getJsonType(currentParam.m_type) + ");\n" 197 | 198 | generatedFunction += "\n" 199 | if len(parameter) > 0: 200 | generatedFunction += "\t// Convert the parameter from JSON to the corresponding cpp types\n" 201 | 202 | # convert JSON to cpp 203 | for currentParam in parameter: 204 | paramType = currentParam.m_type 205 | # Use std::string for const char * types 206 | if paramType == "const char *": 207 | paramType = "std::string" 208 | # Remove const 209 | if paramType.startswith("const"): 210 | paramType = paramType[len("const"): ].strip() 211 | # Remove reference 212 | if paramType.endswith("&"): 213 | paramType = paramType[ : -1].strip() 214 | 215 | if not "*" in paramType: 216 | # Non-pointers 217 | generatedFunction += "\t" + paramType + " " + currentParam.m_name + " = parameter[\"" + currentParam.m_name + "\"].get<"\ 218 | + paramType + ">();\n" 219 | else: 220 | # pointers 221 | generatedFunction += "\t" + paramType + " " + currentParam.m_name + " = reinterpret_cast<" + paramType + \ 222 | ">(parameter[\"" + currentParam.m_name + "\"].get<" + "uintptr_t" + ">());\n" 223 | 224 | generatedFunction += "\n" 225 | generatedFunction += "\t// Call respective API function" 226 | if len(parameter) > 0: 227 | generatedFunction += " with extracted parameter" 228 | generatedFunction += "\n" 229 | 230 | # call API with extracted parameter 231 | generatedFunction += "\tnlohmann::json response;\n" 232 | 233 | generatedFunction += "\n" 234 | 235 | indent = "\t" 236 | if not "noexcept" in modifiers or "std::optional" in returnType: 237 | generatedFunction += "\ttry {\n" 238 | indent = "\t\t" 239 | 240 | generatedFunction += indent 241 | if not returnType == "void": 242 | generatedFunction += returnType + " ret = " 243 | 244 | generatedFunction += "api." + functionName + "(" 245 | 246 | for currentParam in parameter: 247 | generatedFunction += currentParam.m_name 248 | if currentParam.m_type == "const char *": 249 | generatedFunction += ".c_str()" 250 | 251 | generatedFunction += ", " 252 | 253 | if len(parameter) > 0: 254 | # remove extra ", " at the end 255 | generatedFunction = generatedFunction[ : -2] 256 | 257 | generatedFunction += ");\n" 258 | 259 | generatedFunction += "\n" 260 | 261 | # report result 262 | generatedFunction += indent + "// clang-format off\n" 263 | generatedFunction += indent + "response = {\n" 264 | generatedFunction += indent + "\t{\"response_type\", \"api_call\"},\n" 265 | generatedFunction += indent + "\t{\"secret\", bridgeSecret},\n" 266 | generatedFunction += indent + "\t{\"response\",\n" 267 | generatedFunction += indent + "\t\t{\n" 268 | generatedFunction += indent + "\t\t\t{\"function\", \"" + functionName + "\"},\n" 269 | generatedFunction += indent + "\t\t\t{\"status\", \"executed\"}" 270 | if not returnType == "void": 271 | if not "std::optional" in returnType: 272 | generatedFunction += ",\n" + indent + "\t\t\t{\"return_value\", ret}" 273 | else: 274 | generatedFunction += ",\n" + indent + "\t\t\t{\"return_value\", ret.value()}" 275 | generatedFunction += "\n" + indent + "\t\t}\n" 276 | generatedFunction += indent + "\t}\n" 277 | generatedFunction += indent + "};\n" 278 | generatedFunction += indent + "// clang-format on\n" 279 | 280 | 281 | if not "noexcept" in modifiers or "std::optional" in returnType: 282 | generatedFunction += "\t}" 283 | 284 | if not "noexcept" in modifiers: 285 | generatedFunction += " catch (const MumbleAPIException &e) {\n" 286 | generatedFunction += "\t\t// clang-format off\n" 287 | generatedFunction += "\t\tresponse = {\n" 288 | generatedFunction += "\t\t\t{\"response_type\", \"api_error\"},\n" 289 | generatedFunction += "\t\t\t{\"secret\", bridgeSecret},\n" 290 | generatedFunction += "\t\t\t{\"response\",\n" 291 | generatedFunction += "\t\t\t\t{\n" 292 | generatedFunction += "\t\t\t\t\t{\"error_code\", e.errorCode()},\n" 293 | generatedFunction += "\t\t\t\t\t{\"error_message\", e.what()}\n" 294 | generatedFunction += "\t\t\t\t}\n" 295 | generatedFunction += "\t\t\t}\n" 296 | generatedFunction += "\t\t};\n" 297 | generatedFunction += "\t\t// clang-format on\n" 298 | generatedFunction += "\t}" 299 | 300 | if "std::optional" in returnType: 301 | generatedFunction += " catch (const std::bad_optional_access &) {\n" 302 | generatedFunction += "\t\t// clang-format off\n" 303 | generatedFunction += "\t\tresponse = {\n" 304 | generatedFunction += "\t\t\t{\"response_type\", \"api_error_optional\"},\n" 305 | generatedFunction += "\t\t\t{\"secret\", bridgeSecret},\n" 306 | generatedFunction += "\t\t\t{\"response\",\n" 307 | generatedFunction += "\t\t\t\t{\n" 308 | generatedFunction += "\t\t\t\t\t{\"error_message\", \"Optional value not present\"},\n" 309 | generatedFunction += "\t\t\t\t}\n" 310 | generatedFunction += "\t\t\t}\n" 311 | generatedFunction += "\t\t};\n" 312 | generatedFunction += "\t\t// clang-format on\n" 313 | generatedFunction += "\t}" 314 | 315 | generatedFunction += "\n" 316 | 317 | generatedFunction += "\n" 318 | 319 | generatedFunction += "\treturn response;\n" 320 | 321 | generatedFunction += "}" 322 | 323 | generatedImpl += generatedFunction 324 | generatedImpl += "\n\n" 325 | 326 | 327 | 328 | initCode = generateSetInit("s_allFunctions", functionNames) + "\n\n" 329 | initCode += generateSetInit("s_noParamFunctions", noParamFunctionNames) 330 | 331 | generatedImpl = generatedImpl % initCode 332 | 333 | generatedImpl += generateExecuteFunction(functionNames, noParamFunctionNames) + "\n\n" 334 | 335 | if args.output_file is None: 336 | # print to standard output 337 | print(generatedImpl) 338 | else: 339 | outFile = open(args.output_file, "w") 340 | outFile.write(generatedImpl) 341 | 342 | 343 | 344 | 345 | if __name__ == "__main__": 346 | main() 347 | -------------------------------------------------------------------------------- /json_bridge/src/NamedPipe.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #include "mumble/json_bridge/NamedPipe.h" 7 | #include "mumble/json_bridge/MumbleAssert.h" 8 | #include "mumble/json_bridge/NonCopyable.h" 9 | 10 | #include 11 | 12 | #ifdef PLATFORM_UNIX 13 | # include 14 | # include 15 | # include 16 | # include 17 | #endif 18 | 19 | #ifdef PLATFORM_WINDOWS 20 | # include 21 | #endif 22 | 23 | #include 24 | #include 25 | 26 | namespace Mumble { 27 | namespace JsonBridge { 28 | template< typename handle_t, typename close_handle_function_t, handle_t invalid_handle, int successCode > 29 | class FileHandleWrapper : NonCopyable { 30 | private: 31 | handle_t m_handle; 32 | close_handle_function_t m_closeFunc; 33 | 34 | public: 35 | explicit FileHandleWrapper(handle_t handle, close_handle_function_t closeFunc) 36 | : m_handle(handle), m_closeFunc(closeFunc) {} 37 | 38 | explicit FileHandleWrapper() : m_handle(invalid_handle) {} 39 | 40 | ~FileHandleWrapper() { 41 | if (m_handle != invalid_handle) { 42 | if (m_closeFunc(m_handle) != successCode) { 43 | std::cerr << "Failed at closing guarded handle" << std::endl; 44 | } 45 | } 46 | } 47 | 48 | // Be on the safe-side and delete move-constructor as it isn't explicitly implemented 49 | FileHandleWrapper(FileHandleWrapper &&) = delete; 50 | 51 | // Move-assignment operator 52 | FileHandleWrapper &operator=(FileHandleWrapper &&other) { 53 | // Move handle 54 | m_handle = other.m_handle; 55 | other.m_handle = invalid_handle; 56 | 57 | // Copy over the close-func in case this instance was created using the default 58 | // constructor in which case the close-func is not specified. 59 | m_closeFunc = other.m_closeFunc; 60 | 61 | return *this; 62 | } 63 | 64 | handle_t &get() { return m_handle; } 65 | 66 | operator handle_t() { return m_handle; } 67 | 68 | bool operator==(handle_t other) { return m_handle == other; } 69 | bool operator!=(handle_t other) { return m_handle != other; } 70 | operator bool() { return m_handle != invalid_handle; } 71 | }; 72 | 73 | constexpr int PIPE_WAIT_INTERVAL = 10; 74 | constexpr int PIPE_WRITE_WAIT_INTERVAL = 5; 75 | constexpr int PIPE_BUFFER_SIZE = 32; 76 | 77 | #ifdef PLATFORM_UNIX 78 | using handle_t = FileHandleWrapper< int, int (*)(int), -1, 0 >; 79 | 80 | NamedPipe NamedPipe::create(const std::filesystem::path &pipePath) { 81 | std::error_code errorCode; 82 | MUMBLE_ASSERT(std::filesystem::is_directory(pipePath.parent_path(), errorCode) && !errorCode); 83 | 84 | // Create fifo that only the same user can read & write 85 | if (mkfifo(pipePath.c_str(), S_IRUSR | S_IWUSR) != 0) { 86 | throw PipeException< int >(errno, "Create"); 87 | } 88 | 89 | return NamedPipe(pipePath); 90 | } 91 | 92 | void NamedPipe::write(const std::filesystem::path &pipePath, const std::string &content, unsigned int timeout) { 93 | handle_t handle; 94 | do { 95 | handle = handle_t(::open(pipePath.c_str(), O_WRONLY | O_NONBLOCK), &::close); 96 | 97 | if (!handle) { 98 | if (timeout > PIPE_WRITE_WAIT_INTERVAL) { 99 | timeout -= PIPE_WRITE_WAIT_INTERVAL; 100 | boost::this_thread::sleep_for(boost::chrono::milliseconds(PIPE_WRITE_WAIT_INTERVAL)); 101 | } else { 102 | throw TimeoutException(); 103 | } 104 | } 105 | } while (!handle); 106 | 107 | if (::write(handle, content.c_str(), content.size()) < 0) { 108 | throw PipeException< int >(errno, "Write"); 109 | } 110 | } 111 | 112 | bool NamedPipe::exists(const std::filesystem::path &pipePath) { 113 | // We don't explicitly check whether the given path is a pipe or a regular file 114 | return std::filesystem::exists(pipePath); 115 | } 116 | 117 | std::string NamedPipe::read_blocking(unsigned int timeout) const { 118 | std::string content; 119 | 120 | handle_t handle(::open(m_pipePath.c_str(), O_RDONLY | O_NONBLOCK), &::close); 121 | 122 | if (handle == -1) { 123 | throw PipeException< int >(errno, "Open"); 124 | } 125 | 126 | pollfd pollData = { handle, POLLIN, 0 }; 127 | while (::poll(&pollData, 1, PIPE_WAIT_INTERVAL) != -1 && !(pollData.revents & POLLIN)) { 128 | // Check if the thread has been interrupted 129 | boost::this_thread::interruption_point(); 130 | 131 | if (timeout > PIPE_WAIT_INTERVAL) { 132 | timeout -= PIPE_WAIT_INTERVAL; 133 | } else { 134 | throw TimeoutException(); 135 | } 136 | } 137 | 138 | char buffer[PIPE_BUFFER_SIZE]; 139 | 140 | ssize_t readBytes; 141 | while ((readBytes = ::read(handle, &buffer, PIPE_BUFFER_SIZE)) > 0) { 142 | content.append(buffer, readBytes); 143 | } 144 | 145 | // 0 Means there is no more input, negative numbers indicate errors 146 | // If the error simply is EAGAIN this means that the message has been read completely 147 | // and a request for further data would block (since atm there is no more data available). 148 | if (readBytes == -1 && errno != EAGAIN) { 149 | throw PipeException< int >(errno, "Read"); 150 | } 151 | 152 | return content; 153 | } 154 | #endif 155 | 156 | #ifdef PLATFORM_WINDOWS 157 | using handle_t = FileHandleWrapper< HANDLE, decltype(&CloseHandle), INVALID_HANDLE_VALUE, true >; 158 | 159 | void waitOnAsyncIO(HANDLE handle, LPOVERLAPPED overlappedPtr, unsigned int &timeout) { 160 | constexpr unsigned int pendingWaitInterval = 10; 161 | 162 | DWORD transferedBytes; 163 | BOOL result; 164 | while (!(result = GetOverlappedResult(handle, overlappedPtr, &transferedBytes, FALSE)) 165 | && GetLastError() == ERROR_IO_INCOMPLETE) { 166 | if (timeout > pendingWaitInterval) { 167 | timeout -= pendingWaitInterval; 168 | } else { 169 | throw TimeoutException(); 170 | } 171 | 172 | boost::this_thread::sleep_for(boost::chrono::milliseconds(pendingWaitInterval)); 173 | } 174 | 175 | if (!result) { 176 | throw PipeException< DWORD >(GetLastError(), "Waiting for pending IO"); 177 | } 178 | } 179 | 180 | NamedPipe NamedPipe::create(const std::filesystem::path &pipePath) { 181 | MUMBLE_ASSERT(pipePath.parent_path() == "\\\\.\\pipe"); 182 | 183 | HANDLE pipeHandle = CreateNamedPipe(pipePath.string().c_str(), 184 | PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE, 185 | PIPE_TYPE_BYTE | PIPE_WAIT, 186 | 1, // # of allowed pipe instances 187 | 0, // Size of outbound buffer 188 | 0, // Size of inbound buffer 189 | 0, // Use default wait time 190 | NULL // Use default security attributes 191 | ); 192 | 193 | if (pipeHandle == INVALID_HANDLE_VALUE) { 194 | throw PipeException< DWORD >(GetLastError(), "Create"); 195 | } 196 | 197 | NamedPipe pipe(pipePath); 198 | pipe.m_handle = pipeHandle; 199 | 200 | return pipe; 201 | } 202 | 203 | void NamedPipe::write(const std::filesystem::path &pipePath, const std::string &content, unsigned int timeout) { 204 | MUMBLE_ASSERT(pipePath.parent_path() == "\\\\.\\pipe"); 205 | 206 | while (true) { 207 | // We can't use a timeout of 0 as this would be the special value NMPWAIT_USE_DEFAULT_WAIT causing 208 | // the function to use a default wait-time 209 | if (!WaitNamedPipe(pipePath.string().c_str(), 1)) { 210 | if (GetLastError() == ERROR_FILE_NOT_FOUND || GetLastError() == ERROR_SEM_TIMEOUT) { 211 | if (timeout > PIPE_WRITE_WAIT_INTERVAL) { 212 | timeout -= PIPE_WRITE_WAIT_INTERVAL; 213 | } else { 214 | throw TimeoutException(); 215 | } 216 | 217 | // Decrease wait intverval by 1ms as this is the timeout we have waited on the pipe above already 218 | boost::this_thread::sleep_for(boost::chrono::milliseconds(PIPE_WRITE_WAIT_INTERVAL - 1)); 219 | } else { 220 | throw PipeException< DWORD >(GetLastError(), "WaitNamedPipe"); 221 | } 222 | } else { 223 | break; 224 | } 225 | } 226 | 227 | handle_t handle( 228 | CreateFile(pipePath.string().c_str(), GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL), 229 | &CloseHandle); 230 | 231 | if (!handle) { 232 | throw PipeException< DWORD >(GetLastError(), "Open for write"); 233 | } 234 | 235 | OVERLAPPED overlapped; 236 | memset(&overlapped, 0, sizeof(OVERLAPPED)); 237 | if (!WriteFile(handle, content.c_str(), static_cast< DWORD >(content.size()), NULL, &overlapped)) { 238 | if (GetLastError() == ERROR_IO_PENDING) { 239 | waitOnAsyncIO(handle, &overlapped, timeout); 240 | } else { 241 | throw PipeException< DWORD >(GetLastError(), "Write"); 242 | } 243 | } 244 | } 245 | 246 | // Implementation from https://stackoverflow.com/a/66588424/3907364 247 | bool NamedPipe::exists(const std::filesystem::path &pipePath) { 248 | std::string pipeName = pipePath.string(); 249 | if ((pipeName.size() < 10) || (pipeName.compare(0, 9, "\\\\.\\pipe\\") != 0) 250 | || (pipeName.find('\\', 9) != std::string::npos)) { 251 | // This can't be a pipe, so it also can't exist 252 | return false; 253 | } 254 | pipeName.erase(0, 9); 255 | 256 | WIN32_FIND_DATA fd; 257 | DWORD dwErrCode; 258 | 259 | HANDLE hFind = FindFirstFileA("\\\\.\\pipe\\*", &fd); 260 | if (hFind == INVALID_HANDLE_VALUE) { 261 | dwErrCode = GetLastError(); 262 | } else { 263 | do { 264 | if (pipeName == fd.cFileName) { 265 | FindClose(hFind); 266 | return true; 267 | } 268 | } while (FindNextFileA(hFind, &fd)); 269 | 270 | dwErrCode = GetLastError(); 271 | FindClose(hFind); 272 | } 273 | 274 | if ((dwErrCode != ERROR_FILE_NOT_FOUND) && (dwErrCode != ERROR_NO_MORE_FILES)) { 275 | throw PipeException< DWORD >(dwErrCode, "CheckExistance"); 276 | } 277 | 278 | return false; 279 | } 280 | 281 | void disconnectAndReconnect(HANDLE pipeHandle, LPOVERLAPPED overlappedPtr, bool disconnectFirst, 282 | unsigned int &timeout) { 283 | if (disconnectFirst) { 284 | if (!DisconnectNamedPipe(pipeHandle)) { 285 | throw PipeException< DWORD >(GetLastError(), "Disconnect"); 286 | } 287 | } 288 | 289 | if (!ConnectNamedPipe(pipeHandle, overlappedPtr)) { 290 | switch (GetLastError()) { 291 | case ERROR_IO_PENDING: 292 | // Wait for async IO operation to complete 293 | waitOnAsyncIO(pipeHandle, overlappedPtr, timeout); 294 | 295 | return; 296 | 297 | case ERROR_NO_DATA: 298 | case ERROR_PIPE_CONNECTED: 299 | // These error codes mean that there is a client connected already. 300 | // In theory ERROR_NO_DATA means that the client has closed its handle 301 | // to the pipe already but it seems that we can read from the pipe just fine 302 | // so we count this as a success. 303 | return; 304 | default: 305 | throw PipeException< DWORD >(GetLastError(), "Connect"); 306 | } 307 | } 308 | } 309 | 310 | std::string NamedPipe::read_blocking(unsigned int timeout) const { 311 | std::string content; 312 | 313 | OVERLAPPED overlapped; 314 | memset(&overlapped, 0, sizeof(OVERLAPPED)); 315 | 316 | handle_t eventHandle(CreateEvent(NULL, TRUE, TRUE, NULL), &CloseHandle); 317 | overlapped.hEvent = eventHandle; 318 | 319 | // Connect to pipe 320 | disconnectAndReconnect(m_handle, &overlapped, false, timeout); 321 | 322 | // Reset overlapped structure 323 | memset(&overlapped, 0, sizeof(OVERLAPPED)); 324 | overlapped.hEvent = eventHandle; 325 | 326 | char buffer[PIPE_BUFFER_SIZE]; 327 | 328 | // Loop until we explicitly break from it (because we're done reading) 329 | while (true) { 330 | DWORD readBytes = 0; 331 | BOOL success = ReadFile(m_handle, &buffer, PIPE_BUFFER_SIZE, &readBytes, &overlapped); 332 | if (!success && GetLastError() == ERROR_IO_PENDING) { 333 | // Wait for the async IO to complete (note that the thread can't be 334 | // interrupted while waiting this way) 335 | success = GetOverlappedResult(m_handle, &overlapped, &readBytes, TRUE); 336 | 337 | if (!success && GetLastError() != ERROR_BROKEN_PIPE) { 338 | throw PipeException< DWORD >(GetLastError(), "Overlapped waiting"); 339 | } 340 | } 341 | 342 | if (!success && content.size() > 0) { 343 | // We have already read some data -> assume that we reached the end of it 344 | break; 345 | } 346 | 347 | if (success) { 348 | content.append(buffer, readBytes); 349 | 350 | if (readBytes < PIPE_BUFFER_SIZE) { 351 | // It seems like we read the complete message 352 | break; 353 | } 354 | } else { 355 | switch (GetLastError()) { 356 | case ERROR_BROKEN_PIPE: 357 | // Reset overlapped structure 358 | memset(&overlapped, 0, sizeof(OVERLAPPED)); 359 | overlapped.hEvent = eventHandle; 360 | 361 | disconnectAndReconnect(m_handle, &overlapped, true, timeout); 362 | 363 | // Reset overlapped structure 364 | memset(&overlapped, 0, sizeof(OVERLAPPED)); 365 | overlapped.hEvent = eventHandle; 366 | break; 367 | case ERROR_PIPE_LISTENING: 368 | break; 369 | default: 370 | throw PipeException< DWORD >(GetLastError(), "Read"); 371 | } 372 | 373 | if (timeout > PIPE_WAIT_INTERVAL) { 374 | timeout -= PIPE_WAIT_INTERVAL; 375 | } else { 376 | throw TimeoutException(); 377 | } 378 | 379 | boost::this_thread::sleep_for(boost::chrono::milliseconds(PIPE_WAIT_INTERVAL)); 380 | } 381 | } 382 | 383 | DisconnectNamedPipe(m_handle); 384 | 385 | return content; 386 | } 387 | #endif 388 | 389 | // Cross-platform implementations 390 | NamedPipe::NamedPipe(const std::filesystem::path &path) : m_pipePath(path) {} 391 | 392 | #ifdef PLATFORM_WINDOWS 393 | NamedPipe::NamedPipe(NamedPipe &&other) : m_pipePath(std::move(other.m_pipePath)), m_handle(other.m_handle) { 394 | other.m_pipePath.clear(); 395 | other.m_handle = INVALID_HANDLE_VALUE; 396 | } 397 | 398 | NamedPipe &NamedPipe::operator=(NamedPipe &&other) { 399 | m_pipePath = std::move(other.m_pipePath); 400 | m_handle = other.m_handle; 401 | 402 | other.m_pipePath.clear(); 403 | other.m_handle = INVALID_HANDLE_VALUE; 404 | 405 | return *this; 406 | } 407 | 408 | void NamedPipe::destroy() { 409 | if (!m_pipePath.empty()) { 410 | if (!CloseHandle(m_handle)) { 411 | std::cerr << "Failed at closing pipe handle: " << GetLastError() << std::endl; 412 | } 413 | 414 | m_pipePath.clear(); 415 | m_handle = INVALID_HANDLE_VALUE; 416 | } 417 | } 418 | #else // PLATFORM_WINDOWS 419 | NamedPipe::NamedPipe(NamedPipe &&other) : m_pipePath(std::move(other.m_pipePath)) { other.m_pipePath.clear(); } 420 | 421 | NamedPipe &NamedPipe::operator=(NamedPipe &&other) { 422 | m_pipePath = std::move(other.m_pipePath); 423 | 424 | other.m_pipePath.clear(); 425 | 426 | return *this; 427 | } 428 | 429 | void NamedPipe::destroy() { 430 | if (!m_pipePath.empty()) { 431 | std::error_code errorCode; 432 | std::filesystem::remove(m_pipePath, errorCode); 433 | 434 | if (errorCode) { 435 | std::cerr << "Failed at deleting pipe-object: " << errorCode << std::endl; 436 | } 437 | 438 | m_pipePath.clear(); 439 | } 440 | } 441 | #endif // PLATFORM_WINDOWS 442 | 443 | NamedPipe::~NamedPipe() { destroy(); } 444 | 445 | std::filesystem::path NamedPipe::getPath() const noexcept { return m_pipePath; } 446 | 447 | void NamedPipe::write(const std::string &content, unsigned int timeout) const { 448 | write(m_pipePath, content, timeout); 449 | } 450 | 451 | NamedPipe::operator bool() const noexcept { return !m_pipePath.empty(); } 452 | 453 | }; // namespace JsonBridge 454 | }; // namespace Mumble 455 | -------------------------------------------------------------------------------- /json_bridge/tests/bridgeCommunication/test_bridgeCommunication.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // source tree. 5 | 6 | #include "gtest/gtest.h" 7 | 8 | #include 9 | #include 10 | 11 | #include "API_mock.h" 12 | 13 | #include 14 | 15 | #include 16 | #include 17 | 18 | using namespace Mumble::JsonBridge; 19 | 20 | #ifdef PLATFORM_UNIX 21 | # define PIPEDIR "." 22 | #else 23 | # define PIPEDIR "\\\\.\\pipe\\" 24 | #endif 25 | 26 | #define ASSERT_FIELD(msg, name, type) \ 27 | ASSERT_TRUE(msg.contains(name)) << "Field does not contain a \"" << name << "\" field"; \ 28 | ASSERT_TRUE(msg[name].is_##type()) << "Field \"" << name << "\" is not of type " #type 29 | 30 | #define ASSERT_API_CALL_HAPPENED(funcName, amount) \ 31 | ASSERT_TRUE(API_Mock::calledFunctions.count(funcName) > 0) \ 32 | << "Expected an API call to \"" funcName "\" to have happened, but it didn't."; \ 33 | ASSERT_EQ(API_Mock::calledFunctions[funcName], amount) \ 34 | << "Expected API call to \"" funcName "\" to have happened " #amount " time(s) but it happened " \ 35 | << API_Mock::calledFunctions[funcName] << " time(s)"; \ 36 | API_Mock::calledFunctions.erase(funcName); 37 | 38 | const std::filesystem::path clientPipePath(std::filesystem::path(PIPEDIR) / ".client-pipe"); 39 | 40 | constexpr unsigned int READ_TIMEOUT = 5 * 1000; 41 | 42 | const std::string clientSecret = "superSecureClientSecret"; 43 | 44 | class BridgeCommunication : public ::testing::Test { 45 | protected: 46 | MumbleAPI m_api; 47 | Bridge m_bridge; 48 | NamedPipe m_clientPipe; 49 | std::string m_bridgeSecret; 50 | 51 | BridgeCommunication() : m_api(API_Mock::getMumbleAPI_v_1_2_x(), API_Mock::pluginID), m_bridge(m_api) {} 52 | 53 | void SetUp() override { 54 | ASSERT_FALSE(NamedPipe::exists(clientPipePath)) << "There already exists an old pipe"; 55 | 56 | m_bridgeSecret.clear(); 57 | m_clientPipe = NamedPipe::create(clientPipePath); 58 | m_bridge.start(); 59 | } 60 | 61 | void TearDown() override { 62 | // Drain potential left-over messages 63 | // If there are no left-overs, then the function will time out and throw an exception 64 | std::string content; 65 | ASSERT_THROW(content = m_clientPipe.read_blocking(5), TimeoutException) 66 | << "There are unread messages in the client-pipe"; 67 | ASSERT_TRUE(content.size() == 0); 68 | 69 | m_bridge.stop(true); 70 | 71 | if (API_Mock::calledFunctions.size() > 0) { 72 | for (const auto ¤t : API_Mock::calledFunctions) { 73 | std::cerr << "Got " << current.second << " unexpected API call(s) to \"" << current.first << "\"" 74 | << std::endl; 75 | } 76 | 77 | // Clear calls in order to not mess with the following test 78 | API_Mock::calledFunctions.clear(); 79 | 80 | FAIL() << "There were unexpected API function calls"; 81 | } 82 | 83 | m_clientPipe.destroy(); 84 | 85 | // For some reason using ASSERT_FALSE(...) << "msg" throws an SEH on windows 86 | // The circumstances of this are very odd but seem to somehow relate to the 87 | // test case for the invalid JSON. There were a couple of SEH errors in gtest 88 | // in the past though and since I was unable to find any hints of an error in 89 | // the code here, let's just assume that this is some sort of weird bug in gtest. 90 | if (NamedPipe::exists(clientPipePath)) { 91 | std::cerr << "Client pipe was not destroyed!" << std::endl; 92 | FAIL(); 93 | } 94 | if (NamedPipe::exists(Bridge::s_pipePath)) { 95 | std::cerr << "Bridge pipe was not destroyed!" << std::endl; 96 | FAIL(); 97 | } 98 | } 99 | 100 | void performRegistration() { 101 | // clang-format off 102 | nlohmann::json message = { 103 | {"message_type", "registration"}, 104 | {"message", 105 | { 106 | {"pipe_path", clientPipePath.string()}, 107 | {"secret", clientSecret} 108 | } 109 | } 110 | }; 111 | // clang-format off 112 | 113 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 114 | } 115 | 116 | [[nodiscard]] 117 | int performRegistrationAndDrain() { 118 | performRegistration(); 119 | 120 | // Drain the bridge's answer to the registration 121 | std::string strAnswer = m_clientPipe.read_blocking(); 122 | 123 | nlohmann::json answer = nlohmann::json::parse(strAnswer); 124 | 125 | m_bridgeSecret = answer["secret"].get(); 126 | 127 | return answer["response"]["client_id"].get(); 128 | } 129 | 130 | void checkAnswer(const nlohmann::json &answer) { 131 | ASSERT_TRUE(answer.is_object()) << "Answer is not an object"; 132 | ASSERT_FIELD(answer, "response_type", string); 133 | ASSERT_FIELD(answer, "secret", string); 134 | if (answer["response_type"].get() != "disconnect") { 135 | // The disconnect message doesn't have a response body 136 | ASSERT_FIELD(answer, "response", object); 137 | ASSERT_EQ(answer.size(), 3) << "Answer contains wrong amount of fields"; 138 | } else { 139 | ASSERT_EQ(answer.size(), 2) << "Answer contains wrong amount of fields"; 140 | } 141 | 142 | if (m_bridgeSecret.size() > 0) { 143 | ASSERT_EQ(m_bridgeSecret, answer["secret"].get()) << "Bridge used wrong secret"; 144 | } 145 | } 146 | }; 147 | 148 | 149 | TEST_F(BridgeCommunication, basic_registration) { 150 | performRegistration(); 151 | 152 | std::string answer = m_clientPipe.read_blocking(READ_TIMEOUT); 153 | nlohmann::json answerJson = nlohmann::json::parse(answer); 154 | 155 | checkAnswer(answerJson); 156 | 157 | const nlohmann::json &responseObj = answerJson["response"]; 158 | ASSERT_TRUE(responseObj.is_object()) << "Response is not an object"; 159 | ASSERT_EQ(responseObj.size(), 1) << "Response contains wrong amount of fields"; 160 | ASSERT_FIELD(responseObj, "client_id", number_integer); 161 | 162 | ASSERT_EQ(answerJson["response_type"], "registration"); 163 | } 164 | 165 | TEST_F(BridgeCommunication, error_registrationWithNonExistentPipe) { 166 | // clang-format off 167 | nlohmann::json message = { 168 | {"message_type", "registration"}, 169 | {"message", 170 | { 171 | {"pipe_path", (std::filesystem::path(PIPEDIR) / "NonExistentPipeName").string()}, 172 | {"secret", clientSecret} 173 | } 174 | } 175 | }; 176 | // clang-format on 177 | 178 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 179 | 180 | // If the registration references the wrong pipe, we can't expect anything to be written to our pipe 181 | std::string answer; 182 | ASSERT_THROW(answer = m_clientPipe.read_blocking(100), TimeoutException); 183 | } 184 | 185 | TEST_F(BridgeCommunication, disconnect) { 186 | int clientID = performRegistrationAndDrain(); 187 | 188 | // clang-format off 189 | nlohmann::json message = { 190 | {"message_type", "disconnect"}, 191 | {"client_id", clientID}, 192 | {"secret", clientSecret}, 193 | }; 194 | // clang-format on 195 | 196 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 197 | 198 | nlohmann::json answer = nlohmann::json::parse(m_clientPipe.read_blocking(READ_TIMEOUT)); 199 | 200 | checkAnswer(answer); 201 | 202 | // Now that we're unregistered again, the next write operation should simply time-out since 203 | // the Bridge doesn't know about our client anymore and therefore can't report any error to us 204 | // Just send the disconnect message a second time in order to serve as a dummy-message 205 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 206 | 207 | std::string dummy; 208 | ASSERT_THROW(dummy = m_clientPipe.read_blocking(100), TimeoutException); 209 | } 210 | 211 | TEST_F(BridgeCommunication, getLocalUserID) { 212 | int clientID = performRegistrationAndDrain(); 213 | 214 | // clang-format off 215 | nlohmann::json message = { 216 | {"message_type", "api_call"}, 217 | {"client_id", clientID}, 218 | {"secret", clientSecret}, 219 | {"message", 220 | { 221 | {"function", "getLocalUserID"}, 222 | {"parameter", 223 | { 224 | {"connection", API_Mock::activeConnetion} 225 | } 226 | } 227 | } 228 | } 229 | }; 230 | // clang-format on 231 | 232 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 233 | 234 | std::string strAnswer = m_clientPipe.read_blocking(READ_TIMEOUT); 235 | 236 | nlohmann::json answer = nlohmann::json::parse(strAnswer); 237 | 238 | checkAnswer(answer); 239 | 240 | ASSERT_EQ(answer["response_type"].get< std::string >(), "api_call"); 241 | 242 | const nlohmann::json &response = answer["response"]; 243 | 244 | ASSERT_FIELD(response, "function", string); 245 | ASSERT_FIELD(response, "status", string); 246 | ASSERT_FIELD(response, "return_value", number_unsigned); 247 | 248 | ASSERT_EQ(response["function"].get< std::string >(), "getLocalUserID"); 249 | ASSERT_EQ(response["status"].get< std::string >(), "executed"); 250 | ASSERT_EQ(response["return_value"].get< unsigned int >(), API_Mock::localUserID); 251 | 252 | ASSERT_API_CALL_HAPPENED("getLocalUserID", 1); 253 | } 254 | 255 | TEST_F(BridgeCommunication, getAllUsers) { 256 | int clientID = performRegistrationAndDrain(); 257 | 258 | // clang-format off 259 | nlohmann::json message = { 260 | {"message_type", "api_call"}, 261 | {"client_id", clientID}, 262 | {"secret", clientSecret}, 263 | {"message", 264 | { 265 | {"function", "getAllUsers"}, 266 | {"parameter", 267 | { 268 | {"connection", API_Mock::activeConnetion} 269 | } 270 | } 271 | } 272 | } 273 | }; 274 | // clang-format on 275 | 276 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 277 | 278 | std::string strAnswer = m_clientPipe.read_blocking(READ_TIMEOUT); 279 | 280 | nlohmann::json answer = nlohmann::json::parse(strAnswer); 281 | 282 | checkAnswer(answer); 283 | 284 | ASSERT_EQ(answer["response_type"].get< std::string >(), "api_call"); 285 | 286 | const nlohmann::json &response = answer["response"]; 287 | 288 | ASSERT_FIELD(response, "function", string); 289 | ASSERT_FIELD(response, "status", string); 290 | ASSERT_FIELD(response, "return_value", array); 291 | 292 | ASSERT_EQ(response["function"].get< std::string >(), "getAllUsers"); 293 | ASSERT_EQ(response["status"].get< std::string >(), "executed"); 294 | 295 | std::vector< mumble_userid_t > users = response["return_value"].get< std::vector< mumble_userid_t > >(); 296 | ASSERT_EQ(users.size(), 2); 297 | ASSERT_TRUE(std::find(users.begin(), users.end(), API_Mock::localUserID) != users.end()); 298 | ASSERT_TRUE(std::find(users.begin(), users.end(), API_Mock::otherUserID) != users.end()); 299 | 300 | ASSERT_API_CALL_HAPPENED("getAllUsers", 1); 301 | // freeMemory has to be called since the array allocated for the users needs explicit freeing (handled by the API 302 | // cpp wrapper automatically) 303 | ASSERT_API_CALL_HAPPENED("freeMemory", 1); 304 | } 305 | 306 | TEST_F(BridgeCommunication, getUserName) { 307 | int clientID = performRegistrationAndDrain(); 308 | 309 | // clang-format off 310 | nlohmann::json message = { 311 | {"message_type", "api_call"}, 312 | {"client_id", clientID}, 313 | {"secret", clientSecret}, 314 | {"message", 315 | { 316 | {"function", "getUserName"}, 317 | {"parameter", 318 | { 319 | {"connection", API_Mock::activeConnetion}, 320 | {"user_id", API_Mock::localUserID} 321 | } 322 | } 323 | } 324 | } 325 | }; 326 | // clang-format on 327 | 328 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 329 | 330 | std::string strAnswer = m_clientPipe.read_blocking(READ_TIMEOUT); 331 | 332 | nlohmann::json answer = nlohmann::json::parse(strAnswer); 333 | 334 | checkAnswer(answer); 335 | 336 | ASSERT_EQ(answer["response_type"].get< std::string >(), "api_call"); 337 | 338 | const nlohmann::json &response = answer["response"]; 339 | 340 | ASSERT_FIELD(response, "function", string); 341 | ASSERT_FIELD(response, "status", string); 342 | ASSERT_FIELD(response, "return_value", string); 343 | 344 | ASSERT_EQ(response["function"].get< std::string >(), "getUserName"); 345 | ASSERT_EQ(response["status"].get< std::string >(), "executed"); 346 | ASSERT_EQ(response["return_value"].get< std::string >(), API_Mock::localUserName); 347 | 348 | ASSERT_API_CALL_HAPPENED("getUserName", 1); 349 | // freeMemory has to be called since the string allocated for the name needs explicit freeing (handled by the API 350 | // cpp wrapper automatically) 351 | ASSERT_API_CALL_HAPPENED("freeMemory", 1); 352 | } 353 | 354 | TEST_F(BridgeCommunication, findUserByName) { 355 | int clientID = performRegistrationAndDrain(); 356 | 357 | // clang-format off 358 | nlohmann::json message = { 359 | {"message_type", "api_call"}, 360 | {"client_id", clientID}, 361 | {"secret", clientSecret}, 362 | {"message", 363 | { 364 | {"function", "findUserByName"}, 365 | {"parameter", 366 | { 367 | {"connection", API_Mock::activeConnetion}, 368 | {"user_name", API_Mock::localUserName} 369 | } 370 | } 371 | } 372 | } 373 | }; 374 | // clang-format on 375 | 376 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 377 | 378 | std::string strAnswer = m_clientPipe.read_blocking(READ_TIMEOUT); 379 | 380 | nlohmann::json answer = nlohmann::json::parse(strAnswer); 381 | 382 | checkAnswer(answer); 383 | 384 | ASSERT_EQ(answer["response_type"].get< std::string >(), "api_call"); 385 | 386 | const nlohmann::json &response = answer["response"]; 387 | 388 | ASSERT_FIELD(response, "function", string); 389 | ASSERT_FIELD(response, "status", string); 390 | ASSERT_FIELD(response, "return_value", number_unsigned); 391 | 392 | ASSERT_EQ(response["function"].get< std::string >(), "findUserByName"); 393 | ASSERT_EQ(response["status"].get< std::string >(), "executed"); 394 | ASSERT_EQ(response["return_value"].get< mumble_userid_t >(), API_Mock::localUserID); 395 | 396 | ASSERT_API_CALL_HAPPENED("findUserByName", 1); 397 | } 398 | 399 | TEST_F(BridgeCommunication, log) { 400 | int clientID = performRegistrationAndDrain(); 401 | 402 | // clang-format off 403 | nlohmann::json message = { 404 | {"message_type", "api_call"}, 405 | {"client_id", clientID}, 406 | {"secret", clientSecret}, 407 | {"message", 408 | { 409 | {"function", "log"}, 410 | {"parameter", 411 | { 412 | {"message", "I am a dummy log-msg"} 413 | } 414 | } 415 | } 416 | } 417 | }; 418 | // clang-format on 419 | 420 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 421 | 422 | std::string strAnswer = m_clientPipe.read_blocking(READ_TIMEOUT); 423 | 424 | nlohmann::json answer = nlohmann::json::parse(strAnswer); 425 | 426 | checkAnswer(answer); 427 | 428 | ASSERT_EQ(answer["response_type"].get< std::string >(), "api_call"); 429 | 430 | const nlohmann::json &response = answer["response"]; 431 | 432 | ASSERT_FIELD(response, "function", string); 433 | ASSERT_FIELD(response, "status", string); 434 | ASSERT_FALSE(response.contains("return_value")); 435 | 436 | ASSERT_EQ(response["function"].get< std::string >(), "log"); 437 | ASSERT_EQ(response["status"].get< std::string >(), "executed"); 438 | 439 | ASSERT_API_CALL_HAPPENED("log", 1); 440 | } 441 | 442 | TEST_F(BridgeCommunication, error_missingMessageType) { 443 | int clientID = performRegistrationAndDrain(); 444 | 445 | // clang-format off 446 | nlohmann::json message = { 447 | {"client_id", clientID}, 448 | {"secret", clientSecret}, 449 | {"message", 450 | { 451 | {"dummy", 0} 452 | } 453 | } 454 | }; 455 | // clang-format on 456 | 457 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 458 | 459 | std::string strAnswer = m_clientPipe.read_blocking(READ_TIMEOUT); 460 | 461 | nlohmann::json answer = nlohmann::json::parse(strAnswer); 462 | 463 | checkAnswer(answer); 464 | 465 | ASSERT_EQ(answer["response_type"].get< std::string >(), "error"); 466 | 467 | const nlohmann::json &response = answer["response"]; 468 | 469 | ASSERT_FIELD(response, "error_message", string); 470 | std::string errorMsg = response["error_message"].get< std::string >(); 471 | // Make sure the error message actually references the missing field 472 | ASSERT_TRUE(errorMsg.find("message_type") != std::string::npos); 473 | } 474 | 475 | TEST_F(BridgeCommunication, error_missingSecret) { 476 | int clientID = performRegistrationAndDrain(); 477 | 478 | // clang-format off 479 | nlohmann::json message = { 480 | {"message_type", "api_call"}, 481 | {"client_id", clientID}, 482 | {"message", 483 | { 484 | {"dummy", 0} 485 | } 486 | } 487 | }; 488 | // clang-format on 489 | 490 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 491 | 492 | std::string strAnswer = m_clientPipe.read_blocking(READ_TIMEOUT); 493 | 494 | nlohmann::json answer = nlohmann::json::parse(strAnswer); 495 | 496 | checkAnswer(answer); 497 | 498 | ASSERT_EQ(answer["response_type"].get< std::string >(), "error"); 499 | 500 | const nlohmann::json &response = answer["response"]; 501 | 502 | ASSERT_FIELD(response, "error_message", string); 503 | std::string errorMsg = response["error_message"].get< std::string >(); 504 | // Make sure the error message actually references the missing field 505 | ASSERT_TRUE(errorMsg.find("secret") != std::string::npos); 506 | } 507 | 508 | TEST_F(BridgeCommunication, error_WrongSecret) { 509 | int clientID = performRegistrationAndDrain(); 510 | 511 | // clang-format off 512 | nlohmann::json message = { 513 | {"message_type", "api_call"}, 514 | {"client_id", clientID}, 515 | {"secret", "I am wrong"}, 516 | {"message", 517 | { 518 | {"dummy", 0} 519 | } 520 | } 521 | }; 522 | // clang-format on 523 | 524 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 525 | 526 | std::string strAnswer = m_clientPipe.read_blocking(READ_TIMEOUT); 527 | 528 | nlohmann::json answer = nlohmann::json::parse(strAnswer); 529 | 530 | checkAnswer(answer); 531 | 532 | ASSERT_EQ(answer["response_type"].get< std::string >(), "error"); 533 | 534 | const nlohmann::json &response = answer["response"]; 535 | 536 | ASSERT_FIELD(response, "error_message", string); 537 | std::string errorMsg = response["error_message"].get< std::string >(); 538 | // Make sure the error message actually references the missing field 539 | ASSERT_TRUE(errorMsg.find("secret") != std::string::npos); 540 | } 541 | 542 | TEST_F(BridgeCommunication, error_WrongMessageType) { 543 | int clientID = performRegistrationAndDrain(); 544 | 545 | // clang-format off 546 | nlohmann::json message = { 547 | {"message_type", "I am wrong"}, 548 | {"client_id", clientID}, 549 | {"secret", clientSecret}, 550 | {"message", 551 | { 552 | {"dummy", 0} 553 | } 554 | } 555 | }; 556 | // clang-format on 557 | 558 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 559 | 560 | std::string strAnswer = m_clientPipe.read_blocking(READ_TIMEOUT); 561 | 562 | nlohmann::json answer = nlohmann::json::parse(strAnswer); 563 | 564 | checkAnswer(answer); 565 | 566 | ASSERT_EQ(answer["response_type"].get< std::string >(), "error"); 567 | 568 | const nlohmann::json &response = answer["response"]; 569 | 570 | ASSERT_FIELD(response, "error_message", string); 571 | std::string errorMsg = response["error_message"].get< std::string >(); 572 | // Make sure the error message actually references the missing field 573 | ASSERT_TRUE(errorMsg.find("message_type") != std::string::npos); 574 | } 575 | 576 | TEST_F(BridgeCommunication, error_wrongParamCount) { 577 | int clientID = performRegistrationAndDrain(); 578 | 579 | // clang-format off 580 | nlohmann::json message = { 581 | {"message_type", "api_call"}, 582 | {"client_id", clientID}, 583 | {"secret", clientSecret}, 584 | {"message", 585 | { 586 | {"function", "log"}, 587 | {"parameter", 588 | { 589 | {"message", "I am a dummy log-msg"}, 590 | {"dummy", "I am too much"} 591 | } 592 | } 593 | } 594 | } 595 | }; 596 | // clang-format on 597 | 598 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 599 | 600 | std::string strAnswer = m_clientPipe.read_blocking(READ_TIMEOUT); 601 | 602 | nlohmann::json answer = nlohmann::json::parse(strAnswer); 603 | 604 | checkAnswer(answer); 605 | 606 | ASSERT_EQ(answer["response_type"].get< std::string >(), "error"); 607 | 608 | const nlohmann::json &response = answer["response"]; 609 | 610 | ASSERT_FIELD(response, "error_message", string); 611 | 612 | std::string errorMsg = response["error_message"].get< std::string >(); 613 | // Make sure the error message actually references the wrong param count 614 | ASSERT_TRUE(errorMsg.find("expects") != std::string::npos); 615 | ASSERT_TRUE(errorMsg.find("parameter") != std::string::npos); 616 | } 617 | 618 | TEST_F(BridgeCommunication, error_wrongParamType) { 619 | int clientID = performRegistrationAndDrain(); 620 | 621 | // clang-format off 622 | nlohmann::json message = { 623 | {"message_type", "api_call"}, 624 | {"client_id", clientID}, 625 | {"secret", clientSecret}, 626 | {"message", 627 | { 628 | {"function", "log"}, 629 | {"parameter", 630 | { 631 | {"message", 3}, 632 | } 633 | } 634 | } 635 | } 636 | }; 637 | // clang-format on 638 | 639 | NamedPipe::write(m_bridge.s_pipePath, message.dump()); 640 | 641 | std::string strAnswer = m_clientPipe.read_blocking(READ_TIMEOUT); 642 | 643 | nlohmann::json answer = nlohmann::json::parse(strAnswer); 644 | 645 | checkAnswer(answer); 646 | 647 | ASSERT_EQ(answer["response_type"].get< std::string >(), "error"); 648 | 649 | const nlohmann::json &response = answer["response"]; 650 | 651 | ASSERT_FIELD(response, "error_message", string); 652 | 653 | std::string errorMsg = response["error_message"].get< std::string >(); 654 | // Make sure the error message actually references the wrong param type 655 | ASSERT_TRUE(errorMsg.find("message") != std::string::npos); 656 | ASSERT_TRUE(errorMsg.find("expected") != std::string::npos); 657 | ASSERT_TRUE(errorMsg.find("string") != std::string::npos); 658 | } 659 | 660 | TEST_F(BridgeCommunication, error_invalidJSON) { 661 | int clientID = performRegistrationAndDrain(); 662 | 663 | // Note the missing "}" at the end of the message 664 | NamedPipe::write(m_bridge.s_pipePath, 665 | "{\"message\":{\"pipe_path\":\"./.client-pipe\"},\"message_type\":\"registration\""); 666 | 667 | std::string answer; 668 | ASSERT_THROW(answer = m_clientPipe.read_blocking(100), TimeoutException); 669 | } 670 | -------------------------------------------------------------------------------- /json_bridge/tests/bridgeCommunication/API_mock.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2020 The Mumble Developers. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license 3 | // that can be found in the LICENSE file at the root of the 4 | // Mumble source tree or at . 5 | 6 | #include "API_mock.h" 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | 18 | // Pretend to only know a plugin of ID 42 19 | #define VERIFY_PLUGIN_ID(id) \ 20 | if (id != pluginID) { \ 21 | return MUMBLE_EC_INVALID_PLUGIN_ID; \ 22 | } 23 | // Pretend to only know a connection of ID 13 24 | #define VERIFY_CONNECTION(connection) \ 25 | if (connection != activeConnetion) { \ 26 | return MUMBLE_EC_CONNECTION_NOT_FOUND; \ 27 | } 28 | // Pretend that the connection is always synchronized 29 | #define ENSURE_CONNECTION_SYNCHRONIZED(connection) \ 30 | if (false) { \ 31 | return MUMBLE_EC_CONNECTION_UNSYNCHRONIZED; \ 32 | } 33 | 34 | #define UNUSED(var) (void) var 35 | 36 | namespace API_Mock { 37 | 38 | std::unordered_map< std::string, int > calledFunctions; 39 | 40 | /// A "curator" that will keep track of allocated resources and how to delete them 41 | struct MumbleAPICurator { 42 | std::vector< const void * > m_allocatedMemory; 43 | 44 | ~MumbleAPICurator() { 45 | // free all allocated functions 46 | 47 | if (m_allocatedMemory.size() > 0) { 48 | std::cerr << "There are " << m_allocatedMemory.size() << " leaking resources" << std::endl; 49 | std::exit(1); 50 | } 51 | } 52 | }; 53 | 54 | static MumbleAPICurator curator; 55 | 56 | 57 | ////////////////////////////////////////////// 58 | /////////// API IMPLEMENTATION /////////////// 59 | ////////////////////////////////////////////// 60 | 61 | // The description of the functions is provided in MumbleAPI.h 62 | 63 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION freeMemory_v_1_0_x(mumble_plugin_id_t callerID, const void *ptr) { 64 | calledFunctions["freeMemory"]++; 65 | 66 | // Don't verify plugin ID here to avoid memory leaks 67 | UNUSED(callerID); 68 | 69 | auto it = std::find(curator.m_allocatedMemory.begin(), curator.m_allocatedMemory.end(), ptr); 70 | 71 | if (it == curator.m_allocatedMemory.end()) { 72 | return MUMBLE_EC_POINTER_NOT_FOUND; 73 | } else { 74 | curator.m_allocatedMemory.erase(it); 75 | 76 | free(const_cast< void * >(ptr)); 77 | 78 | return MUMBLE_STATUS_OK; 79 | } 80 | } 81 | 82 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getActiveServerConnection_v_1_0_x(mumble_plugin_id_t callerID, 83 | mumble_connection_t *connection) { 84 | calledFunctions["getActiveServerConnection"]++; 85 | 86 | VERIFY_PLUGIN_ID(callerID); 87 | 88 | *connection = 13; 89 | 90 | return MUMBLE_STATUS_OK; 91 | } 92 | 93 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION isConnectionSynchronized_v_1_0_x(mumble_plugin_id_t callerID, 94 | mumble_connection_t connection, 95 | bool *synchronized) { 96 | calledFunctions["isConnectionSychronized"]++; 97 | 98 | VERIFY_PLUGIN_ID(callerID); 99 | VERIFY_CONNECTION(connection); 100 | 101 | // In this mock the connection is always assumed to be synchronized 102 | *synchronized = true; 103 | 104 | return MUMBLE_STATUS_OK; 105 | } 106 | 107 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getLocalUserID_v_1_0_x(mumble_plugin_id_t callerID, 108 | mumble_connection_t connection, 109 | mumble_userid_t *userID) { 110 | calledFunctions["getLocalUserID"]++; 111 | 112 | VERIFY_PLUGIN_ID(callerID); 113 | 114 | VERIFY_CONNECTION(connection); 115 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 116 | 117 | *userID = localUserID; 118 | 119 | return MUMBLE_STATUS_OK; 120 | } 121 | 122 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getUserName_v_1_0_x(mumble_plugin_id_t callerID, 123 | mumble_connection_t connection, mumble_userid_t userID, 124 | const char **name) { 125 | calledFunctions["getUserName"]++; 126 | 127 | VERIFY_PLUGIN_ID(callerID); 128 | 129 | // Right now there can only be one connection managed by the current ServerHandler 130 | VERIFY_CONNECTION(connection); 131 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 132 | 133 | std::string userName; 134 | switch (userID) { 135 | case localUserID: 136 | userName = localUserName; 137 | break; 138 | case otherUserID: 139 | userName = otherUserName; 140 | break; 141 | default: 142 | return MUMBLE_EC_USER_NOT_FOUND; 143 | } 144 | 145 | size_t size = userName.size() + 1; 146 | 147 | char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); 148 | 149 | std::strcpy(nameArray, userName.c_str()); 150 | 151 | curator.m_allocatedMemory.push_back(nameArray); 152 | 153 | *name = nameArray; 154 | 155 | return MUMBLE_STATUS_OK; 156 | } 157 | 158 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getChannelName_v_1_0_x(mumble_plugin_id_t callerID, 159 | mumble_connection_t connection, 160 | mumble_channelid_t channelID, const char **name) { 161 | calledFunctions["getChannelName"]++; 162 | 163 | VERIFY_PLUGIN_ID(callerID); 164 | 165 | // Right now there can only be one connection managed by the current ServerHandler 166 | VERIFY_CONNECTION(connection); 167 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 168 | 169 | std::string channelName; 170 | switch (channelID) { 171 | case localUserChannel: 172 | channelName = localUserChannelName; 173 | break; 174 | case otherUserChannel: 175 | channelName = otherUserChannelName; 176 | break; 177 | default: 178 | return MUMBLE_EC_CHANNEL_NOT_FOUND; 179 | } 180 | 181 | size_t size = channelName.size() + 1; 182 | 183 | char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); 184 | 185 | std::strcpy(nameArray, channelName.c_str()); 186 | 187 | curator.m_allocatedMemory.push_back(nameArray); 188 | 189 | *name = nameArray; 190 | 191 | return MUMBLE_STATUS_OK; 192 | } 193 | 194 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getAllUsers_v_1_0_x(mumble_plugin_id_t callerID, 195 | mumble_connection_t connection, mumble_userid_t **users, 196 | size_t *userCount) { 197 | calledFunctions["getAllUsers"]++; 198 | 199 | VERIFY_PLUGIN_ID(callerID); 200 | 201 | // Right now there can only be one connection managed by the current ServerHandler 202 | VERIFY_CONNECTION(connection); 203 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 204 | 205 | size_t amount = 2; 206 | 207 | mumble_userid_t *userIDs = reinterpret_cast< mumble_userid_t * >(malloc(sizeof(mumble_userid_t) * amount)); 208 | 209 | userIDs[0] = localUserID; 210 | userIDs[1] = otherUserID; 211 | 212 | curator.m_allocatedMemory.push_back(userIDs); 213 | 214 | *users = userIDs; 215 | *userCount = amount; 216 | 217 | return MUMBLE_STATUS_OK; 218 | } 219 | 220 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getAllChannels_v_1_0_x(mumble_plugin_id_t callerID, 221 | mumble_connection_t connection, 222 | mumble_channelid_t **channels, size_t *channelCount) { 223 | calledFunctions["getAllChannels"]++; 224 | 225 | VERIFY_PLUGIN_ID(callerID); 226 | 227 | // Right now there can only be one connection managed by the current ServerHandler 228 | VERIFY_CONNECTION(connection); 229 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 230 | 231 | size_t amount = 2; 232 | 233 | mumble_channelid_t *channelIDs = 234 | reinterpret_cast< mumble_channelid_t * >(malloc(sizeof(mumble_channelid_t) * amount)); 235 | 236 | channelIDs[0] = localUserChannel; 237 | channelIDs[1] = otherUserChannel; 238 | 239 | curator.m_allocatedMemory.push_back(channelIDs); 240 | 241 | *channels = channelIDs; 242 | *channelCount = amount; 243 | 244 | return MUMBLE_STATUS_OK; 245 | } 246 | 247 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getChannelOfUser_v_1_0_x(mumble_plugin_id_t callerID, 248 | mumble_connection_t connection, 249 | mumble_userid_t userID, mumble_channelid_t *channel) { 250 | calledFunctions["getChannelOfUser"]++; 251 | 252 | VERIFY_PLUGIN_ID(callerID); 253 | 254 | // Right now there can only be one connection managed by the current ServerHandler 255 | VERIFY_CONNECTION(connection); 256 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 257 | 258 | switch (userID) { 259 | case localUserID: 260 | *channel = localUserChannel; 261 | break; 262 | case otherUserID: 263 | *channel = otherUserChannel; 264 | break; 265 | default: 266 | return MUMBLE_EC_USER_NOT_FOUND; 267 | } 268 | 269 | return MUMBLE_STATUS_OK; 270 | } 271 | 272 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getUsersInChannel_v_1_0_x(mumble_plugin_id_t callerID, 273 | mumble_connection_t connection, 274 | mumble_channelid_t channelID, 275 | mumble_userid_t **userList, size_t *userCount) { 276 | calledFunctions["getUsersInChannel"]++; 277 | 278 | VERIFY_PLUGIN_ID(callerID); 279 | 280 | // Right now there can only be one connection managed by the current ServerHandler 281 | VERIFY_CONNECTION(connection); 282 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 283 | 284 | if (channelID != localUserChannel && channelID != otherUserChannel) { 285 | return MUMBLE_EC_CHANNEL_NOT_FOUND; 286 | } 287 | 288 | size_t amount = 1; 289 | 290 | mumble_userid_t *userIDs = reinterpret_cast< mumble_userid_t * >(malloc(sizeof(mumble_userid_t) * amount)); 291 | 292 | if (channelID == localUserChannel) { 293 | userIDs[0] = localUserID; 294 | } else { 295 | userIDs[0] = otherUserID; 296 | } 297 | 298 | curator.m_allocatedMemory.push_back(userIDs); 299 | 300 | *userList = userIDs; 301 | *userCount = amount; 302 | 303 | return MUMBLE_STATUS_OK; 304 | } 305 | 306 | 307 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION 308 | getLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, mumble_transmission_mode_t *transmissionMode) { 309 | calledFunctions["getLocalUserTransmissionMode"]++; 310 | 311 | VERIFY_PLUGIN_ID(callerID); 312 | 313 | // Pretend local user always uses VAD 314 | *transmissionMode = MUMBLE_TM_VOICE_ACTIVATION; 315 | 316 | return MUMBLE_STATUS_OK; 317 | } 318 | 319 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION isUserLocallyMuted_v_1_0_x(mumble_plugin_id_t callerID, 320 | mumble_connection_t connection, 321 | mumble_userid_t userID, bool *muted) { 322 | calledFunctions["isUserLocallyMuted"]++; 323 | 324 | VERIFY_PLUGIN_ID(callerID); 325 | 326 | // Right now there can only be one connection managed by the current ServerHandler 327 | VERIFY_CONNECTION(connection); 328 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 329 | 330 | if (userID != localUserID && userID != otherUserID) { 331 | return MUMBLE_EC_USER_NOT_FOUND; 332 | } 333 | 334 | // pretend the other user is locally muted 335 | *muted = userID == otherUserID; 336 | 337 | return MUMBLE_STATUS_OK; 338 | } 339 | 340 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION isLocalUserMuted_v_1_0_x(mumble_plugin_id_t callerID, bool *muted) { 341 | calledFunctions["isLocalUserMuted"]++; 342 | 343 | VERIFY_PLUGIN_ID(callerID); 344 | 345 | *muted = false; 346 | 347 | return MUMBLE_STATUS_OK; 348 | } 349 | 350 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION isLocalUserDeafened_v_1_0_x(mumble_plugin_id_t callerID, bool *deafened) { 351 | calledFunctions["isLocalUserDeafened"]++; 352 | 353 | VERIFY_PLUGIN_ID(callerID); 354 | 355 | *deafened = false; 356 | 357 | return MUMBLE_STATUS_OK; 358 | } 359 | 360 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getUserHash_v_1_0_x(mumble_plugin_id_t callerID, 361 | mumble_connection_t connection, mumble_userid_t userID, 362 | const char **hash) { 363 | calledFunctions["getUserHash"]++; 364 | 365 | VERIFY_PLUGIN_ID(callerID); 366 | 367 | // Right now there can only be one connection managed by the current ServerHandler 368 | VERIFY_CONNECTION(connection); 369 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 370 | 371 | std::string userHash; 372 | switch (userID) { 373 | case localUserID: 374 | userHash = "85240b5b2d5ef4227270d2a400957140d2299523"; 375 | break; 376 | case otherUserID: 377 | userHash = "4535efde23c002a726072c9c39d9ede9d3e76be5"; 378 | break; 379 | default: 380 | return MUMBLE_EC_USER_NOT_FOUND; 381 | } 382 | 383 | // The user's hash is already in hexadecimal representation, so we don't have to worry about null-bytes in it 384 | size_t size = userHash.size() + 1; 385 | 386 | char *hashArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); 387 | 388 | std::strcpy(hashArray, userHash.c_str()); 389 | 390 | curator.m_allocatedMemory.push_back(hashArray); 391 | 392 | *hash = hashArray; 393 | 394 | return MUMBLE_STATUS_OK; 395 | } 396 | 397 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getServerHash_v_1_0_x(mumble_plugin_id_t callerID, 398 | mumble_connection_t connection, const char **hash) { 399 | calledFunctions["getServerHash"]++; 400 | 401 | VERIFY_PLUGIN_ID(callerID); 402 | 403 | // Right now there can only be one connection managed by the current ServerHandler 404 | VERIFY_CONNECTION(connection); 405 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 406 | 407 | std::string strHash = "9449d173bcc01d96c6a01de5b93f0d70760fb0f2"; 408 | 409 | size_t size = strHash.size() + 1; 410 | 411 | char *hashArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); 412 | 413 | std::strcpy(hashArray, strHash.c_str()); 414 | 415 | curator.m_allocatedMemory.push_back(hashArray); 416 | 417 | *hash = hashArray; 418 | 419 | return MUMBLE_STATUS_OK; 420 | } 421 | 422 | 423 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION 424 | requestLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, mumble_transmission_mode_t transmissionMode) { 425 | calledFunctions["requestLocalUserTransmissionMode"]++; 426 | 427 | VERIFY_PLUGIN_ID(callerID); 428 | 429 | // We don't actually set the transmission mode 430 | 431 | switch (transmissionMode) { 432 | case MUMBLE_TM_CONTINOUS: 433 | break; 434 | case MUMBLE_TM_VOICE_ACTIVATION: 435 | break; 436 | case MUMBLE_TM_PUSH_TO_TALK: 437 | break; 438 | default: 439 | return MUMBLE_EC_UNKNOWN_TRANSMISSION_MODE; 440 | } 441 | 442 | return MUMBLE_STATUS_OK; 443 | } 444 | 445 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getUserComment_v_1_0_x(mumble_plugin_id_t callerID, 446 | mumble_connection_t connection, mumble_userid_t userID, 447 | const char **comment) { 448 | calledFunctions["getUserComment"]++; 449 | 450 | VERIFY_PLUGIN_ID(callerID); 451 | 452 | // Right now there can only be one connection managed by the current ServerHandler 453 | VERIFY_CONNECTION(connection); 454 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 455 | 456 | if (userID != localUserID && userID != otherUserID) { 457 | return MUMBLE_EC_USER_NOT_FOUND; 458 | } 459 | 460 | std::string strComment = userID == localUserID ? "I am the local user" : "I am another user"; 461 | 462 | size_t size = strComment.size() + 1; 463 | 464 | char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); 465 | 466 | std::strcpy(nameArray, strComment.c_str()); 467 | 468 | curator.m_allocatedMemory.push_back(nameArray); 469 | 470 | *comment = nameArray; 471 | 472 | return MUMBLE_STATUS_OK; 473 | } 474 | 475 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getChannelDescription_v_1_0_x(mumble_plugin_id_t callerID, 476 | mumble_connection_t connection, 477 | mumble_channelid_t channelID, 478 | const char **description) { 479 | calledFunctions["getChannelDescription"]++; 480 | 481 | VERIFY_PLUGIN_ID(callerID); 482 | 483 | // Right now there can only be one connection managed by the current ServerHandler 484 | VERIFY_CONNECTION(connection); 485 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 486 | 487 | if (channelID != localUserChannel && channelID != otherUserChannel) { 488 | return MUMBLE_EC_CHANNEL_NOT_FOUND; 489 | } 490 | 491 | std::string desc = channelID == localUserChannel ? localUserChannelDesc : otherUserChannelDesc; 492 | 493 | size_t size = desc.size() + 1; 494 | 495 | char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); 496 | 497 | std::strcpy(nameArray, desc.c_str()); 498 | 499 | curator.m_allocatedMemory.push_back(nameArray); 500 | 501 | *description = nameArray; 502 | 503 | return MUMBLE_STATUS_OK; 504 | } 505 | 506 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION requestUserMove_v_1_0_x(mumble_plugin_id_t callerID, 507 | mumble_connection_t connection, mumble_userid_t userID, 508 | mumble_channelid_t channelID, const char *password) { 509 | calledFunctions["requestUserMove"]++; 510 | 511 | VERIFY_PLUGIN_ID(callerID); 512 | 513 | // Right now there can only be one connection managed by the current ServerHandler 514 | VERIFY_CONNECTION(connection); 515 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 516 | 517 | if (userID != localUserID && userID != otherUserID) { 518 | return MUMBLE_EC_USER_NOT_FOUND; 519 | } 520 | 521 | if (channelID != localUserChannel && channelID != otherUserChannel) { 522 | return MUMBLE_EC_CHANNEL_NOT_FOUND; 523 | } 524 | 525 | // Don't actually move the user 526 | 527 | return MUMBLE_STATUS_OK; 528 | } 529 | 530 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION requestMicrophoneActivationOverwrite_v_1_0_x(mumble_plugin_id_t callerID, 531 | bool activate) { 532 | calledFunctions["requestMicrophoneActivationOverwrite"]++; 533 | 534 | VERIFY_PLUGIN_ID(callerID); 535 | 536 | // Don't actually do something 537 | UNUSED(activate); 538 | 539 | return MUMBLE_STATUS_OK; 540 | } 541 | 542 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION requestLocalMute_v_1_0_x(mumble_plugin_id_t callerID, 543 | mumble_connection_t connection, 544 | mumble_userid_t userID, bool muted) { 545 | calledFunctions["requestLocalMute"]++; 546 | 547 | VERIFY_PLUGIN_ID(callerID); 548 | 549 | // Right now there can only be one connection managed by the current ServerHandler 550 | VERIFY_CONNECTION(connection); 551 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 552 | 553 | if (userID == localUserID) { 554 | // Can't locally mute the local user 555 | return MUMBLE_EC_INVALID_MUTE_TARGET; 556 | } 557 | 558 | // Don't actually do something 559 | UNUSED(muted); 560 | 561 | return MUMBLE_STATUS_OK; 562 | } 563 | 564 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION requestLocalUserMute_v_1_0_x(mumble_plugin_id_t callerID, bool muted) { 565 | calledFunctions["requestLocalUserMute"]++; 566 | 567 | VERIFY_PLUGIN_ID(callerID); 568 | 569 | // Don't actually do something 570 | UNUSED(muted); 571 | 572 | return MUMBLE_STATUS_OK; 573 | } 574 | 575 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION requestLocalUserDeaf_v_1_0_x(mumble_plugin_id_t callerID, bool deafened) { 576 | calledFunctions["requestLocalUserDeaf"]++; 577 | 578 | VERIFY_PLUGIN_ID(callerID); 579 | 580 | // Don't actually do something 581 | UNUSED(deafened); 582 | 583 | return MUMBLE_STATUS_OK; 584 | } 585 | 586 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION requestSetLocalUserComment_v_1_0_x(mumble_plugin_id_t callerID, 587 | mumble_connection_t connection, 588 | const char *comment) { 589 | calledFunctions["requestSetLocalUserComment"]++; 590 | 591 | VERIFY_PLUGIN_ID(callerID); 592 | 593 | // Right now there can only be one connection managed by the current ServerHandler 594 | VERIFY_CONNECTION(connection); 595 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 596 | 597 | UNUSED(comment); 598 | 599 | return MUMBLE_STATUS_OK; 600 | } 601 | 602 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION findUserByName_v_1_0_x(mumble_plugin_id_t callerID, 603 | mumble_connection_t connection, const char *userName, 604 | mumble_userid_t *userID) { 605 | calledFunctions["findUserByName"]++; 606 | 607 | VERIFY_PLUGIN_ID(callerID); 608 | 609 | // Right now there can only be one connection managed by the current ServerHandler 610 | VERIFY_CONNECTION(connection); 611 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 612 | 613 | if (localUserName == userName) { 614 | std::cout << "Found user" << std::endl; 615 | *userID = localUserID; 616 | } else if (otherUserName == userName) { 617 | *userID = otherUserID; 618 | } else { 619 | return MUMBLE_EC_USER_NOT_FOUND; 620 | } 621 | 622 | return MUMBLE_STATUS_OK; 623 | } 624 | 625 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION findChannelByName_v_1_0_x(mumble_plugin_id_t callerID, 626 | mumble_connection_t connection, 627 | const char *channelName, 628 | mumble_channelid_t *channelID) { 629 | calledFunctions["findChannelByName"]++; 630 | 631 | VERIFY_PLUGIN_ID(callerID); 632 | 633 | // Right now there can only be one connection managed by the current ServerHandler 634 | VERIFY_CONNECTION(connection); 635 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 636 | 637 | if (localUserChannelName == channelName) { 638 | *channelID = localUserChannel; 639 | } else if (otherUserChannelName == channelName) { 640 | *channelID = otherUserChannel; 641 | } else { 642 | return MUMBLE_EC_CHANNEL_NOT_FOUND; 643 | } 644 | 645 | return MUMBLE_STATUS_OK; 646 | } 647 | 648 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, 649 | mumble_settings_key_t key, bool *outValue) { 650 | calledFunctions["getMumbleSetting_bool"]++; 651 | 652 | VERIFY_PLUGIN_ID(callerID); 653 | 654 | UNUSED(key); 655 | UNUSED(outValue); 656 | 657 | return MUMBLE_STATUS_OK; 658 | } 659 | 660 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, 661 | mumble_settings_key_t key, int64_t *outValue) { 662 | calledFunctions["getMumbleSetting_int"]++; 663 | 664 | VERIFY_PLUGIN_ID(callerID); 665 | 666 | UNUSED(key); 667 | UNUSED(outValue); 668 | 669 | return MUMBLE_STATUS_OK; 670 | } 671 | 672 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, 673 | mumble_settings_key_t key, double *outValue) { 674 | calledFunctions["getMumbleSetting_double"]++; 675 | 676 | VERIFY_PLUGIN_ID(callerID); 677 | 678 | UNUSED(key); 679 | UNUSED(outValue); 680 | 681 | return MUMBLE_STATUS_OK; 682 | } 683 | 684 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION getMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, 685 | mumble_settings_key_t key, 686 | const char **outValue) { 687 | calledFunctions["getMumbleSetting_string"]++; 688 | 689 | VERIFY_PLUGIN_ID(callerID); 690 | 691 | UNUSED(key); 692 | UNUSED(outValue); 693 | 694 | return MUMBLE_STATUS_OK; 695 | } 696 | 697 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION setMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, 698 | mumble_settings_key_t key, bool value) { 699 | calledFunctions["setMumbleSetting_bool"]++; 700 | 701 | VERIFY_PLUGIN_ID(callerID); 702 | 703 | UNUSED(key); 704 | UNUSED(value); 705 | 706 | return MUMBLE_STATUS_OK; 707 | } 708 | 709 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION setMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, 710 | mumble_settings_key_t key, int64_t value) { 711 | calledFunctions["setMumbleSetting_int"]++; 712 | 713 | VERIFY_PLUGIN_ID(callerID); 714 | 715 | UNUSED(key); 716 | UNUSED(value); 717 | 718 | return MUMBLE_STATUS_OK; 719 | } 720 | 721 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION setMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, 722 | mumble_settings_key_t key, double value) { 723 | calledFunctions["setMumbleSetting_double"]++; 724 | 725 | VERIFY_PLUGIN_ID(callerID); 726 | 727 | UNUSED(key); 728 | UNUSED(value); 729 | 730 | return MUMBLE_STATUS_OK; 731 | } 732 | 733 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION setMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, 734 | mumble_settings_key_t key, const char *value) { 735 | calledFunctions["setMumbleSetting_string"]++; 736 | 737 | VERIFY_PLUGIN_ID(callerID); 738 | 739 | UNUSED(key); 740 | UNUSED(value); 741 | 742 | return MUMBLE_STATUS_OK; 743 | } 744 | 745 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION sendData_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, 746 | const mumble_userid_t *users, size_t userCount, 747 | const uint8_t *data, size_t dataLength, const char *dataID) { 748 | calledFunctions["sendData"]++; 749 | 750 | VERIFY_PLUGIN_ID(callerID); 751 | 752 | // Right now there can only be one connection managed by the current ServerHandler 753 | VERIFY_CONNECTION(connection); 754 | ENSURE_CONNECTION_SYNCHRONIZED(connection); 755 | 756 | for (size_t i = 0; i < userCount; i++) { 757 | if (users[i] != localUserID && users[i] != otherUserID) { 758 | return MUMBLE_EC_USER_NOT_FOUND; 759 | } 760 | } 761 | 762 | return MUMBLE_STATUS_OK; 763 | } 764 | 765 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION log_v_1_0_x(mumble_plugin_id_t callerID, const char *message) { 766 | calledFunctions["log"]++; 767 | 768 | VERIFY_PLUGIN_ID(callerID); 769 | 770 | UNUSED(message); 771 | 772 | return MUMBLE_STATUS_OK; 773 | } 774 | 775 | mumble_error_t MUMBLE_PLUGIN_CALLING_CONVENTION playSample_v_1_0_x(mumble_plugin_id_t callerID, const char *samplePath, float volume) { 776 | calledFunctions["playSample"]++; 777 | 778 | VERIFY_PLUGIN_ID(callerID); 779 | 780 | UNUSED(samplePath); 781 | UNUSED(volume); 782 | 783 | return MUMBLE_EC_AUDIO_NOT_AVAILABLE; 784 | } 785 | 786 | MumbleAPI_v_1_2_x getMumbleAPI_v_1_2_x() { 787 | return { freeMemory_v_1_0_x, 788 | getActiveServerConnection_v_1_0_x, 789 | isConnectionSynchronized_v_1_0_x, 790 | getLocalUserID_v_1_0_x, 791 | getUserName_v_1_0_x, 792 | getChannelName_v_1_0_x, 793 | getAllUsers_v_1_0_x, 794 | getAllChannels_v_1_0_x, 795 | getChannelOfUser_v_1_0_x, 796 | getUsersInChannel_v_1_0_x, 797 | getLocalUserTransmissionMode_v_1_0_x, 798 | isUserLocallyMuted_v_1_0_x, 799 | isLocalUserMuted_v_1_0_x, 800 | isLocalUserDeafened_v_1_0_x, 801 | getUserHash_v_1_0_x, 802 | getServerHash_v_1_0_x, 803 | getUserComment_v_1_0_x, 804 | getChannelDescription_v_1_0_x, 805 | requestLocalUserTransmissionMode_v_1_0_x, 806 | requestUserMove_v_1_0_x, 807 | requestMicrophoneActivationOverwrite_v_1_0_x, 808 | requestLocalMute_v_1_0_x, 809 | requestLocalUserMute_v_1_0_x, 810 | requestLocalUserDeaf_v_1_0_x, 811 | requestSetLocalUserComment_v_1_0_x, 812 | findUserByName_v_1_0_x, 813 | findChannelByName_v_1_0_x, 814 | getMumbleSetting_bool_v_1_0_x, 815 | getMumbleSetting_int_v_1_0_x, 816 | getMumbleSetting_double_v_1_0_x, 817 | getMumbleSetting_string_v_1_0_x, 818 | setMumbleSetting_bool_v_1_0_x, 819 | setMumbleSetting_int_v_1_0_x, 820 | setMumbleSetting_double_v_1_0_x, 821 | setMumbleSetting_string_v_1_0_x, 822 | sendData_v_1_0_x, 823 | log_v_1_0_x, 824 | playSample_v_1_0_x }; 825 | } 826 | }; // namespace API_Mock 827 | --------------------------------------------------------------------------------