我在上一篇文章中介绍了 Kamailio 的 JSCON-RPC over HTTP,旨在介绍 RPC 的使用方法,但被指出不安全且不适合生产。现在,让我们看看如何纠正这一问题。本文章的更新配置可在此处找到:
https://github.com/whosgonna/kamailio-httprpc/tree/main/ex2-secure
这个较新的示例将添加:
- 仅通过专用侦听端口接受 HTTP。然后,该端口只能暴露给本地网络,人们将无法通过 Internet 在您的 TCP SIP 端口上访问管理服务。
- 使用摘要式身份验证以及预设的用户名和密码来控制访问。
- 限制公开的 RPC 方法的数量。即使 LAN 上的某人连接到服务器,他们也无法停止 Kamailio 或检索预处理器定义等。
唯一推荐的其他方法是使用 TLS,但这超出了本文的范围。
与之前版本的差异
那么,有什么不同呢?首先,比之前的版本长很多,有近 100 行,而之前的版本有 19 行。当然它还包含更多的功能。
用户名和密码定义
首先,它从环境变量加载用户名和密码:
#!KAMAILIO
#!defenv RPC_USER
#!defenv RPC_PASS
随附的 docker-compose 文件确实为这些设置了默认值,但也可以通过 .env 文件等进行覆盖。
附加模块
第一个最小模型只使用了 sl、json、xhttp 和 jsonrpcs。这个版本加载了更多的模块:
loadmodule "pv"
loadmodule "xlog"
loadmodule "sl"
loadmodule "json"
loadmodule "xhttp"
loadmodule "jsonrpcs"
loadmodule "tcpops"
loadmodule "htable"
loadmodule "auth"
loadmodule "kex"
这些都不是什么稀奇事。我们添加了 xlog 模块,以便在 Kamailio 的输出中为自己提供一些信息。auth 模块为我们提供了用户身份验证功能,我们将在 htable 中定义允许在 JSON-RPC over HTTP 上使用的方法。之所以添加 kex 模块,是因为它有用于获取服务器统计数据的 RPC 方法,我们将把它作为一个允许使用的示例。
网络配置
由于我们希望 HTTP 接口使用默认 SIP 端口 5060 以外的端口,因此需要定义侦听器。这是对上一个示例中定义的 TCP 参数的补充:
tcp_accept_no_cl = yes
http_reply_parse = yes
listen = udp:eth0:5060
listen = tcp:eth0:5060
socket_workers = 2
listen = tcp:eth0:8081
listen
端口的定义在8081
定义方式上没有什么特别之处,除了它使用 TCP 之外。之所以socket_workers
减少,是因为虽然生产 Kamailio 服务器可能会处理大量 SIP 流量,但它可能不会处理太多 http 流量(不支持 Web 套接字)。
模块参数
modparam("jsonrpcs", "transport", 0)
modparam("htable", "htable", "rpc_allow=>size=4");
jsonrpcs 模块的传输参数与之前相同(0 – 使用所有传输)。添加了名为 rpc_allow
的 htable。文件中的 htable:mod-init
事件路由设置了它的默认配置。
请求路由
就像前面的例子一样,这个路由并不是用来处理任何 SIP 流量的,它只是一个出口:
request_route {
exit;
}
HTTP 请求事件路由
这里的情况开始变得有点有趣,但为了更好地阅读和理解,我们并没有在这条路径中加入大量逻辑,而是将其划分为不同的路径。每个路由要么通过检查并返回,要么失败并退出。如果所有检查都通过并返回,则调用 jsonrpc_dispatch()
,执行 RPC 请求。
event_route[xhttp:request] {
route(XHTTP_PORT_CHECK);
route(XHTTP_RPC_AUTH);
route(VALIDATE_RPC_METHOD);
jsonrpc_dispatch();
}
端口检查
这很简单。如果我们在 8081 端口以外的端口收到 TCP 检查,我们就会关闭套接字。我们不会发送任何响应 – 任何人都没有理由向 5060 端口发送 HTTP 请求,所以我们甚至不会响应。
route[XHTTP_PORT_CHECK] {
if ( $Rp != 8081 ) {
xcrit("HTTP Request not received on port 8081. reject\n");
tcp_close_connection();
exit;
}
}
用户身份验证
这是配置文件中最复杂的部分,其中包含几项内容。
首先,我们要检查是否提供了用户名以及用户名是否正确。如果没有提供用户名,我们将继续路由,但如果给出的用户名与已知用户不匹配,我们将回复 403 未授权。不使用 sl_send_reply() 或 send_reply(),而是使用 http_reply()。这样就可以在消息中定义 Content-Type 头和正文。如果我们想花时间写出 JSON 对象(并使用 application/json 而不是 text/plain 的内容类型),那么在 200 OK 中格式化适当的 JSON-RPC 失败消息可能会更合适。
接下来,将使用 pv_www_authenticate()检查凭据,它允许我们输入静态realm 和密码进行验证。它可以返回多个值,但我们只对其中几个感兴趣。
首先,也许最明显的是 pv_www_authenticate 返回 true 且用户已通过验证的情况。consumer_credentials() 会将凭据从消息中删除,以免它们在下一跳中被带到下游。由于这是对调用者的回复,因此严格来说可能并无必要,但却是一个很好的做法。
如果 pv_www_authenticate()失败,我们会检查返回代码(存储在 $rc),以了解失败原因。如果返回值为 -2,则表示密码不正确。就像用户不正确一样,我们将发送 403 Unauthorized(未授权)响应。事实上,这是一个完全相同的响应,因为我们并不想说明失败的原因是用户名还是密码。请求者只需知道其中至少有一个不正确即可。
如果没有收到用户名和密码(这对第一次请求来说是正常的),我们将使用 www_challenge() 发出验证挑战。这样,客户端就会知道预期的境界是什么。
对于其他任何失败,我们都会返回一个更普通的 503 服务器错误信息。在这个示例中,我们没有必要进一步研究它。
route[XHTTP_RPC_AUTH]{
if ( $au != $null && $au != $def(RPC_USER) ) {
xerr("Invalid RPC user : [$au].\n");
xhttp_reply(
"403", "Unauthorized", "text/plain", "403 Unauthorized\r\n"
);
exit;
}
if ( !pv_www_authenticate("$Ri", "$def(RPC_PASS)", "0") ) {
switch ( $rc ) {
case -2:
xerr("Invalid RPC password for user $au\n");
xhttp_reply(
"403", "Unauthorized", "text/plain", "403 Unauthorized\r\n"
);
exit;
case -5:
xinfo("HTTP request with no crednetials. Send challenge\n");
www_challenge("$Ri", "0");
exit;
default:
xinfo("Misc. WWW auth failure. $$rc [$rc]\n");
xhttp_reply(
"503", "Server Error", "text/plain", "503 Server Error\r\n"
);
exit;
}
}
consume_credentials();
}
检查 RPC 方法是否允许
验证请求后,我们执行请求前的最后一步是检查 RPC 方法是否已被允许。这里显示了两个路径。VALIDATE_RPC_METHOD 进行实际检查。htable:mod-init 事件路由在 Kamailio 启动时运行,以填充 rpc_allow htable。
route[VALIDATE_RPC_METHOD] {
json_get_field("$rb", "method", "$avp(rpc_method)");
$avp(rpc_method) = $(avp(rpc_method){s.unquote});
if ( $sht(rpc_allow=>$avp(rpc_method)) == 1 ) {
return;
}
xhttp_reply("403", "Not Allowed", "text/plain", "Not Allowed\n");
exit;
}
event_route[htable:mod-init] {
$sht(rpc_allow=>stats.fetch) = 1;
$sht(rpc_allow=>core.version) = 1;
$sht(rpc_allow=>mod.stats) = 1;
}
json_get_field() 会从请求中提取方法。然后,我们检查该方法是否在 rpc_allow htable 中。如果是,我们就返回并执行请求。如果不在,则向客户端返回 403 Not Allowed。
使用方法
既然我们已经介绍了配置,那么该如何使用它呢?让我们来看几个curl示例:
获取Kamailio版本(Get Kamailio Version)–与上一篇文章相同,但使用了验证参数。注意必须定义–digest:
[kaufmania]$ curl -X POST \
-d '{"jsonrpc":"2.0","method":"core.version","id":"1"}' \
-s --digest -u rpcuser:rpcpass \
http://localhost:8081
{
"jsonrpc": "2.0",
"result": "kamailio 5.6.3 (x86_64/linux) ",
"id": "1"
}
当然,如果我们想检查错误的凭据以确认身份验证检查是否正常工作:
[kaufmania]$ curl -X POST \
-d '{"jsonrpc":"2.0","method":"core.version","id":"1"}' \
-s --digest -u rpcuser:badpass \
http://localhost:8081
403 Unauthorized
如果我们尝试其中一种不允许的方法(例如关闭服务器)怎么办?
[kaufmania]$ curl -X POST \
-d '{"jsonrpc":"2.0","method":"core.kill","id":"1"}' \
-s --digest -u rpcuser:rpcpass \
http://localhost:8081
Not Allowed
如果我们想查看统计数据?
[kaufmania]$ curl -X POST \
-d '{"jsonrpc":"2.0","method":"stats.fetch","id":"1","params":["all"]}' \
-s --digest -u rpcuser:rpcpass \
http://localhost:8081
{
"jsonrpc": "2.0",
"result": {
"core.bad_URIs_rcvd": "0",
"core.bad_msg_hdr": "0",
"core.drop_replies": "0",
"core.drop_requests": "0",
"core.err_replies": "0",
"core.err_requests": "0",
. . .
结论
这就构成了在实际应用中可行的 HTTP RPC 配置。发送命令以重新加载特定的Kamailio配置或以编程方式远程检索特定数据的能力极具价值,凸显了Kamailio的灵活性。
原文:https://kaufmania.wordpress.com/2023/09/13/securing-kamailios-json-rpc-over-http/
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/35242.html