SHA256
feat(call): серверная выдача ICE/TURN и подключение в WebRTC
This commit is contained in:
+4
@@ -79,11 +79,13 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_UpsertPushToken_Reque
|
||||
|
||||
// --- NEW: Ping ---
|
||||
import server.logic.ws_protocol.JSON.handlers.system.Net_GetServerInfo_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.system.Net_GetCallIceConfig_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.system.Net_ClientErrorLog_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.system.Net_ClientDebugLog_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.system.Net_Ping_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientErrorLog_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientDebugLog_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetCallIceConfig_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetServerInfo_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request;
|
||||
|
||||
@@ -145,6 +147,7 @@ public final class JsonHandlerRegistry {
|
||||
// --- system ---
|
||||
Map.entry("Ping", new Net_Ping_Handler()),
|
||||
Map.entry("GetServerInfo", new Net_GetServerInfo_Handler()),
|
||||
Map.entry("GetCallIceConfig", new Net_GetCallIceConfig_Handler()),
|
||||
Map.entry("ClientErrorLog", new Net_ClientErrorLog_Handler()),
|
||||
Map.entry("ClientDebugLog", new Net_ClientDebugLog_Handler())
|
||||
|
||||
@@ -198,6 +201,7 @@ public final class JsonHandlerRegistry {
|
||||
// --- system ---
|
||||
Map.entry("Ping", Net_Ping_Request.class),
|
||||
Map.entry("GetServerInfo", Net_GetServerInfo_Request.class),
|
||||
Map.entry("GetCallIceConfig", Net_GetCallIceConfig_Request.class),
|
||||
Map.entry("ClientErrorLog", Net_ClientErrorLog_Request.class),
|
||||
Map.entry("ClientDebugLog", Net_ClientDebugLog_Request.class)
|
||||
);
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.system;
|
||||
|
||||
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.handlers.system.entyties.Net_GetCallIceConfig_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetCallIceConfig_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import utils.config.AppConfig;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Выдаёт ICE-конфиг для звонка.
|
||||
* Поддерживает два режима TURN:
|
||||
* 1) Временные учётки через shared-secret (рекомендуется).
|
||||
* 2) Статический username/password (fallback).
|
||||
*/
|
||||
public class Net_GetCallIceConfig_Handler implements JsonMessageHandler {
|
||||
private static final AppConfig CONFIG = AppConfig.getInstance();
|
||||
private static final String HMAC_SHA1 = "HmacSHA1";
|
||||
private static final int DEFAULT_TTL_SEC = 600;
|
||||
private static final int MIN_TTL_SEC = 60;
|
||||
private static final int MAX_TTL_SEC = 3600;
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||
Net_GetCallIceConfig_Request req = (Net_GetCallIceConfig_Request) baseRequest;
|
||||
if (ctx == null || !ctx.isAuthenticatedUser()) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация");
|
||||
}
|
||||
|
||||
int ttlSec = clampTtlSec(readInt("call.ice.turn.ttlSec", DEFAULT_TTL_SEC));
|
||||
long nowMs = System.currentTimeMillis();
|
||||
long expiresAtMs = nowMs + ttlSec * 1000L;
|
||||
|
||||
List<String> stunUrls = parseUrls(
|
||||
readStr("call.ice.stun.urls", "stun:stun.l.google.com:19302")
|
||||
);
|
||||
List<String> turnUrls = parseUrls(readStr("call.ice.turn.urls", ""));
|
||||
|
||||
String turnUsername = "";
|
||||
String turnPassword = "";
|
||||
|
||||
String sharedSecret = readStr("call.ice.turn.sharedSecret", "");
|
||||
String staticUsername = readStr("call.ice.turn.username", "");
|
||||
String staticPassword = readStr("call.ice.turn.password", "");
|
||||
|
||||
if (!turnUrls.isEmpty()) {
|
||||
if (!sharedSecret.isBlank()) {
|
||||
long expiresEpochSec = nowMs / 1000L + ttlSec;
|
||||
expiresAtMs = expiresEpochSec * 1000L;
|
||||
String prefix = readStr("call.ice.turn.userPrefix", "shine");
|
||||
String safeLogin = sanitizeLogin(ctx.getLogin());
|
||||
turnUsername = expiresEpochSec + ":" + prefix + "_" + safeLogin;
|
||||
turnPassword = makeTurnRestPassword(sharedSecret, turnUsername);
|
||||
} else if (!staticUsername.isBlank() && !staticPassword.isBlank()) {
|
||||
turnUsername = staticUsername;
|
||||
turnPassword = staticPassword;
|
||||
}
|
||||
}
|
||||
|
||||
Net_GetCallIceConfig_Response resp = new Net_GetCallIceConfig_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
resp.setStunUrls(stunUrls);
|
||||
resp.setTurnUrls(turnUrls);
|
||||
resp.setTurnUsername(turnUsername);
|
||||
resp.setTurnPassword(turnPassword);
|
||||
resp.setTurnEnabled(!turnUrls.isEmpty() && !turnUsername.isBlank() && !turnPassword.isBlank());
|
||||
resp.setGeneratedAtMs(nowMs);
|
||||
resp.setExpiresAtMs(expiresAtMs);
|
||||
resp.setTtlSec(ttlSec);
|
||||
return resp;
|
||||
}
|
||||
|
||||
private static int readInt(String key, int fallback) {
|
||||
String value = CONFIG.getParam(key);
|
||||
if (value == null || value.isBlank()) return fallback;
|
||||
try {
|
||||
return Integer.parseInt(value.trim());
|
||||
} catch (Exception e) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private static String readStr(String key, String fallback) {
|
||||
String value = CONFIG.getParam(key);
|
||||
if (value == null) return fallback;
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? fallback : trimmed;
|
||||
}
|
||||
|
||||
private static int clampTtlSec(int ttlSec) {
|
||||
if (ttlSec < MIN_TTL_SEC) return MIN_TTL_SEC;
|
||||
return Math.min(ttlSec, MAX_TTL_SEC);
|
||||
}
|
||||
|
||||
private static List<String> parseUrls(String raw) {
|
||||
Set<String> out = new LinkedHashSet<>();
|
||||
if (raw == null || raw.isBlank()) return new ArrayList<>();
|
||||
|
||||
String[] chunks = raw.split("[,\\n\\r]+");
|
||||
for (String chunk : chunks) {
|
||||
String val = chunk == null ? "" : chunk.trim();
|
||||
if (val.isEmpty()) continue;
|
||||
out.add(val);
|
||||
}
|
||||
return new ArrayList<>(out);
|
||||
}
|
||||
|
||||
private static String sanitizeLogin(String login) {
|
||||
String value = login == null ? "" : login.trim().toLowerCase(Locale.ROOT);
|
||||
if (value.isEmpty()) return "user";
|
||||
StringBuilder sb = new StringBuilder(value.length());
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char ch = value.charAt(i);
|
||||
boolean ok = (ch >= 'a' && ch <= 'z')
|
||||
|| (ch >= '0' && ch <= '9')
|
||||
|| ch == '_' || ch == '-' || ch == '.';
|
||||
sb.append(ok ? ch : '_');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String makeTurnRestPassword(String sharedSecret, String username) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance(HMAC_SHA1);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(sharedSecret.getBytes(StandardCharsets.UTF_8), HMAC_SHA1);
|
||||
mac.init(keySpec);
|
||||
byte[] digest = mac.doFinal(username.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(digest);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Не удалось сгенерировать TURN пароль", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.system.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
/**
|
||||
* Пустой запрос:
|
||||
* {
|
||||
* "op": "GetCallIceConfig",
|
||||
* "requestId": "req-1",
|
||||
* "payload": {}
|
||||
* }
|
||||
*/
|
||||
public class Net_GetCallIceConfig_Request extends Net_Request {
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.system.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Net_GetCallIceConfig_Response extends Net_Response {
|
||||
private List<String> stunUrls = new ArrayList<>();
|
||||
private List<String> turnUrls = new ArrayList<>();
|
||||
private String turnUsername = "";
|
||||
private String turnPassword = "";
|
||||
private boolean turnEnabled;
|
||||
private long generatedAtMs;
|
||||
private long expiresAtMs;
|
||||
private int ttlSec;
|
||||
|
||||
public List<String> getStunUrls() { return stunUrls; }
|
||||
public void setStunUrls(List<String> stunUrls) { this.stunUrls = stunUrls; }
|
||||
|
||||
public List<String> getTurnUrls() { return turnUrls; }
|
||||
public void setTurnUrls(List<String> turnUrls) { this.turnUrls = turnUrls; }
|
||||
|
||||
public String getTurnUsername() { return turnUsername; }
|
||||
public void setTurnUsername(String turnUsername) { this.turnUsername = turnUsername; }
|
||||
|
||||
public String getTurnPassword() { return turnPassword; }
|
||||
public void setTurnPassword(String turnPassword) { this.turnPassword = turnPassword; }
|
||||
|
||||
public boolean isTurnEnabled() { return turnEnabled; }
|
||||
public void setTurnEnabled(boolean turnEnabled) { this.turnEnabled = turnEnabled; }
|
||||
|
||||
public long getGeneratedAtMs() { return generatedAtMs; }
|
||||
public void setGeneratedAtMs(long generatedAtMs) { this.generatedAtMs = generatedAtMs; }
|
||||
|
||||
public long getExpiresAtMs() { return expiresAtMs; }
|
||||
public void setExpiresAtMs(long expiresAtMs) { this.expiresAtMs = expiresAtMs; }
|
||||
|
||||
public int getTtlSec() { return ttlSec; }
|
||||
public void setTtlSec(int ttlSec) { this.ttlSec = ttlSec; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user