Добавить SendSignal и remote AddBlock через homeserver

This commit is contained in:
AidarKC
2026-06-28 11:20:51 +04:00
parent c397c28acb
commit aa02e92e4d
19 changed files with 1329 additions and 224 deletions
@@ -89,6 +89,7 @@ import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListConta
import server.logic.ws_protocol.JSON.messages.Net_AckSessionDelivery_Handler;
import server.logic.ws_protocol.JSON.messages.Net_CallInviteBroadcast_Handler;
import server.logic.ws_protocol.JSON.messages.Net_CallSignalToSession_Handler;
import server.logic.ws_protocol.JSON.messages.Net_SendSignal_Handler;
import server.logic.ws_protocol.JSON.messages.Net_ReceiveIncomingMessage_Handler;
import server.logic.ws_protocol.JSON.messages.Net_SendDirectMessage_Handler;
import server.logic.ws_protocol.JSON.messages.Net_SendMessagePair_Handler;
@@ -97,6 +98,7 @@ import server.logic.ws_protocol.JSON.messages.Net_UpsertPushToken_Handler;
import server.logic.ws_protocol.JSON.messages.entyties.Net_AckSessionDelivery_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_CallInviteBroadcast_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_CallSignalToSession_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendDirectMessage_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Request;
@@ -196,6 +198,7 @@ public final class JsonHandlerRegistry {
Map.entry("AckSessionDelivery", new Net_AckSessionDelivery_Handler()),
Map.entry("CallInviteBroadcast", new Net_CallInviteBroadcast_Handler()),
Map.entry("CallSignalToSession", new Net_CallSignalToSession_Handler()),
Map.entry("SendSignal", new Net_SendSignal_Handler()),
// --- system ---
Map.entry("Ping", new Net_Ping_Handler()),
@@ -274,6 +277,7 @@ public final class JsonHandlerRegistry {
Map.entry("AckSessionDelivery", Net_AckSessionDelivery_Request.class),
Map.entry("CallInviteBroadcast", Net_CallInviteBroadcast_Request.class),
Map.entry("CallSignalToSession", Net_CallSignalToSession_Request.class),
Map.entry("SendSignal", Net_SendSignal_Request.class),
// --- system ---
Map.entry("Ping", Net_Ping_Request.class),
@@ -0,0 +1,220 @@
package server.logic.ws_protocol.JSON.messages;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.Base64Ws;
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Response;
import server.logic.ws_protocol.JSON.push.WsEventSender;
import server.logic.ws_protocol.JSON.utils.AuthKeyUtils;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUserEntry;
import utils.crypto.Ed25519Util;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
public class Net_SendSignal_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_SendSignal_Handler.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final String TARGET_MODE_SINGLE = "single_session";
private static final String TARGET_MODE_ALL = "all_sessions";
private static final long ALLOWED_SKEW_MS = 30_000L;
@Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
Net_SendSignal_Request req = (Net_SendSignal_Request) baseRequest;
if (ctx == null || !ctx.isAuthenticatedUser() || ctx.getActiveSession() == null) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация");
}
String fromLogin = safe(ctx.getLogin());
String fromSessionId = safe(ctx.getSessionId());
ActiveSessionEntry activeSession = ctx.getActiveSession();
String toRequest = safe(req.getToLogin());
String targetMode = safe(req.getTargetMode()).toLowerCase();
String targetSessionId = safe(req.getTargetSessionId());
String signalType = safe(req.getSignalType());
String signalRequestId = safe(req.getSignalRequestId());
String data = req.getData() == null ? "" : req.getData();
Long timeMs = req.getTimeMs();
String sessionSignatureB64 = safe(req.getSessionSignatureB64());
String clientSignatureB64 = safe(req.getClientSignatureB64());
if (toRequest.isBlank() || signalType.isBlank() || signalRequestId.isBlank() || timeMs == null || sessionSignatureB64.isBlank()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "toLogin/targetMode/signalType/signalRequestId/timeMs/sessionSignatureB64 обязательны");
}
if (!TARGET_MODE_SINGLE.equals(targetMode) && !TARGET_MODE_ALL.equals(targetMode)) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_TARGET_MODE", "Поддерживаются targetMode=single_session или all_sessions");
}
if (TARGET_MODE_SINGLE.equals(targetMode) && targetSessionId.isBlank()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "Для single_session нужен targetSessionId");
}
if (TARGET_MODE_ALL.equals(targetMode) && !targetSessionId.isBlank()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "Для all_sessions нельзя передавать targetSessionId");
}
long nowMs = System.currentTimeMillis();
if (Math.abs(nowMs - timeMs) > ALLOWED_SKEW_MS) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "TIME_SKEW", "Время клиента отличается от сервера более чем на 30 секунд");
}
SolanaUserEntry senderUser = ctx.getSolanaUser();
if (senderUser == null || senderUser.getClientKey() == null || senderUser.getClientKey().isBlank()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.SERVER_DATA_ERROR, "NO_CLIENT_KEY", "Для пользователя не найден client key");
}
SolanaUserEntry targetUser = SolanaUsersDAO.getInstance().getByLogin(toRequest);
if (targetUser == null) {
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
}
String toLogin = safe(targetUser.getLogin());
String digestB64;
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digestB64 = Base64Ws.encode(digest.digest(data.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
log.warn("SendSignal: failed to hash signal data (fromLogin={}, signalType={})", fromLogin, signalType, e);
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_DATA", "Не удалось обработать data");
}
String sessionPreimage = buildSessionPreimage(fromLogin, fromSessionId, toLogin, targetMode, targetSessionId, signalType, signalRequestId, timeMs, digestB64);
if (!verifySignature(activeSession.getSessionKey(), sessionPreimage, sessionSignatureB64, "sessionKey")) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_SESSION_SIGNATURE", "Некорректная подпись session key");
}
if (!clientSignatureB64.isBlank()) {
String clientPreimage = buildClientPreimage(fromLogin, fromSessionId, toLogin, targetMode, targetSessionId, signalType, signalRequestId, timeMs, digestB64);
if (!verifySignature(senderUser.getClientKey(), clientPreimage, clientSignatureB64, "clientKey")) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_CLIENT_SIGNATURE", "Некорректная подпись client key");
}
}
List<ConnectionContext> targets = resolveTargets(targetMode, toLogin, targetSessionId);
if (targets.isEmpty()) {
String code = TARGET_MODE_SINGLE.equals(targetMode) ? "SESSION_NOT_FOUND" : "NO_TARGET_SESSIONS";
String msg = TARGET_MODE_SINGLE.equals(targetMode) ? "Целевая сессия не найдена" : "Нет активных сессий для доставки сигнала";
return NetExceptionResponseFactory.error(req, 404, code, msg);
}
List<String> deliveredSessionIds = new ArrayList<>();
for (ConnectionContext targetCtx : targets) {
String eventId = server.logic.ws_protocol.JSON.utils.NetIdGenerator.eventId("evt");
ObjectNode payload = MAPPER.createObjectNode();
payload.put("eventId", eventId);
payload.put("fromLogin", fromLogin);
payload.put("fromSessionId", fromSessionId);
payload.put("toLogin", toLogin);
payload.put("targetMode", targetMode);
payload.put("targetSessionId", safe(targetCtx.getSessionId()));
payload.put("signalType", signalType);
payload.put("signalRequestId", signalRequestId);
payload.put("data", data);
payload.put("timeMs", timeMs);
payload.put("sessionSignatureB64", sessionSignatureB64);
payload.put("clientSignatureB64", clientSignatureB64);
payload.put("dataSha256B64", digestB64);
if (WsEventSender.sendEvent(targetCtx, "IncomingSignal", eventId, payload)) {
deliveredSessionIds.add(safe(targetCtx.getSessionId()));
}
}
if (deliveredSessionIds.isEmpty()) {
return NetExceptionResponseFactory.error(req, 404, "DELIVERY_FAILED", "Не удалось доставить сигнал ни в одну целевую сессию");
}
Net_SendSignal_Response resp = new Net_SendSignal_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
resp.setDeliveredCount(deliveredSessionIds.size());
resp.setDeliveredSessionIds(deliveredSessionIds);
return resp;
}
private static String buildSessionPreimage(String fromLogin,
String fromSessionId,
String toLogin,
String targetMode,
String targetSessionId,
String signalType,
String signalRequestId,
long timeMs,
String dataSha256B64) {
return "SEND_SIGNAL_SESSION:"
+ fromLogin + ":"
+ fromSessionId + ":"
+ toLogin + ":"
+ targetMode + ":"
+ targetSessionId + ":"
+ signalType + ":"
+ signalRequestId + ":"
+ timeMs + ":"
+ dataSha256B64;
}
private static String buildClientPreimage(String fromLogin,
String fromSessionId,
String toLogin,
String targetMode,
String targetSessionId,
String signalType,
String signalRequestId,
long timeMs,
String dataSha256B64) {
return "SEND_SIGNAL_CLIENT:"
+ fromLogin + ":"
+ fromSessionId + ":"
+ toLogin + ":"
+ targetMode + ":"
+ targetSessionId + ":"
+ signalType + ":"
+ signalRequestId + ":"
+ timeMs + ":"
+ dataSha256B64;
}
private static boolean verifySignature(String publicKeyValue, String preimage, String signatureB64, String fieldName) throws Exception {
byte[] publicKey32 = AuthKeyUtils.parseEd25519PublicKey(publicKeyValue, fieldName);
byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");
return Ed25519Util.verify(preimage.getBytes(StandardCharsets.UTF_8), signature64, publicKey32);
}
private static List<ConnectionContext> resolveTargets(String targetMode, String toLogin, String targetSessionId) {
List<ConnectionContext> targets = new ArrayList<>();
if (TARGET_MODE_SINGLE.equals(targetMode)) {
ConnectionContext targetCtx = ActiveConnectionsRegistry.getInstance().getBySessionId(targetSessionId);
if (targetCtx != null && toLogin.equalsIgnoreCase(safe(targetCtx.getLogin()))) {
targets.add(targetCtx);
}
return targets;
}
Set<ConnectionContext> onlineTargets = ActiveConnectionsRegistry.getInstance().getByLogin(toLogin);
onlineTargets.stream()
.filter(item -> item != null && item.getWsSession() != null && item.getWsSession().isOpen())
.sorted(Comparator.comparing(ConnectionContext::getSessionId, Comparator.nullsLast(String::compareTo)))
.forEach(targets::add);
return targets;
}
private static String safe(String value) {
return value == null ? "" : value.trim();
}
}
@@ -0,0 +1,87 @@
package server.logic.ws_protocol.JSON.messages.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
public class Net_SendSignal_Request extends Net_Request {
private String toLogin;
private String targetMode;
private String targetSessionId;
private String signalType;
private String signalRequestId;
private String data;
private Long timeMs;
private String sessionSignatureB64;
private String clientSignatureB64;
public String getToLogin() {
return toLogin;
}
public void setToLogin(String toLogin) {
this.toLogin = toLogin;
}
public String getTargetMode() {
return targetMode;
}
public void setTargetMode(String targetMode) {
this.targetMode = targetMode;
}
public String getTargetSessionId() {
return targetSessionId;
}
public void setTargetSessionId(String targetSessionId) {
this.targetSessionId = targetSessionId;
}
public String getSignalType() {
return signalType;
}
public void setSignalType(String signalType) {
this.signalType = signalType;
}
public String getSignalRequestId() {
return signalRequestId;
}
public void setSignalRequestId(String signalRequestId) {
this.signalRequestId = signalRequestId;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public Long getTimeMs() {
return timeMs;
}
public void setTimeMs(Long timeMs) {
this.timeMs = timeMs;
}
public String getSessionSignatureB64() {
return sessionSignatureB64;
}
public void setSessionSignatureB64(String sessionSignatureB64) {
this.sessionSignatureB64 = sessionSignatureB64;
}
public String getClientSignatureB64() {
return clientSignatureB64;
}
public void setClientSignatureB64(String clientSignatureB64) {
this.clientSignatureB64 = clientSignatureB64;
}
}
@@ -0,0 +1,27 @@
package server.logic.ws_protocol.JSON.messages.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import java.util.ArrayList;
import java.util.List;
public class Net_SendSignal_Response extends Net_Response {
private int deliveredCount;
private List<String> deliveredSessionIds = new ArrayList<>();
public int getDeliveredCount() {
return deliveredCount;
}
public void setDeliveredCount(int deliveredCount) {
this.deliveredCount = deliveredCount;
}
public List<String> getDeliveredSessionIds() {
return deliveredSessionIds;
}
public void setDeliveredSessionIds(List<String> deliveredSessionIds) {
this.deliveredSessionIds = deliveredSessionIds;
}
}