mirror of
https://github.com/kunkundi/crossdesk-server.git
synced 2026-10-25 17:21:59 +08:00
[fix] preserve join authentication compatibility for released clients
This commit is contained in:
1 parent
bd553897dd
commit
87255024f0
4 files changed
+51
-15
No files matched your search
@@ -264,9 +264,13 @@ sudo docker compose up -d --no-build
|
||||
|
||||
同一 WebSocket 连接只允许一次成功登录。后续 `login` 请求返回 `Already authenticated`,不会再注册设备、验证密码或更新在线状态;如需重新登录,请建立新连接。首次登录失败后仍可在原连接重试。
|
||||
|
||||
`query_user_id_list` 和 `join_transmission` 要求连接先完成登录,加入会话时还校验请求者身份与连接绑定。设备密码错误和设备 ID 不存在均返回 `Authentication failed`,不会返回设备列表或连接凭据。
|
||||
`query_user_id_list` 和 `join_transmission` 要求连接先完成登录,加入会话时还校验请求者身份与连接绑定。设备密码错误和设备 ID 不存在使用相同回复,不会返回设备列表或连接凭据。登录和查询保留 `Authentication failed`;Join 为兼容已发布桌面端,统一使用 `reason: "Incorrect password"` 和 `error_code: "AUTHENTICATION_FAILED"`,不恢复可区分 ID 是否存在的报错。
|
||||
|
||||
设备登录、查询和加入会话共用失败计数:15 分钟内,同一来源 IP 失败 20 次,或同一目标设备失败 5 次,将从达到阈值时起冷却 15 分钟。冷却期间跳过密码验证(包括正确密码),返回 `Too many authentication attempts` 和以秒计的 `retry_after`。重连、更换控制端 ID、切换接口或成功验证其他设备都不会清除失败计数;被限流的请求不延长冷却时间。已有远控会话不受影响。
|
||||
旧桌面端的空密码 Join(`ID@`)返回 `Incorrect password` / `PASSWORD_REQUIRED` 以打开密码框;不查询设备是否存在、不进行密码验证,也不消耗密码计算或失败额度。此提示不授予任何会话权限,不清除或延长已有冷却。新客户端应先收集密码再 Join。
|
||||
|
||||
Join 的其他拒绝(冷却、认证服务繁忙、未登录、远端不可用)统一保留旧客户端可识别的 `reason: "Remote unavailable"`,使其结束等待;旧版界面会显示设备离线。新客户端优先读取 `error_code`:`AUTHENTICATION_THROTTLED`、`CREDENTIAL_SERVICE_BUSY`、`CREDENTIAL_RATE_LIMITED`、`NOT_AUTHENTICATED` 或 `REMOTE_UNAVAILABLE`,并在存在 `retry_after` 时显示剩余秒数、禁止提前重试。认证诊断按错误码计数,空密码提示不计为鉴权失败。
|
||||
|
||||
设备登录、查询和加入会话共用失败计数:15 分钟内,同一来源 IP 失败 20 次,或同一目标设备失败 5 次,将从达到阈值时起冷却 15 分钟。冷却期间跳过密码验证(包括正确密码),返回 `Too many authentication attempts`(Join 使用上述兼容文案)及 `error_code: "AUTHENTICATION_THROTTLED"` 和以秒计的 `retry_after`。重连、更换控制端 ID、切换接口或成功验证其他设备都不会清除失败计数;被限流的请求不延长冷却时间。已有远控会话不受影响。
|
||||
|
||||
来源使用 TCP 对端 IP(不含端口),不信任请求字段或 `X-Forwarded-For`。同一 NAT 或反向代理后的连接共用来源额度。计数保存在单个服务进程内,重启后清空;多实例部署需要在可信网关或共享存储中补充统一限流。来源和目标各最多保留 16,384 条记录,容量耗尽时拒绝新增验证键,直到过期记录释放空间。
|
||||
|
||||
|
||||
+6
-2
@@ -246,9 +246,13 @@ Six-character passwords still have limited strength: source budgets and the fail
|
||||
|
||||
Each WebSocket connection allows one successful login. Subsequent `login` requests return `Already authenticated` without registering devices, verifying passwords or updating presence. Open a new connection to log in again; an initial failed login can still be retried on the same connection.
|
||||
|
||||
`query_user_id_list` and `join_transmission` require a logged-in connection; joining also checks that the requester's identity matches its connection binding. Wrong passwords and unknown device IDs both return `Authentication failed`, without a device list or connection credentials.
|
||||
`query_user_id_list` and `join_transmission` require a logged-in connection; joining also checks that the requester's identity matches its connection binding. Wrong passwords and unknown device IDs return the same failure, without a device list or connection credentials. Login and queries use `Authentication failed`; joins use the compatibility reasons below.
|
||||
|
||||
Device login, queries and joins share failure counts. Within a 15-minute window, 20 failures from one source IP or 5 failures against one target device start a 15-minute cooldown at the threshold. During cooldown, password verification is skipped even for correct passwords; responses include `Too many authentication attempts` and `retry_after` in seconds. Reconnecting, changing controller IDs, switching endpoints or successfully authenticating other devices does not clear failures. Blocked requests do not extend cooldown. Existing remote-control sessions remain active.
|
||||
Device login, queries and joins share failure counts. Within a 15-minute window, 20 failures from one source IP or 5 failures against one target device start a 15-minute cooldown at the threshold. During cooldown, password verification is skipped even for correct passwords; responses include `Too many authentication attempts` (joins use the compatibility reason below), `error_code: "AUTHENTICATION_THROTTLED"` and `retry_after` in seconds. Reconnecting, changing controller IDs, switching endpoints or successfully authenticating other devices does not clear failures. Blocked requests do not extend cooldown. Existing remote-control sessions remain active.
|
||||
|
||||
Join failures preserve reasons recognized by released desktops: wrong passwords and unknown IDs both return `Incorrect password` with `error_code: "AUTHENTICATION_FAILED"`. An empty-password Join (`ID@`) returns `Incorrect password` / `PASSWORD_REQUIRED` without looking up the target, verifying a password or consuming credential/failure budgets. It grants no access and never clears or extends a cooldown. New clients should collect a password before joining.
|
||||
|
||||
Other Join rejections retain the legacy `Remote unavailable` reason so old clients stop waiting (their UI displays offline). New clients must prefer `error_code`: `AUTHENTICATION_THROTTLED`, `CREDENTIAL_SERVICE_BUSY`, `CREDENTIAL_RATE_LIMITED`, `NOT_AUTHENTICATED`, `REMOTE_UNAVAILABLE`, displaying and honoring `retry_after` when present. Authentication diagnostics count error codes; empty-password prompts are not authentication failures. Login and query failures retain their generic reason.
|
||||
|
||||
The source is the TCP peer IP without its port; request fields and `X-Forwarded-For` are not trusted. Connections behind one NAT or reverse proxy share a source quota. Counters are local to one server process and reset on restart; multiple instances need coordinated limits in a trusted gateway or shared store. Each dimension holds at most 16,384 records and rejects new verification keys at capacity until expired records free space.
|
||||
|
||||
|
||||
@@ -12,6 +12,21 @@
|
||||
|
||||
namespace {
|
||||
|
||||
void SetFailure(json& message, const std::string& code,
|
||||
const std::string& reason) {
|
||||
message["error_code"] = code;
|
||||
message["reason"] = reason;
|
||||
// Released MiniRTC clients only recognize these legacy join reasons.
|
||||
// Keep unknown IDs indistinguishable from wrong passwords. New clients use
|
||||
// error_code so cooldowns and busy responses are not mistaken for offline.
|
||||
if (message.value("type", "") == "user_join_transmission") {
|
||||
message["reason"] =
|
||||
code == "AUTHENTICATION_FAILED" || code == "PASSWORD_REQUIRED"
|
||||
? "Incorrect password"
|
||||
: "Remote unavailable";
|
||||
}
|
||||
}
|
||||
|
||||
bool GetStringField(const json& j, const char* key, std::string& value) {
|
||||
if (!j.contains(key) || !j[key].is_string()) {
|
||||
return false;
|
||||
@@ -156,7 +171,7 @@ void SignalNegotiation::RunCredentialWork(websocketpp::connection_hdl hdl,
|
||||
};
|
||||
if (dispatch) {
|
||||
auto expired = [this, hdl, failure]() mutable {
|
||||
failure["reason"] = "Credential service busy";
|
||||
SetFailure(failure, "CREDENTIAL_SERVICE_BUSY", "Credential service busy");
|
||||
failure["retry_after"] = 1;
|
||||
send_msg_(hdl, failure);
|
||||
};
|
||||
@@ -165,7 +180,7 @@ void SignalNegotiation::RunCredentialWork(websocketpp::connection_hdl hdl,
|
||||
}
|
||||
const int delay = admit();
|
||||
if (delay > 0) {
|
||||
failure["reason"] = "Too many credential requests";
|
||||
SetFailure(failure, "CREDENTIAL_RATE_LIMITED", "Too many credential requests");
|
||||
failure["retry_after"] = delay;
|
||||
send_msg_(hdl, failure);
|
||||
} else {
|
||||
@@ -177,7 +192,7 @@ void SignalNegotiation::AuthenticateDevice(
|
||||
websocketpp::connection_hdl hdl, const std::string& source_address,
|
||||
const std::string& device_id, const std::string& password, json failure,
|
||||
const CredentialDispatch& dispatch, std::function<void()> on_verified) {
|
||||
failure["reason"] = "Authentication failed";
|
||||
SetFailure(failure, "AUTHENTICATION_FAILED", "Authentication failed");
|
||||
if (!ValidDeviceCredentials(device_id, password)) {
|
||||
send_msg_(hdl, failure);
|
||||
return;
|
||||
@@ -206,7 +221,8 @@ void SignalNegotiation::AuthenticateDevice(
|
||||
on_verified();
|
||||
} else {
|
||||
if (result.retry_after > 0) {
|
||||
failure["reason"] = "Too many authentication attempts";
|
||||
SetFailure(failure, "AUTHENTICATION_THROTTLED",
|
||||
"Too many authentication attempts");
|
||||
failure["retry_after"] = result.retry_after;
|
||||
}
|
||||
send_msg_(hdl, failure);
|
||||
@@ -617,7 +633,15 @@ bool SignalNegotiation::join_transmission(websocketpp::connection_hdl hdl,
|
||||
{"transmission_id", transmission_id},
|
||||
{"status", "failed"}};
|
||||
if (hdl.expired() || transmission_manager_->GetUserId(hdl) != user_id) {
|
||||
failure["reason"] = "Not authenticated";
|
||||
SetFailure(failure, "NOT_AUTHENTICATED", "Not authenticated");
|
||||
send_msg_(hdl, failure);
|
||||
return true;
|
||||
}
|
||||
if (password.empty() && ValidDeviceCredentials(transmission_id, password)) {
|
||||
// Older desktops probe with ID@ to open their password dialog. This is
|
||||
// not a password guess: never read the target record, spend a credential
|
||||
// worker slot, or consume a failure budget for it.
|
||||
SetFailure(failure, "PASSWORD_REQUIRED", "Password required");
|
||||
send_msg_(hdl, failure);
|
||||
return true;
|
||||
}
|
||||
@@ -634,8 +658,8 @@ bool SignalNegotiation::join_transmission(websocketpp::connection_hdl hdl,
|
||||
transmission_id.c_str());
|
||||
json message = {{"type", "user_join_transmission"},
|
||||
{"transmission_id", transmission_id},
|
||||
{"status", "failed"},
|
||||
{"reason", "Remote unavailable"}};
|
||||
{"status", "failed"}};
|
||||
SetFailure(message, "REMOTE_UNAVAILABLE", "Remote unavailable");
|
||||
send_msg_(hdl, message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1011,6 +1011,7 @@ void SignalServer::SendMsg(websocketpp::connection_hdl hdl, json message) {
|
||||
}
|
||||
std::string login_id;
|
||||
const auto failure_reason = message.value("reason", "");
|
||||
const auto failure_code = message.value("error_code", "");
|
||||
const bool reconnected = type == "login" && message.value("reconnected", false);
|
||||
const bool reconnect_rejected = type == "login" && message.value("reconnect_rejected", false);
|
||||
if (type == "login" && message.value("status", "") == "success") {
|
||||
@@ -1019,7 +1020,7 @@ void SignalServer::SendMsg(websocketpp::connection_hdl hdl, json message) {
|
||||
}
|
||||
auto payload = message.dump();
|
||||
server_.get_io_service().post(
|
||||
[this, hdl, payload = std::move(payload), login_id, failure_reason,
|
||||
[this, hdl, payload = std::move(payload), login_id, failure_reason, failure_code,
|
||||
reconnected, reconnect_rejected] {
|
||||
--pending_sends_;
|
||||
auto it = connections_.find(hdl);
|
||||
@@ -1034,11 +1035,14 @@ void SignalServer::SendMsg(websocketpp::connection_hdl hdl, json message) {
|
||||
it->second->last_heartbeat = Clock::now();
|
||||
}
|
||||
if (reconnect_rejected) ++diagnostics_.reconnect_failed;
|
||||
if (failure_reason == "Authentication failed")
|
||||
if (failure_code == "AUTHENTICATION_FAILED" ||
|
||||
(failure_code.empty() && failure_reason == "Authentication failed"))
|
||||
++diagnostics_.authentication_failed;
|
||||
else if (failure_reason == "Credential service busy")
|
||||
else if (failure_code == "CREDENTIAL_SERVICE_BUSY" ||
|
||||
(failure_code.empty() && failure_reason == "Credential service busy"))
|
||||
++diagnostics_.credential_busy;
|
||||
else if (failure_reason == "Too many authentication attempts")
|
||||
else if (failure_code == "AUTHENTICATION_THROTTLED" ||
|
||||
(failure_code.empty() && failure_reason == "Too many authentication attempts"))
|
||||
++diagnostics_.authentication_throttled;
|
||||
websocketpp::lib::error_code ec;
|
||||
auto con = server_.get_con_from_hdl(hdl, ec);
|
||||
|
||||
Reference in new issue
Block a user