mirror of
https://github.com/anope/anope.git
synced 2026-06-12 17:04:47 +02:00
334 lines
8.9 KiB
C++
334 lines
8.9 KiB
C++
// Anope IRC Services <https://www.anope.org/>
|
|
//
|
|
// Copyright (C) 2003-2026 Anope Contributors
|
|
//
|
|
// Anope is free software. You can use, modify, and/or distribute it under the
|
|
// terms of version 2 of the GNU General Public License. See docs/LICENSE.txt
|
|
// for the complete terms of this license and docs/AUTHORS.txt for a list of
|
|
// contributors.
|
|
//
|
|
// Based on the original code of Epona by Lara
|
|
// Based on the original code of Services by Andy Church
|
|
//
|
|
// SPDX-License-Identifier: GPL-2.0-only
|
|
|
|
#include "module.h"
|
|
#include "modules/rpc.h"
|
|
#include "modules/httpd.h"
|
|
|
|
#include "yyjson/yyjson.c"
|
|
|
|
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
|
|
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
|
|
|
|
inline Anope::string yyjson_get_astr(yyjson_val *val, const char *key)
|
|
{
|
|
const auto *str = yyjson_get_str(yyjson_obj_get(val, key));
|
|
return str ? str : "";
|
|
}
|
|
|
|
class JSONRPCServiceInterface final
|
|
: public RPC::ServiceInterface
|
|
, public HTTP::Page
|
|
{
|
|
private:
|
|
static std::pair<yyjson_mut_doc *, yyjson_mut_val *> CreateReply(const Anope::string &id)
|
|
{
|
|
auto* doc = yyjson_mut_doc_new(nullptr);
|
|
|
|
auto* root = yyjson_mut_obj(doc);
|
|
yyjson_mut_doc_set_root(doc, root);
|
|
|
|
yyjson_mut_obj_add_str(doc, root, "jsonrpc", "2.0");
|
|
|
|
if (id.empty())
|
|
yyjson_mut_obj_add_null(doc, root, "id");
|
|
else
|
|
yyjson_mut_obj_add_strn(doc, root, "id", id.c_str(), id.length());
|
|
|
|
return { doc, root };
|
|
}
|
|
|
|
static void SendError(HTTP::Reply &reply, int64_t code, const Anope::string &message, const Anope::string &id = "")
|
|
{
|
|
Log(LOG_DEBUG) << "JSON-RPC error " << code << ": " << message;
|
|
|
|
// {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}
|
|
auto [doc, root] = CreateReply(id);
|
|
|
|
auto *error = yyjson_mut_obj(doc);
|
|
yyjson_mut_obj_add_sint(doc, error, "code", code);
|
|
yyjson_mut_obj_add_strn(doc, error, "message", message.c_str(), message.length());
|
|
|
|
yyjson_mut_obj_add_val(doc, root, "error", error);
|
|
|
|
auto *json = yyjson_mut_write(doc, YYJSON_WRITE_ALLOW_INVALID_UNICODE | YYJSON_WRITE_NEWLINE_AT_END, nullptr);
|
|
if (json)
|
|
{
|
|
reply.Write(json);
|
|
free(json);
|
|
}
|
|
yyjson_mut_doc_free(doc);
|
|
}
|
|
|
|
static yyjson_mut_val *SerializeElement(yyjson_mut_doc *doc, const RPC::Value &value);
|
|
|
|
static void SerializeArray(yyjson_mut_doc *doc, yyjson_mut_val *value, const RPC::Array &array)
|
|
{
|
|
for (const auto &elem : array.GetReplies())
|
|
{
|
|
auto *obj = SerializeElement(doc, elem);
|
|
yyjson_mut_arr_add_val(value, obj);
|
|
}
|
|
}
|
|
|
|
static void SerializeMap(yyjson_mut_doc *doc, yyjson_mut_val *value, const RPC::Map &map)
|
|
{
|
|
for (const auto &[k, v] : map.GetReplies())
|
|
{
|
|
auto *obj = SerializeElement(doc, v);
|
|
yyjson_mut_obj_add_val(doc, value, k.c_str(), obj);
|
|
}
|
|
}
|
|
|
|
public:
|
|
// The number of bits that can be represented using the native integer type.
|
|
static unsigned integer_bits;
|
|
|
|
JSONRPCServiceInterface(Module *creator)
|
|
: RPC::ServiceInterface(creator)
|
|
, HTTP::Page("/jsonrpc", "application/json")
|
|
{
|
|
}
|
|
|
|
bool OnRequest(HTTP::Provider *provider, const Anope::string &page_name, HTTP::Client *client, HTTP::Message &message, HTTP::Reply &reply) override
|
|
{
|
|
yyjson_read_err error;
|
|
const auto flags = YYJSON_READ_ALLOW_TRAILING_COMMAS | YYJSON_READ_ALLOW_INVALID_UNICODE;
|
|
auto *doc = yyjson_read_opts(const_cast<char *>(message.content.c_str()), message.content.length(), flags, nullptr, &error);
|
|
if (!doc)
|
|
{
|
|
SendError(reply, RPC::ERR_PARSE_ERROR, Anope::Format("JSON parse error #%u: %s", error.code, error.msg));
|
|
return true;
|
|
}
|
|
|
|
auto *root = yyjson_doc_get_root(doc);
|
|
if (!yyjson_is_obj(root))
|
|
{
|
|
// TODO: handle an array of JSON-RPC requests
|
|
yyjson_doc_free(doc);
|
|
SendError(reply, RPC::ERR_INVALID_REQUEST, "Wrong JSON root element");
|
|
return true;
|
|
}
|
|
|
|
const auto id = yyjson_get_astr(root, "id");
|
|
const auto jsonrpc = yyjson_get_astr(root, "jsonrpc");
|
|
if (!jsonrpc.empty() && jsonrpc != "2.0")
|
|
{
|
|
yyjson_doc_free(doc);
|
|
SendError(reply, RPC::ERR_INVALID_REQUEST, "Unsupported JSON-RPC version: " + jsonrpc, id);
|
|
return true;
|
|
}
|
|
|
|
RPC::Request request(reply);
|
|
request.id = id;
|
|
request.name = yyjson_get_astr(root, "method");
|
|
if (request.name.empty())
|
|
{
|
|
yyjson_doc_free(doc);
|
|
SendError(reply, RPC::ERR_INVALID_REQUEST, "No JSON-RPC method was specified", id);
|
|
return true;
|
|
}
|
|
|
|
if (!tokens.empty())
|
|
{
|
|
auto it = message.headers.find("Authorization");
|
|
if (it == message.headers.end() || !CanExecute(it->second, request.name))
|
|
{
|
|
SendError(reply, RPC::ERR_METHOD_NOT_FOUND, "No authorization for method: " + request.name, id);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
auto *params = yyjson_obj_get(root, "params");
|
|
size_t idx, max;
|
|
yyjson_val *val;
|
|
yyjson_arr_foreach(params, idx, max, val)
|
|
{
|
|
const auto *str = yyjson_get_str(val);
|
|
request.data.push_back(str ? str : "");
|
|
}
|
|
|
|
yyjson_doc_free(doc);
|
|
|
|
ServiceReference<RPC::Event> event(RPC_EVENT, request.name);
|
|
if (!event)
|
|
{
|
|
SendError(reply, RPC::ERR_METHOD_NOT_FOUND, "Method not found: " + request.name, id);
|
|
return true;
|
|
}
|
|
|
|
if (request.data.size() < event->GetMinParams())
|
|
{
|
|
auto error = Anope::Format("Not enough parameters for %s (given %zu, expected %zu)",
|
|
request.name.c_str(), request.data.size(), event->GetMinParams());
|
|
SendError(reply, RPC::ERR_INVALID_PARAMS, error, id);
|
|
return true;
|
|
}
|
|
|
|
if (!event->Run(this, client, request))
|
|
return false;
|
|
|
|
this->Reply(request);
|
|
return true;
|
|
}
|
|
|
|
void Reply(RPC::Request &request) override
|
|
{
|
|
if (request.GetError())
|
|
{
|
|
SendError(request.reply, request.GetError()->first, request.GetError()->second, request.id);
|
|
return;
|
|
}
|
|
|
|
// {"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}
|
|
auto [doc, root] = CreateReply(request.id);
|
|
|
|
if (request.GetRoot())
|
|
{
|
|
auto *result = SerializeElement(doc, request.GetRoot().value());
|
|
yyjson_mut_obj_add_val(doc, root, "result", result);
|
|
}
|
|
else
|
|
yyjson_mut_obj_add_null(doc, root, "result");
|
|
|
|
auto *json = yyjson_mut_write(doc, YYJSON_WRITE_ALLOW_INVALID_UNICODE | YYJSON_WRITE_NEWLINE_AT_END, nullptr);
|
|
if (json)
|
|
{
|
|
request.reply.Write(json);
|
|
free(json);
|
|
}
|
|
yyjson_mut_doc_free(doc);
|
|
}
|
|
};
|
|
|
|
yyjson_mut_val *JSONRPCServiceInterface::SerializeElement(yyjson_mut_doc *doc, const RPC::Value &value)
|
|
{
|
|
yyjson_mut_val *elem;
|
|
std::visit(overloaded
|
|
{
|
|
[&doc, &elem](const RPC::Array &a)
|
|
{
|
|
elem = yyjson_mut_arr(doc);
|
|
SerializeArray(doc, elem, a);
|
|
},
|
|
[&doc, &elem](const RPC::Map &m)
|
|
{
|
|
elem = yyjson_mut_obj(doc);
|
|
SerializeMap(doc, elem, m);
|
|
},
|
|
[&doc, &elem](const Anope::string &s)
|
|
{
|
|
elem = yyjson_mut_strn(doc, s.c_str(), s.length());
|
|
},
|
|
[&doc, &elem](std::nullptr_t)
|
|
{
|
|
elem = yyjson_mut_null(doc);
|
|
},
|
|
[&doc, &elem](bool b)
|
|
{
|
|
elem = yyjson_mut_bool(doc, b);
|
|
},
|
|
[&doc, &elem](double d)
|
|
{
|
|
elem = yyjson_mut_real(doc, d);
|
|
},
|
|
[&doc, &elem](int64_t i)
|
|
{
|
|
auto bits = std::floor(std::log2(abs(i))) + 1;
|
|
if (bits <= integer_bits)
|
|
{
|
|
// We can fit this into an integer.
|
|
elem = yyjson_mut_int(doc, i);
|
|
}
|
|
else
|
|
{
|
|
// We need to convert this to a string.
|
|
auto s = Anope::ToString(i);
|
|
elem = yyjson_mut_strncpy(doc, s.c_str(), s.length());
|
|
}
|
|
},
|
|
[&doc, &elem](uint64_t u)
|
|
{
|
|
auto bits = std::floor(std::log2(u)) + 1;
|
|
if (bits <= integer_bits)
|
|
{
|
|
// We can fit this into an integer.
|
|
elem = yyjson_mut_uint(doc, u);
|
|
}
|
|
else
|
|
{
|
|
// We need to convert this to a string.
|
|
auto s = Anope::ToString(u);
|
|
elem = yyjson_mut_strncpy(doc, s.c_str(), s.length());
|
|
}
|
|
},
|
|
}, value.Get());
|
|
return elem;
|
|
}
|
|
|
|
unsigned JSONRPCServiceInterface::integer_bits = 64;
|
|
|
|
class ModuleJSONRPC final
|
|
: public Module
|
|
{
|
|
private:
|
|
ServiceReference<HTTP::Provider> httpref;
|
|
JSONRPCServiceInterface jsonrpcinterface;
|
|
|
|
public:
|
|
ModuleJSONRPC(const Anope::string &modname, const Anope::string &creator)
|
|
: Module(modname, creator, EXTRA | VENDOR)
|
|
, httpref(HTTP_PROVIDER)
|
|
, jsonrpcinterface(this)
|
|
{
|
|
}
|
|
|
|
~ModuleJSONRPC() override
|
|
{
|
|
if (httpref)
|
|
httpref->UnregisterPage(&jsonrpcinterface);
|
|
}
|
|
|
|
void OnReload(Configuration::Conf &conf) override
|
|
{
|
|
if (httpref)
|
|
httpref->UnregisterPage(&jsonrpcinterface);
|
|
|
|
const auto &modconf = conf.GetModule(this);
|
|
JSONRPCServiceInterface::integer_bits = modconf.Get<unsigned>("integer_bits", "64");
|
|
|
|
this->httpref.SetServiceName(modconf.Get<const Anope::string>("server", "httpd/main"));
|
|
if (!httpref)
|
|
throw ConfigException("Unable to find http reference, is httpd loaded?");
|
|
|
|
jsonrpcinterface.tokens.clear();
|
|
|
|
for (const auto &[_, block] : modconf.GetBlocks("token"))
|
|
{
|
|
RPC::Token token;
|
|
token.token = block.Get<const Anope::string>("token");
|
|
if (!token.token.empty())
|
|
{
|
|
token.token_hash = block.Get<const Anope::string>("token_hash");
|
|
spacesepstream(block.Get<const Anope::string>("methods")).GetTokens(token.methods);
|
|
jsonrpcinterface.tokens.emplace_back(token);
|
|
}
|
|
}
|
|
|
|
httpref->RegisterPage(&jsonrpcinterface);
|
|
}
|
|
};
|
|
|
|
MODULE_INIT(ModuleJSONRPC)
|