diff --git a/.env b/.env
index c21b809..4e0c135 100644
--- a/.env
+++ b/.env
@@ -14,6 +14,10 @@ SDU_SERVERS=http://10.211.55.2:7777
#Firebase Credentials
FIREBASE_CREDENTIALS_PATH=serviceAccount.json
+#Apple APNS
+APNS_KEY_PATH=voip.p12
+APNS_P12_PASSWORD=rosetta1
+IOS_BUNDLE_ID=com.rosetta.dev
#Каждые сколько дней будет очищаться буфер (максимальная дистанция синхронизации сообщений)
BUFFER_CLEANUP_DAYS=7
diff --git a/.gitignore b/.gitignore
index 47e61c3..ab2eaf7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,6 +34,9 @@ target
.settings
.project
.classpath
+serviceAccount.json
+build/*.p12
+*.p12
build/.env
build/.env*
diff --git a/pom.xml b/pom.xml
index 4605919..a0a3469 100644
--- a/pom.xml
+++ b/pom.xml
@@ -46,6 +46,12 @@
jakarta.persistence-api
3.1.0
+
+
+ com.eatthepath
+ pushy
+ 0.15.4
+
diff --git a/src/main/java/im/rosetta/executors/Executor26SignalPeer.java b/src/main/java/im/rosetta/executors/Executor26SignalPeer.java
index 9ea8147..aea72be 100644
--- a/src/main/java/im/rosetta/executors/Executor26SignalPeer.java
+++ b/src/main/java/im/rosetta/executors/Executor26SignalPeer.java
@@ -1,10 +1,15 @@
package im.rosetta.executors;
+import java.util.HashMap;
+import java.util.UUID;
+
import im.rosetta.Failures;
import im.rosetta.client.ClientManager;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.packet.Packet26SignalPeer;
import im.rosetta.packet.runtime.NetworkSignalType;
+import im.rosetta.service.dispatch.push.PushNotifyDispatcher;
+import im.rosetta.service.dispatch.runtime.PushType;
import im.rosetta.service.services.ForwardUnitService;
import io.g365sfu.Room;
import io.orprotocol.ProtocolException;
@@ -18,6 +23,7 @@ public class Executor26SignalPeer extends PacketExecutor {
private ClientManager clientManager;
private ForwardUnitService fus;
+ private PushNotifyDispatcher pushNotifyDispatcher = new PushNotifyDispatcher();
public Executor26SignalPeer(ClientManager clientManager, ForwardUnitService fus) {
this.clientManager = clientManager;
@@ -26,6 +32,8 @@ public class Executor26SignalPeer extends PacketExecutor {
@Override
public void onPacketReceived(Packet26SignalPeer packet, Client client) throws Exception, ProtocolException {
+ String src = packet.getSrc();
+ String dst = packet.getDst();
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if (eciAuthentificate == null || !eciAuthentificate.hasAuthorized()) {
/**
@@ -35,6 +43,14 @@ public class Executor26SignalPeer extends PacketExecutor {
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
+ if(!src.equals(eciAuthentificate.getPublicKey())) {
+ /**
+ * Если src в пакете не совпадает с авторизованным PK клиента, то это может означать, что клиент пытается
+ * отправить сигнал от другого пользователя, отключаем его от сервера.
+ */
+ client.disconnect(Failures.DATA_MISSMATCH);
+ return;
+ }
NetworkSignalType type = packet.getSignalType();
if(type == NetworkSignalType.CALL) {
/**
@@ -50,6 +66,14 @@ public class Executor26SignalPeer extends PacketExecutor {
this.clientManager.sendPacketToAuthorizedPK(packet.getSrc(), responsePacket);
return;
}
+ /**
+ * Получатель сигнала не занят, отправляем ему пуш уведомление о входящем звонке и сигнал CALL для инициализации звонка
+ */
+ pushNotifyDispatcher.sendPush(dst, new HashMap<>(){{
+ put("type", PushType.CALL);
+ put("dialog", src);
+ put("callId", UUID.randomUUID().toString());
+ }});
}
if(type == NetworkSignalType.CREATE_ROOM){
/**
@@ -66,14 +90,8 @@ public class Executor26SignalPeer extends PacketExecutor {
this.clientManager.sendPacketToAuthorizedPK(packet.getDst(), packet);
return;
}
- /**
- * TODO: Проверка на существование получателя
- */
+
this.clientManager.sendPacketToAuthorizedPK(packet.getDst(), packet);
- /**
- * TODO: Высокоприоритетный пуш для сигналов звонков, чтобы мобильные устройства могли показать
- * интерфейс входящего звонка, даже если приложение находится в фоне
- */
}
}
diff --git a/src/main/java/im/rosetta/service/dispatch/push/dispatchers/FCM.java b/src/main/java/im/rosetta/service/dispatch/push/dispatchers/FCM.java
index 036e27a..a181e88 100644
--- a/src/main/java/im/rosetta/service/dispatch/push/dispatchers/FCM.java
+++ b/src/main/java/im/rosetta/service/dispatch/push/dispatchers/FCM.java
@@ -82,8 +82,7 @@ public class FCM extends Pusher {
break;
case PushType.CALL:
/**
- * Звонок для андроид используем high priority, чтобы уведомление доставлялось даже если устройство в режиме Doze,
- * для iOS используем VoIP уведомление, которое доставляется даже если приложение убито
+ * Это только для Android, для iOS используется VoIP APNs с отдельным сертификатом
*/
androidConfig.setPriority(AndroidConfig.Priority.HIGH);
messageBuilder.setAndroidConfig(androidConfig.build());
diff --git a/src/main/java/im/rosetta/service/dispatch/push/dispatchers/VoIPApns.java b/src/main/java/im/rosetta/service/dispatch/push/dispatchers/VoIPApns.java
index 2a37a94..cabe6f2 100644
--- a/src/main/java/im/rosetta/service/dispatch/push/dispatchers/VoIPApns.java
+++ b/src/main/java/im/rosetta/service/dispatch/push/dispatchers/VoIPApns.java
@@ -1,15 +1,94 @@
package im.rosetta.service.dispatch.push.dispatchers;
+import java.io.File;
import java.util.HashMap;
+import java.util.concurrent.ExecutionException;
+
+import com.eatthepath.pushy.apns.ApnsClient;
+import com.eatthepath.pushy.apns.ApnsClientBuilder;
+import com.eatthepath.pushy.apns.PushNotificationResponse;
+import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification;
+import com.eatthepath.pushy.apns.util.TokenUtil;
import im.rosetta.service.dispatch.push.Pusher;
+import im.rosetta.service.dispatch.runtime.PushType;
public class VoIPApns extends Pusher {
+ private ApnsClient client;
+ private String topic;
+
+ public VoIPApns(){
+ this.initializeApns();
+ }
+
+ private void initializeApns() {
+ try {
+ String p12Path = System.getenv("APNS_KEY_PATH");
+ String p12Password = System.getenv("APNS_P12_PASSWORD");
+ String bundleId = System.getenv("IOS_BUNDLE_ID");
+
+ if (p12Path == null || bundleId == null) {
+ throw new IllegalStateException("APNS_P12_PATH and IOS_BUNDLE_ID must be set");
+ }
+
+ this.topic = bundleId + ".voip";
+
+ this.client = new ApnsClientBuilder()
+ .setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST)
+ .setClientCredentials(new File(p12Path), p12Password)
+ .build();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to init VoIP APNs client", e);
+ }
+ }
+
@Override
public void sendPush(String token, HashMap data) {
- // TODO Auto-generated method stub
- throw new UnsupportedOperationException("Unimplemented method 'sendPush'");
+ if(data.get("type") != PushType.CALL) {
+ /**
+ * Для VoIP APNs отправляем уведомления только о входящих звонках
+ */
+ return;
+ }
+ try {
+ String normalizedToken = TokenUtil.sanitizeTokenString(token);
+
+ String payload = """
+ {
+ "aps": { "content-available": 1 },
+ "type": "CALL",
+ "callId": "%s",
+ "from": "%s"
+ }
+ """.formatted(
+ escape(data.getOrDefault("callId", "")),
+ escape(data.getOrDefault("dialog", ""))
+ );
+
+ SimpleApnsPushNotification push = new SimpleApnsPushNotification(
+ normalizedToken,
+ topic,
+ payload,
+ null, // invalidation time
+ com.eatthepath.pushy.apns.DeliveryPriority.IMMEDIATE,
+ com.eatthepath.pushy.apns.PushType.VOIP // apns-push-type: voip
+ );
+
+ PushNotificationResponse response = client.sendNotification(push).get();
+
+ if (!response.isAccepted()) {
+ System.err.println("VoIP push rejected: " + response.getRejectionReason());
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } catch (ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private String escape(String v) {
+ return v == null ? "" : v.replace("\\", "\\\\").replace("\"", "\\\"");
}
}
diff --git a/src/main/java/io/orprotocol/Server.java b/src/main/java/io/orprotocol/Server.java
index f446c08..dfa705a 100644
--- a/src/main/java/io/orprotocol/Server.java
+++ b/src/main/java/io/orprotocol/Server.java
@@ -150,7 +150,6 @@ public class Server extends WebSocketServer {
}
} catch (Exception e) {
client.disconnect(ServerFailures.BAD_PACKET);
- e.printStackTrace();
}
}