diff --git a/app/src/main/java/com/futo/platformplayer/activities/SyncPairActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/SyncPairActivity.kt index c34e8362..b0ca616a 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/SyncPairActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/SyncPairActivity.kt @@ -110,7 +110,7 @@ class SyncPairActivity : AppCompatActivity() { lifecycleScope.launch(Dispatchers.IO) { try { - StateSync.instance.connect(deviceInfo) { complete, message -> + StateSync.instance.syncService?.connect(deviceInfo) { complete, message -> lifecycleScope.launch(Dispatchers.Main) { if (complete != null) { if (complete) { diff --git a/app/src/main/java/com/futo/platformplayer/activities/SyncShowPairingCodeActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/SyncShowPairingCodeActivity.kt index b9f3d437..848c320c 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/SyncShowPairingCodeActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/SyncShowPairingCodeActivity.kt @@ -67,11 +67,18 @@ class SyncShowPairingCodeActivity : AppCompatActivity() { } val ips = getIPs() - val selfDeviceInfo = SyncDeviceInfo(StateSync.instance.publicKey!!, ips.toTypedArray(), StateSync.PORT, StateSync.instance.pairingCode) - val json = Json.encodeToString(selfDeviceInfo) - val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP) - val url = "grayjay://sync/${base64}" - setCode(url) + val publicKey = StateSync.instance.syncService?.publicKey + val pairingCode = StateSync.instance.syncService?.pairingCode + if (publicKey == null || pairingCode == null) { + setCode("Public key or pairing code was not known, is sync enabled?") + } else { + val selfDeviceInfo = SyncDeviceInfo(publicKey, ips.toTypedArray(), StateSync.PORT, pairingCode) + val json = Json.encodeToString(selfDeviceInfo) + val base64 = Base64.encodeToString(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP) + val url = "grayjay://sync/${base64}" + setCode(url) + } + } fun setCode(code: String?) { diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index ff997d3d..647aaae6 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -45,8 +45,6 @@ import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.parsers.HLS import com.futo.platformplayer.states.StateApp -import com.futo.platformplayer.states.StateSync -import com.futo.platformplayer.states.StateSync.Companion import com.futo.platformplayer.stores.CastingDeviceInfoStorage import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.toUrlAddress diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index 1937e939..da969386 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -58,7 +58,6 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import kotlinx.serialization.Contextual -import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.Transient import java.io.File import java.io.FileOutputStream @@ -73,7 +72,6 @@ import java.util.concurrent.ThreadLocalRandom import kotlin.coroutines.resumeWithException import kotlin.time.times -@InternalSerializationApi @kotlinx.serialization.Serializable class VideoDownload { var state: State = State.QUEUED; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index bc2c683b..21f20403 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -47,7 +47,6 @@ import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UISlideOverlays import com.futo.platformplayer.activities.MainActivity -import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.api.media.IPluginSourced import com.futo.platformplayer.api.media.LiveChatManager import com.futo.platformplayer.api.media.PlatformID @@ -150,7 +149,6 @@ import com.futo.platformplayer.views.pills.PillRatingLikesDislikes import com.futo.platformplayer.views.pills.RoundButton import com.futo.platformplayer.views.pills.RoundButtonGroup import com.futo.platformplayer.views.platform.PlatformIndicator -import com.futo.platformplayer.views.segments.ChaptersList import com.futo.platformplayer.views.segments.CommentsList import com.futo.platformplayer.views.subscriptions.SubscribeButton import com.futo.platformplayer.views.video.FutoVideoPlayer diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt index 10aaa24d..0e5cda56 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt @@ -1,506 +1,159 @@ package com.futo.platformplayer.states import android.content.Context -import android.net.nsd.NsdManager -import android.net.nsd.NsdServiceInfo -import android.os.Build -import android.util.Log -import com.futo.platformplayer.LittleEndianDataInputStream -import com.futo.platformplayer.LittleEndianDataOutputStream import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.SyncShowPairingCodeActivity import com.futo.platformplayer.api.media.Serializer -import com.futo.platformplayer.casting.StateCasting -import com.futo.platformplayer.casting.StateCasting.Companion import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.encryption.GEncryptionProvider -import com.futo.platformplayer.generateReadablePassword -import com.futo.platformplayer.getConnectedSocket import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.HistoryVideo import com.futo.platformplayer.models.Subscription -import com.futo.platformplayer.noise.protocol.DHState -import com.futo.platformplayer.noise.protocol.Noise import com.futo.platformplayer.sToOffsetDateTimeUTC import com.futo.platformplayer.smartMerge import com.futo.platformplayer.stores.FragmentedStorage -import com.futo.platformplayer.stores.StringStringMapStorage import com.futo.platformplayer.stores.StringArrayStorage import com.futo.platformplayer.stores.StringStorage +import com.futo.platformplayer.stores.StringStringMapStorage import com.futo.platformplayer.stores.StringTMapStorage import com.futo.platformplayer.sync.SyncSessionData -import com.futo.platformplayer.sync.internal.ChannelSocket import com.futo.platformplayer.sync.internal.GJSyncOpcodes -import com.futo.platformplayer.sync.internal.IAuthorizable -import com.futo.platformplayer.sync.internal.IChannel -import com.futo.platformplayer.sync.internal.LinkType +import com.futo.platformplayer.sync.internal.ISyncDatabaseProvider import com.futo.platformplayer.sync.internal.Opcode -import com.futo.platformplayer.sync.internal.SyncDeviceInfo import com.futo.platformplayer.sync.internal.SyncKeyPair +import com.futo.platformplayer.sync.internal.SyncService +import com.futo.platformplayer.sync.internal.SyncServiceSettings import com.futo.platformplayer.sync.internal.SyncSession -import com.futo.platformplayer.sync.internal.SyncSocketSession import com.futo.platformplayer.sync.models.SendToDevicePackage import com.futo.platformplayer.sync.models.SyncPlaylistsPackage import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage import com.futo.platformplayer.sync.models.SyncWatchLaterPackage -import com.futo.polycentric.core.base64ToByteArray -import com.futo.polycentric.core.toBase64 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.io.ByteArrayInputStream -import java.net.Inet4Address -import java.net.InetAddress -import java.net.InetSocketAddress -import java.net.ServerSocket -import java.net.Socket import java.nio.ByteBuffer -import java.time.Instant import java.time.OffsetDateTime -import java.time.ZoneOffset -import java.util.Base64 -import java.util.Locale -import kotlin.math.min import kotlin.system.measureTimeMillis class StateSync { - private val _authorizedDevices = FragmentedStorage.get("authorized_devices") - private val _nameStorage = FragmentedStorage.get("sync_remembered_name_storage") - private val _syncKeyPair = FragmentedStorage.get("sync_key_pair") - private val _lastAddressStorage = FragmentedStorage.get("sync_last_address_storage") private val _syncSessionData = FragmentedStorage.get>("syncSessionData") - private var _serverSocket: ServerSocket? = null - private var _thread: Thread? = null - private var _connectThread: Thread? = null - @Volatile private var _started = false - private val _sessions: MutableMap = mutableMapOf() - private val _lastConnectTimesMdns: MutableMap = mutableMapOf() - private val _lastConnectTimesIp: MutableMap = mutableMapOf() - private var _serverStarted = false - //TODO: Should sync mdns and casting mdns be merged? - //TODO: Decrease interval that devices are updated - //TODO: Send less data - - private val _pairingCode: String? = generateReadablePassword(8) - val pairingCode: String? get() = _pairingCode - private var _relaySession: SyncSocketSession? = null - private var _threadRelay: Thread? = null - private val _remotePendingStatusUpdate = mutableMapOf Unit>() - private var _nsdManager: NsdManager? = null - private var _discoveryListener: NsdManager.DiscoveryListener = object : NsdManager.DiscoveryListener { - override fun onDiscoveryStarted(regType: String) { - Log.d(TAG, "Service discovery started for $regType") - } - - override fun onDiscoveryStopped(serviceType: String) { - Log.i(TAG, "Discovery stopped: $serviceType") - } - - override fun onServiceLost(service: NsdServiceInfo) { - Log.e(TAG, "service lost: $service") - // TODO: Handle service lost, e.g., remove device - } - - override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) { - Log.e(TAG, "Discovery failed for $serviceType: Error code:$errorCode") - try { - _nsdManager?.stopServiceDiscovery(this) - } catch (e: Throwable) { - Logger.w(TAG, "Failed to stop service discovery", e) - } - } - - override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { - Log.e(TAG, "Stop discovery failed for $serviceType: Error code:$errorCode") - try { - _nsdManager?.stopServiceDiscovery(this) - } catch (e: Throwable) { - Logger.w(TAG, "Failed to stop service discovery", e) - } - } - - fun addOrUpdate(name: String, adrs: Array, port: Int, attributes: Map) { - if (!Settings.instance.synchronization.connectDiscovered) { - return - } - - val urlSafePkey = attributes.get("pk")?.decodeToString() ?: return - val pkey = Base64.getEncoder().encodeToString(Base64.getDecoder().decode(urlSafePkey.replace('-', '+').replace('_', '/'))) - val syncDeviceInfo = SyncDeviceInfo(pkey, adrs.map { it.hostAddress }.toTypedArray(), port, null) - val authorized = isAuthorized(pkey) - - if (authorized && !isConnected(pkey)) { - val now = System.currentTimeMillis() - val lastConnectTime = synchronized(_lastConnectTimesMdns) { - _lastConnectTimesMdns[pkey] ?: 0 - } - - //Connect once every 30 seconds, max - if (now - lastConnectTime > 30000) { - synchronized(_lastConnectTimesMdns) { - _lastConnectTimesMdns[pkey] = now - } - - Logger.i(TAG, "Found device authorized device '${name}' with pkey=$pkey, attempting to connect") - - try { - connect(syncDeviceInfo) - } catch (e: Throwable) { - Logger.i(TAG, "Failed to connect to $pkey", e) - } - } - } - } - - override fun onServiceFound(service: NsdServiceInfo) { - Log.v(TAG, "Service discovery success for ${service.serviceType}: $service") - addOrUpdate(service.serviceName, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - service.hostAddresses.toTypedArray() - } else { - if(service.host != null) - arrayOf(service.host); - else - arrayOf(); - }, service.port, service.attributes) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - _nsdManager?.registerServiceInfoCallback(service, { it.run() }, object : NsdManager.ServiceInfoCallback { - override fun onServiceUpdated(serviceInfo: NsdServiceInfo) { - Log.v(TAG, "onServiceUpdated: $serviceInfo") - addOrUpdate(serviceInfo.serviceName, serviceInfo.hostAddresses.toTypedArray(), serviceInfo.port, serviceInfo.attributes) - } - - override fun onServiceLost() { - Log.v(TAG, "onServiceLost: $service") - // TODO: Handle service lost - } - - override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) { - Log.v(TAG, "onServiceInfoCallbackRegistrationFailed: $errorCode") - } - - override fun onServiceInfoCallbackUnregistered() { - Log.v(TAG, "onServiceInfoCallbackUnregistered") - } - }) - } else { - _nsdManager?.resolveService(service, object : NsdManager.ResolveListener { - override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { - Log.v(TAG, "Resolve failed: $errorCode") - } - - override fun onServiceResolved(serviceInfo: NsdServiceInfo) { - Log.v(TAG, "Resolve Succeeded: $serviceInfo") - addOrUpdate(serviceInfo.serviceName, arrayOf(serviceInfo.host), serviceInfo.port, serviceInfo.attributes) - } - }) - } - } - } - - private val _registrationListener = object : NsdManager.RegistrationListener { - override fun onServiceRegistered(serviceInfo: NsdServiceInfo) { - Log.v(TAG, "onServiceRegistered: ${serviceInfo.serviceName}") - } - - override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { - Log.v(TAG, "onRegistrationFailed: ${serviceInfo.serviceName} (error code: $errorCode)") - } - - override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) { - Log.v(TAG, "onServiceUnregistered: ${serviceInfo.serviceName}") - } - - override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { - Log.v(TAG, "onUnregistrationFailed: ${serviceInfo.serviceName} (error code: $errorCode)") - } - } - - var keyPair: DHState? = null - var publicKey: String? = null + var syncService: SyncService? = null + private set val deviceRemoved: Event1 = Event1() val deviceUpdatedOrAdded: Event2 = Event2() - //TODO: Should authorize acknowledge be implemented? - - fun hasAuthorizedDevice(): Boolean { - synchronized(_sessions) { - return _sessions.any{ it.value.connected && it.value.isAuthorized }; - } - } - fun start(context: Context, onServerBindFail: () -> Unit) { - if (_started) { + if (syncService != null) { Logger.i(TAG, "Already started.") return } - _started = true - _nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager - if (Settings.instance.synchronization.connectDiscovered) { - _nsdManager?.apply { - discoverServices("_gsync._tcp", NsdManager.PROTOCOL_DNS_SD, _discoveryListener) + syncService = SyncService( + SERVICE_NAME, + RELAY_SERVER, + RELAY_PUBLIC_KEY, + APP_ID, + StoreBasedSyncDatabaseProvider(), + SyncServiceSettings( + mdnsBroadcast = Settings.instance.synchronization.broadcast, + mdnsConnectDiscovered = Settings.instance.synchronization.connectDiscovered, + bindListener = Settings.instance.synchronization.localConnections, + relayHandshakeAllowed = Settings.instance.synchronization.connectThroughRelay, + relayPairAllowed = Settings.instance.synchronization.pairThroughRelay, + relayEnabled = Settings.instance.synchronization.discoverThroughRelay, + relayConnectDirect = Settings.instance.synchronization.connectLocalDirectThroughRelay, + relayConnectRelayed = Settings.instance.synchronization.connectThroughRelay + ) + ).apply { + syncService?.onAuthorized = { sess, isNewlyAuthorized, isNewSession -> + if (isNewSession) { + deviceUpdatedOrAdded.emit(sess.remotePublicKey, sess) + StateApp.instance.scope.launch { checkForSync(sess) } + } } - } - try { - val syncKeyPair = Json.decodeFromString(GEncryptionProvider.instance.decrypt(_syncKeyPair.value)) - val p = Noise.createDH(dh) - p.setPublicKey(syncKeyPair.publicKey.base64ToByteArray(), 0) - p.setPrivateKey(syncKeyPair.privateKey.base64ToByteArray(), 0) - keyPair = p - } catch (e: Throwable) { - //Sync key pair non-existing, invalid or lost - val p = Noise.createDH(dh) - p.generateKeyPair() - - val publicKey = ByteArray(p.publicKeyLength) - p.getPublicKey(publicKey, 0) - val privateKey = ByteArray(p.privateKeyLength) - p.getPrivateKey(privateKey, 0) - - val syncKeyPair = SyncKeyPair(1, publicKey.toBase64(), privateKey.toBase64()) - _syncKeyPair.setAndSave(GEncryptionProvider.instance.encrypt(Json.encodeToString(syncKeyPair))) - - Logger.e(TAG, "Failed to load existing key pair", e) - keyPair = p - } - - publicKey = keyPair?.let { - val pkey = ByteArray(it.publicKeyLength) - it.getPublicKey(pkey, 0) - return@let pkey.toBase64() - } - - if (Settings.instance.synchronization.broadcast) { - val pk = publicKey - val nsdManager = _nsdManager - - if (pk != null && nsdManager != null) { - val serviceInfo = NsdServiceInfo().apply { - serviceName = getDeviceName() - serviceType = "_gsync._tcp" - port = PORT - setAttribute("pk", pk.replace('+', '-').replace('/', '_').replace("=", "")) + syncService?.onUnauthorized = { sess -> + StateApp.instance.scope.launch(Dispatchers.Main) { + UIDialogs.showConfirmationDialog( + context, + "Device Unauthorized: ${sess.displayName}", + action = { + Logger.i(TAG, "${sess.remotePublicKey} unauthorized received") + syncService?.removeAuthorizedDevice(sess.remotePublicKey) + deviceRemoved.emit(sess.remotePublicKey) + }, + cancelAction = {} + ) } - - nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, _registrationListener) } - } - Logger.i(TAG, "Sync key pair initialized (public key = ${publicKey})") + syncService?.onConnectedChanged = { sess, _ -> deviceUpdatedOrAdded.emit(sess.remotePublicKey, sess) } + syncService?.onClose = { sess -> deviceRemoved.emit(sess.remotePublicKey) } + syncService?.onData = { it, opcode, subOpcode, data -> + val dataCopy = ByteArray(data.remaining()) + data.get(dataCopy) - if (Settings.instance.synchronization.localConnections) { - _serverStarted = true - _thread = Thread { - try { - val serverSocket = ServerSocket(PORT) - _serverSocket = serverSocket - - Log.i(TAG, "Running on port ${PORT} (TCP)") - - while (_started) { - val socket = serverSocket.accept() - val session = createSocketSession(socket, true) - session.startAsResponder() - } - } catch (e: Throwable) { - _serverStarted = false - Logger.e(TAG, "Failed to bind server socket to port ${PORT}", e) - StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { - onServerBindFail.invoke() - } - } finally { - _serverStarted = false - } - }.apply { start() } - } - - if (Settings.instance.synchronization.connectLast) { - _connectThread = Thread { - Log.i(TAG, "Running auto reconnector") - - while (_started) { - val authorizedDevices = synchronized(_authorizedDevices) { - return@synchronized _authorizedDevices.values - } - - val lastKnownMap = synchronized(_lastAddressStorage) { - return@synchronized _lastAddressStorage.map.toMap() - } - - val addressesToConnect = authorizedDevices.mapNotNull { - val connected = isConnected(it) - if (connected) { - return@mapNotNull null - } - - val lastKnownAddress = lastKnownMap[it] ?: return@mapNotNull null - return@mapNotNull Pair(it, lastKnownAddress) - } - - for (connectPair in addressesToConnect) { - try { - val now = System.currentTimeMillis() - val lastConnectTime = synchronized(_lastConnectTimesIp) { - _lastConnectTimesIp[connectPair.first] ?: 0 - } - - //Connect once every 30 seconds, max - if (now - lastConnectTime > 30000) { - synchronized(_lastConnectTimesIp) { - _lastConnectTimesIp[connectPair.first] = now - } - - Logger.i(TAG, "Attempting to connect to authorized device by last known IP '${connectPair.first}' with pkey=${connectPair.first}") - connect(arrayOf(connectPair.second), PORT, connectPair.first, null) - } - } catch (e: Throwable) { - Logger.i(TAG, "Failed to connect to " + connectPair.first, e) - } - } - Thread.sleep(5000) - } - }.apply { start() } - } - - if (Settings.instance.synchronization.discoverThroughRelay) { - _threadRelay = Thread { - var backoffs: Array = arrayOf(1000, 5000, 10000, 20000) - var backoffIndex = 0; - - while (_started) { + StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { try { - Log.i(TAG, "Starting relay session...") + handleData(it, opcode, subOpcode, ByteBuffer.wrap(dataCopy)) + } catch (e: Throwable) { + Logger.e(TAG, "Exception occurred while handling data, closing session", e) + it.close() + } + } + } + syncService?.authorizePrompt = { remotePublicKey, callback -> + val scope = StateApp.instance.scopeOrNull + val activity = SyncShowPairingCodeActivity.activity - var socketClosed = false; - val socket = Socket(RELAY_SERVER, 9000) - _relaySession = SyncSocketSession( - (socket.remoteSocketAddress as InetSocketAddress).address.hostAddress!!, - keyPair!!, - socket, - isHandshakeAllowed = { linkType, syncSocketSession, publicKey, pairingCode, appId -> isHandshakeAllowed(linkType, syncSocketSession, publicKey, pairingCode, appId) }, - onNewChannel = { _, c -> - val remotePublicKey = c.remotePublicKey - if (remotePublicKey == null) { - Log.e(TAG, "Remote public key should never be null in onNewChannel.") - return@SyncSocketSession - } - - Log.i(TAG, "New channel established from relay (pk: '$remotePublicKey').") - - var session: SyncSession? - synchronized(_sessions) { - session = _sessions[remotePublicKey] - if (session == null) { - val remoteDeviceName = synchronized(_nameStorage) { - _nameStorage.get(remotePublicKey) - } - session = createNewSyncSession(remotePublicKey, remoteDeviceName) - _sessions[remotePublicKey] = session!! - } - session!!.addChannel(c) - } - - c.setDataHandler { _, channel, opcode, subOpcode, data -> - session?.handlePacket(opcode, subOpcode, data) - } - c.setCloseHandler { channel -> - session?.removeChannel(channel) - } - }, - onChannelEstablished = { _, channel, isResponder -> - handleAuthorization(channel, isResponder) - }, - onClose = { socketClosed = true }, - onHandshakeComplete = { relaySession -> - backoffIndex = 0 - - Thread { + if (scope != null && activity != null) { + scope.launch(Dispatchers.Main) { + UIDialogs.showConfirmationDialog(activity, "Allow connection from $remotePublicKey?", + action = { + scope.launch(Dispatchers.IO) { try { - while (_started && !socketClosed) { - val unconnectedAuthorizedDevices = synchronized(_authorizedDevices) { - _authorizedDevices.values.filter { !isConnected(it) }.toTypedArray() - } + callback(true) + Logger.i(TAG, "Connection authorized for $remotePublicKey by confirmation") - relaySession.publishConnectionInformation(unconnectedAuthorizedDevices, PORT, Settings.instance.synchronization.discoverThroughRelay, false, false, Settings.instance.synchronization.discoverThroughRelay && Settings.instance.synchronization.connectThroughRelay) - - Logger.v(TAG, "Requesting ${unconnectedAuthorizedDevices.size} devices connection information") - val connectionInfos = runBlocking { relaySession.requestBulkConnectionInfo(unconnectedAuthorizedDevices) } - Logger.v(TAG, "Received ${connectionInfos.size} devices connection information") - - for ((targetKey, connectionInfo) in connectionInfos) { - val potentialLocalAddresses = connectionInfo.ipv4Addresses - .filter { it != connectionInfo.remoteIp } - if (connectionInfo.allowLocalDirect && Settings.instance.synchronization.connectLocalDirectThroughRelay) { - Thread { - try { - Log.v(TAG, "Attempting to connect directly, locally to '$targetKey'.") - connect(potentialLocalAddresses.map { it }.toTypedArray(), PORT, targetKey, null) - } catch (e: Throwable) { - Log.e(TAG, "Failed to start direct connection using connection info with $targetKey.", e) - } - }.start() - } - - if (connectionInfo.allowRemoteDirect) { - // TODO: Implement direct remote connection if needed - } - - if (connectionInfo.allowRemoteHolePunched) { - // TODO: Implement hole punching if needed - } - - if (connectionInfo.allowRemoteRelayed && Settings.instance.synchronization.connectThroughRelay) { - try { - Logger.v(TAG, "Attempting relayed connection with '$targetKey'.") - runBlocking { relaySession.startRelayedChannel(targetKey, APP_ID, null) } - } catch (e: Throwable) { - Logger.e(TAG, "Failed to start relayed channel with $targetKey.", e) - } - } - } - - Thread.sleep(15000) - } + activity.finish() } catch (e: Throwable) { - Logger.e(TAG, "Unhandled exception in relay session.", e) - relaySession.stop() + Logger.e(TAG, "Failed to send authorize", e) } - }.start() + } + }, + cancelAction = { + scope.launch(Dispatchers.IO) { + try { + callback(false) + Logger.i(TAG, "$remotePublicKey unauthorized received") + } catch (e: Throwable) { + Logger.w(TAG, "Failed to send unauthorize", e) + } + } } ) - - _relaySession!!.authorizable = object : IAuthorizable { - override val isAuthorized: Boolean get() = true - } - - _relaySession!!.runAsInitiator(RELAY_PUBLIC_KEY, APP_ID, null) - - Log.i(TAG, "Started relay session.") - } catch (e: Throwable) { - Log.e(TAG, "Relay session failed.", e) - } finally { - _relaySession?.stop() - _relaySession = null - Thread.sleep(backoffs[min(backoffs.size - 1, backoffIndex++)]) } + } else { + callback(false) + Logger.i(TAG, "Connection unauthorized for $remotePublicKey because not authorized and not on pairing activity to ask") } - }.apply { start() } + } } + + syncService?.start(context, onServerBindFail) } fun showFailedToBindDialogIfNecessary(context: Context) { - if (!_serverStarted && Settings.instance.synchronization.localConnections) { + if (syncService?.serverSocketFailedToStart == true && Settings.instance.synchronization.localConnections) { try { UIDialogs.showDialogOk(context, R.drawable.ic_warning, "Local discovery unavailable, port was in use") } catch (e: Throwable) { @@ -510,10 +163,10 @@ class StateSync { } fun confirmStarted(context: Context, onStarted: () -> Unit, onNotStarted: () -> Unit, onServerBindFail: () -> Unit) { - if (!_started) { + if (syncService == null) { UIDialogs.showConfirmationDialog(context, "Sync has not been enabled yet, would you like to enable sync?", { Settings.instance.synchronization.enabled = true - StateSync.instance.start(context, onServerBindFail) + start(context, onServerBindFail) Settings.instance.save() onStarted.invoke() }, { @@ -524,43 +177,20 @@ class StateSync { } } - private fun getDeviceName(): String { - val manufacturer = Build.MANUFACTURER.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } - val model = Build.MODEL - - return if (model.startsWith(manufacturer, ignoreCase = true)) { - model.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } - } else { - "$manufacturer $model".replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } - } - } - - fun isConnected(publicKey: String): Boolean { - return synchronized(_sessions) { - _sessions[publicKey]?.connected ?: false - } + fun hasAuthorizedDevice(): Boolean { + return (syncService?.getAuthorizedDeviceCount() ?: 0) > 0 } fun isAuthorized(publicKey: String): Boolean { - return synchronized(_authorizedDevices) { - _authorizedDevices.values.contains(publicKey) - } + return syncService?.isAuthorized(publicKey) ?: false } fun getSession(publicKey: String): SyncSession? { - return synchronized(_sessions) { - _sessions[publicKey] - } - } - fun getSessions(): List { - synchronized(_sessions) { - return _sessions.values.toList() - } + return syncService?.getSession(publicKey) } + fun getAuthorizedSessions(): List { - synchronized(_sessions) { - return _sessions.values.filter { it.isAuthorized }.toList() - } + return syncService?.getSessions()?.filter { it.isAuthorized }?.toList() ?: listOf() } fun getSyncSessionData(key: String): SyncSessionData { @@ -573,14 +203,6 @@ class StateSync { _syncSessionData.setAndSave(data.publicKey, data); } - private fun unauthorize(remotePublicKey: String) { - Logger.i(TAG, "${remotePublicKey} unauthorized received") - _authorizedDevices.remove(remotePublicKey) - _authorizedDevices.save() - deviceRemoved.emit(remotePublicKey) - } - - private fun handleSyncSubscriptionPackage(origin: SyncSession, pack: SyncSubscriptionsPackage) { val added = mutableListOf() for(sub in pack.subscriptions) { @@ -832,218 +454,6 @@ class StateSync { } } - private fun createNewSyncSession(rpk: String, remoteDeviceName: String?): SyncSession { - val remotePublicKey = rpk.base64ToByteArray().toBase64() - return SyncSession( - remotePublicKey, - onAuthorized = { it, isNewlyAuthorized, isNewSession -> - synchronized(_remotePendingStatusUpdate) { - _remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(true, "Authorized") - } - - if (!isNewSession) { - return@SyncSession - } - - it.remoteDeviceName?.let { remoteDeviceName -> - synchronized(_nameStorage) { - _nameStorage.setAndSave(remotePublicKey, remoteDeviceName) - } - } - - Logger.i(TAG, "$remotePublicKey authorized (name: ${it.displayName})") - _authorizedDevices.addDistinct(remotePublicKey) - _authorizedDevices.save() - deviceUpdatedOrAdded.emit(it.remotePublicKey, it) - - checkForSync(it); - }, - onUnauthorized = { - synchronized(_remotePendingStatusUpdate) { - _remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(false, "Unauthorized") - } - - unauthorize(remotePublicKey) - Logger.i(TAG, "$remotePublicKey unauthorized (name: ${it.displayName})") - - synchronized(_sessions) { - it.close() - _sessions.remove(remotePublicKey) - } - }, - onConnectedChanged = { it, connected -> - Logger.i(TAG, "$remotePublicKey connected: $connected") - deviceUpdatedOrAdded.emit(it.remotePublicKey, it) - }, - onClose = { - Logger.i(TAG, "$remotePublicKey closed") - - synchronized(_sessions) - { - _sessions.remove(it.remotePublicKey) - } - - deviceRemoved.emit(it.remotePublicKey) - - synchronized(_remotePendingStatusUpdate) { - _remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(false, "Connection closed") - } - }, - dataHandler = { it, opcode, subOpcode, data -> - val dataCopy = ByteArray(data.remaining()) - data.get(dataCopy) - - StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { - try { - handleData(it, opcode, subOpcode, ByteBuffer.wrap(dataCopy)) - } catch (e: Throwable) { - Logger.e(TAG, "Exception occurred while handling data, closing session", e) - it.close() - } - } - }, - remoteDeviceName - ) - } - - private fun isHandshakeAllowed(linkType: LinkType, syncSocketSession: SyncSocketSession, publicKey: String, pairingCode: String?, appId: UInt): Boolean { - Log.v(TAG, "Check if handshake allowed from '$publicKey' (app id: $appId).") - if (publicKey == RELAY_PUBLIC_KEY) - return true - - synchronized(_authorizedDevices) { - if (_authorizedDevices.values.contains(publicKey)) { - if (linkType == LinkType.Relayed && !Settings.instance.synchronization.connectThroughRelay) - return false - return true - } - } - - Log.v(TAG, "Check if handshake allowed with pairing code '$pairingCode' with active pairing code '$_pairingCode' (app id: $appId).") - if (_pairingCode == null || pairingCode.isNullOrEmpty()) - return false - - if (linkType == LinkType.Relayed && !Settings.instance.synchronization.pairThroughRelay) - return false - - return _pairingCode == pairingCode - } - - private fun createSocketSession(socket: Socket, isResponder: Boolean): SyncSocketSession { - var session: SyncSession? = null - var channelSocket: ChannelSocket? = null - return SyncSocketSession( - (socket.remoteSocketAddress as InetSocketAddress).address.hostAddress!!, - keyPair!!, - socket, - onClose = { s -> - if (channelSocket != null) - session?.removeChannel(channelSocket!!) - }, - isHandshakeAllowed = { linkType, syncSocketSession, publicKey, pairingCode, appId -> isHandshakeAllowed(linkType, syncSocketSession, publicKey, pairingCode, appId) }, - onHandshakeComplete = { s -> - val remotePublicKey = s.remotePublicKey - if (remotePublicKey == null) { - s.stop() - return@SyncSocketSession - } - - Logger.i(TAG, "Handshake complete with (LocalPublicKey = ${s.localPublicKey}, RemotePublicKey = ${s.remotePublicKey})") - - channelSocket = ChannelSocket(s) - - synchronized(_sessions) { - session = _sessions[s.remotePublicKey] - if (session == null) { - val remoteDeviceName = synchronized(_nameStorage) { - _nameStorage.get(remotePublicKey) - } - - synchronized(_lastAddressStorage) { - _lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress) - } - - session = createNewSyncSession(remotePublicKey, remoteDeviceName) - _sessions[remotePublicKey] = session!! - } - session!!.addChannel(channelSocket!!) - } - - handleAuthorization(channelSocket!!, isResponder) - }, - onData = { s, opcode, subOpcode, data -> - session?.handlePacket(opcode, subOpcode, data) - } - ) - } - - private fun handleAuthorization(channel: IChannel, isResponder: Boolean) { - val syncSession = channel.syncSession!! - val remotePublicKey = channel.remotePublicKey!! - - if (isResponder) { - val isAuthorized = synchronized(_authorizedDevices) { - _authorizedDevices.values.contains(remotePublicKey) - } - - if (!isAuthorized) { - val scope = StateApp.instance.scopeOrNull - val activity = SyncShowPairingCodeActivity.activity - - if (scope != null && activity != null) { - scope.launch(Dispatchers.Main) { - UIDialogs.showConfirmationDialog(activity, "Allow connection from ${remotePublicKey}?", - action = { - scope.launch(Dispatchers.IO) { - try { - syncSession.authorize() - Logger.i(TAG, "Connection authorized for $remotePublicKey by confirmation") - - activity.finish() - } catch (e: Throwable) { - Logger.e(TAG, "Failed to send authorize", e) - } - } - }, - cancelAction = { - scope.launch(Dispatchers.IO) { - try { - unauthorize(remotePublicKey) - } catch (e: Throwable) { - Logger.w(TAG, "Failed to send unauthorize", e) - } - - syncSession.close() - synchronized(_sessions) { - _sessions.remove(remotePublicKey) - } - } - } - ) - } - } else { - val publicKey = syncSession.remotePublicKey - syncSession.unauthorize() - syncSession.close() - - synchronized(_sessions) { - _sessions.remove(publicKey) - } - - Logger.i(TAG, "Connection unauthorized for $remotePublicKey because not authorized and not on pairing activity to ask") - } - } else { - //Responder does not need to check because already approved - syncSession.authorize() - Logger.i(TAG, "Connection authorized for $remotePublicKey because already authorized") - } - } else { - //Initiator does not need to check because the manual action of scanning the QR counts as approval - syncSession.authorize() - Logger.i(TAG, "Connection authorized for $remotePublicKey because initiator") - } - } - inline fun broadcastJsonData(subOpcode: UByte, data: T) { broadcast(Opcode.DATA.value, subOpcode, Json.encodeToString(data)); } @@ -1074,84 +484,17 @@ class StateSync { } fun stop() { - _started = false - - try { - _nsdManager?.stopServiceDiscovery(_discoveryListener) - } catch (e: Throwable) { - Logger.e(TAG, "Failed to stop discovery listener", e) - } - - try { - _nsdManager?.unregisterService(_registrationListener) - } catch (e: Throwable) { - Logger.e(TAG, "Failed to unregister service", e) - } - - _relaySession?.stop() - _serverSocket?.close() - _serverSocket = null - - synchronized(_sessions) { - _sessions.values.forEach { it.close() } - _sessions.clear() - } - - _thread = null - _connectThread = null - _threadRelay = null - _relaySession = null + syncService?.stop() + syncService = null } - fun connect(deviceInfo: SyncDeviceInfo, onStatusUpdate: ((complete: Boolean?, message: String) -> Unit)? = null) { - try { - connect(deviceInfo.addresses, deviceInfo.port, deviceInfo.publicKey, deviceInfo.pairingCode, onStatusUpdate) - } catch (e: Throwable) { - Logger.e(TAG, "Failed to connect directly", e) - val relaySession = _relaySession - if (relaySession != null && Settings.instance.synchronization.pairThroughRelay) { - onStatusUpdate?.invoke(null, "Connecting via relay...") - - runBlocking { - if (onStatusUpdate != null) { - synchronized(_remotePendingStatusUpdate) { - _remotePendingStatusUpdate[deviceInfo.publicKey.base64ToByteArray().toBase64()] = onStatusUpdate - } - } - relaySession.startRelayedChannel(deviceInfo.publicKey.base64ToByteArray().toBase64(), APP_ID, deviceInfo.pairingCode) - } - } else { - throw e - } - } - } - - fun connect(addresses: Array, port: Int, publicKey: String, pairingCode: String?, onStatusUpdate: ((complete: Boolean?, message: String) -> Unit)? = null): SyncSocketSession { - onStatusUpdate?.invoke(null, "Connecting directly...") - val socket = getConnectedSocket(addresses.map { InetAddress.getByName(it) }, port) ?: throw Exception("Failed to connect") - onStatusUpdate?.invoke(null, "Handshaking...") - - val session = createSocketSession(socket, false) - if (onStatusUpdate != null) { - synchronized(_remotePendingStatusUpdate) { - _remotePendingStatusUpdate[publicKey.base64ToByteArray().toBase64()] = onStatusUpdate - } - } - - session.startAsInitiator(publicKey, APP_ID, pairingCode) - return session - } fun getAll(): List { - synchronized(_authorizedDevices) { - return _authorizedDevices.values.toList() - } + return syncService?.getAllAuthorizedDevices()?.toList() ?: listOf() } fun getCachedName(publicKey: String): String? { - return synchronized(_nameStorage) { - _nameStorage.get(publicKey) - } + return syncService?.getCachedName(publicKey) } suspend fun delete(publicKey: String) { @@ -1168,14 +511,8 @@ class StateSync { session.close() } - synchronized(_sessions) { - _sessions.remove(publicKey) - } - - synchronized(_authorizedDevices) { - _authorizedDevices.remove(publicKey) - } - _authorizedDevices.save() + syncService?.removeSession(publicKey) + syncService?.removeAuthorizedDevice(publicKey) withContext(Dispatchers.Main) { deviceRemoved.emit(publicKey) @@ -1184,17 +521,45 @@ class StateSync { Logger.w(TAG, "Failed to delete", e) } } + } + class StoreBasedSyncDatabaseProvider : ISyncDatabaseProvider { + private val _authorizedDevices = FragmentedStorage.get("authorized_devices") + private val _nameStorage = FragmentedStorage.get("sync_remembered_name_storage") + private val _syncKeyPair = FragmentedStorage.get("sync_key_pair") + private val _lastAddressStorage = FragmentedStorage.get("sync_last_address_storage") + + override fun isAuthorized(publicKey: String): Boolean = synchronized(_authorizedDevices) { _authorizedDevices.values.contains(publicKey) } + override fun addAuthorizedDevice(publicKey: String) = synchronized(_authorizedDevices) { + _authorizedDevices.addDistinct(publicKey) + _authorizedDevices.save() + } + override fun removeAuthorizedDevice(publicKey: String) = synchronized(_authorizedDevices) { + _authorizedDevices.remove(publicKey) + _authorizedDevices.save() + } + override fun getAllAuthorizedDevices(): Array = synchronized(_authorizedDevices) { _authorizedDevices.values.toTypedArray() } + override fun getAuthorizedDeviceCount(): Int = synchronized(_authorizedDevices) { _authorizedDevices.values.size } + override fun getSyncKeyPair(): SyncKeyPair? = try { + Json.decodeFromString(GEncryptionProvider.instance.decrypt(_syncKeyPair.value)) + } catch (e: Throwable) { null } + override fun setSyncKeyPair(value: SyncKeyPair) { _syncKeyPair.setAndSave(GEncryptionProvider.instance.encrypt(Json.encodeToString(value))) } + override fun getLastAddress(publicKey: String): String? = synchronized(_lastAddressStorage) { _lastAddressStorage.map[publicKey] } + override fun setLastAddress(publicKey: String, address: String) = synchronized(_lastAddressStorage) { + _lastAddressStorage.map[publicKey] = address + _lastAddressStorage.save() + } + override fun getDeviceName(publicKey: String): String? = synchronized(_nameStorage) { _nameStorage.map[publicKey] } + override fun setDeviceName(publicKey: String, name: String) = synchronized(_nameStorage) { + _nameStorage.map[publicKey] = name + _nameStorage.save() + } } companion object { - val dh = "25519" - val pattern = "IK" - val cipher = "ChaChaPoly" - val hash = "BLAKE2b" - var protocolName = "Noise_${pattern}_${dh}_${cipher}_${hash}" val version = 1 val RELAY_SERVER = "relay.grayjay.app" + val SERVICE_NAME = "_gsync._tcp" val RELAY_PUBLIC_KEY = "xGbHRzDOvE6plRbQaFgSen82eijF+gxS0yeUaeEErkw=" val APP_ID = 0x534A5247u //GRayJaySync (GRJS) diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/Channel.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/Channel.kt index 89e4e3a7..e17b6309 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/Channel.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/Channel.kt @@ -73,12 +73,12 @@ class ChannelRelayed( private val sendLock = Object() private val decryptLock = Object() private var handshakeState: HandshakeState? = if (initiator) { - HandshakeState(StateSync.protocolName, HandshakeState.INITIATOR).apply { + HandshakeState(SyncService.protocolName, HandshakeState.INITIATOR).apply { localKeyPair.copyFrom(this@ChannelRelayed.localKeyPair) remotePublicKey.setPublicKey(Base64.getDecoder().decode(publicKey), 0) } } else { - HandshakeState(StateSync.protocolName, HandshakeState.RESPONDER).apply { + HandshakeState(SyncService.protocolName, HandshakeState.RESPONDER).apply { localKeyPair.copyFrom(this@ChannelRelayed.localKeyPair) } } diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt new file mode 100644 index 00000000..d1209abd --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncService.kt @@ -0,0 +1,721 @@ +package com.futo.platformplayer.sync.internal + +import android.content.Context +import android.net.nsd.NsdManager +import android.net.nsd.NsdServiceInfo +import android.os.Build +import android.util.Log +import com.futo.platformplayer.Settings +import com.futo.platformplayer.generateReadablePassword +import com.futo.platformplayer.getConnectedSocket +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.noise.protocol.DHState +import com.futo.platformplayer.noise.protocol.Noise +import com.futo.platformplayer.states.StateSync +import com.futo.polycentric.core.base64ToByteArray +import com.futo.polycentric.core.toBase64 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.ServerSocket +import java.net.Socket +import java.nio.ByteBuffer +import java.util.Base64 +import java.util.Locale +import kotlin.math.min + +public data class SyncServiceSettings( + val listenerPort: Int = 12315, + val mdnsBroadcast: Boolean = true, + val mdnsConnectDiscovered: Boolean = true, + val bindListener: Boolean = true, + val connectLastKnown: Boolean = true, + val relayHandshakeAllowed: Boolean = true, + val relayPairAllowed: Boolean = true, + val relayEnabled: Boolean = true, + val relayConnectDirect: Boolean = true, + val relayConnectRelayed: Boolean = true +) + +interface ISyncDatabaseProvider { + fun isAuthorized(publicKey: String): Boolean + fun addAuthorizedDevice(publicKey: String) + fun removeAuthorizedDevice(publicKey: String) + fun getAllAuthorizedDevices(): Array? + fun getAuthorizedDeviceCount(): Int + fun getSyncKeyPair(): SyncKeyPair? + fun setSyncKeyPair(value: SyncKeyPair) + fun getLastAddress(publicKey: String): String? + fun setLastAddress(publicKey: String, address: String) + fun getDeviceName(publicKey: String): String? + fun setDeviceName(publicKey: String, name: String) +} + +class SyncService( + private val serviceName: String, + private val relayServer: String, + private val relayPublicKey: String, + private val appId: UInt, + private val database: ISyncDatabaseProvider, + private val settings: SyncServiceSettings = SyncServiceSettings() +) { + private var _serverSocket: ServerSocket? = null + private var _thread: Thread? = null + private var _connectThread: Thread? = null + @Volatile private var _started = false + private val _sessions: MutableMap = mutableMapOf() + private val _lastConnectTimesMdns: MutableMap = mutableMapOf() + private val _lastConnectTimesIp: MutableMap = mutableMapOf() + var serverSocketFailedToStart = false + //TODO: Should sync mdns and casting mdns be merged? + //TODO: Decrease interval that devices are updated + //TODO: Send less data + + private val _pairingCode: String? = generateReadablePassword(8) + val pairingCode: String? get() = _pairingCode + private var _relaySession: SyncSocketSession? = null + private var _threadRelay: Thread? = null + private val _remotePendingStatusUpdate = mutableMapOf Unit>() + private var _nsdManager: NsdManager? = null + private var _scope: CoroutineScope? = null + private var _discoveryListener: NsdManager.DiscoveryListener = object : NsdManager.DiscoveryListener { + override fun onDiscoveryStarted(regType: String) { + Log.d(TAG, "Service discovery started for $regType") + } + + override fun onDiscoveryStopped(serviceType: String) { + Log.i(TAG, "Discovery stopped: $serviceType") + } + + override fun onServiceLost(service: NsdServiceInfo) { + Log.e(TAG, "service lost: $service") + // TODO: Handle service lost, e.g., remove device + } + + override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) { + Log.e(TAG, "Discovery failed for $serviceType: Error code:$errorCode") + try { + _nsdManager?.stopServiceDiscovery(this) + } catch (e: Throwable) { + Logger.w(TAG, "Failed to stop service discovery", e) + } + } + + override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { + Log.e(TAG, "Stop discovery failed for $serviceType: Error code:$errorCode") + try { + _nsdManager?.stopServiceDiscovery(this) + } catch (e: Throwable) { + Logger.w(TAG, "Failed to stop service discovery", e) + } + } + + fun addOrUpdate(name: String, adrs: Array, port: Int, attributes: Map) { + if (!Settings.instance.synchronization.connectDiscovered) { + return + } + + val urlSafePkey = attributes.get("pk")?.decodeToString() ?: return + val pkey = Base64.getEncoder().encodeToString(Base64.getDecoder().decode(urlSafePkey.replace('-', '+').replace('_', '/'))) + val syncDeviceInfo = SyncDeviceInfo(pkey, adrs.map { it.hostAddress }.toTypedArray(), port, null) + val authorized = isAuthorized(pkey) + + if (authorized && !isConnected(pkey)) { + val now = System.currentTimeMillis() + val lastConnectTime = synchronized(_lastConnectTimesMdns) { + _lastConnectTimesMdns[pkey] ?: 0 + } + + //Connect once every 30 seconds, max + if (now - lastConnectTime > 30000) { + synchronized(_lastConnectTimesMdns) { + _lastConnectTimesMdns[pkey] = now + } + + Logger.i(TAG, "Found device authorized device '${name}' with pkey=$pkey, attempting to connect") + + try { + connect(syncDeviceInfo) + } catch (e: Throwable) { + Logger.i(TAG, "Failed to connect to $pkey", e) + } + } + } + } + + override fun onServiceFound(service: NsdServiceInfo) { + Log.v(TAG, "Service discovery success for ${service.serviceType}: $service") + addOrUpdate(service.serviceName, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + service.hostAddresses.toTypedArray() + } else { + if(service.host != null) + arrayOf(service.host); + else + arrayOf(); + }, service.port, service.attributes) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + _nsdManager?.registerServiceInfoCallback(service, { it.run() }, object : NsdManager.ServiceInfoCallback { + override fun onServiceUpdated(serviceInfo: NsdServiceInfo) { + Log.v(TAG, "onServiceUpdated: $serviceInfo") + addOrUpdate(serviceInfo.serviceName, serviceInfo.hostAddresses.toTypedArray(), serviceInfo.port, serviceInfo.attributes) + } + + override fun onServiceLost() { + Log.v(TAG, "onServiceLost: $service") + // TODO: Handle service lost + } + + override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) { + Log.v(TAG, "onServiceInfoCallbackRegistrationFailed: $errorCode") + } + + override fun onServiceInfoCallbackUnregistered() { + Log.v(TAG, "onServiceInfoCallbackUnregistered") + } + }) + } else { + _nsdManager?.resolveService(service, object : NsdManager.ResolveListener { + override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.v(TAG, "Resolve failed: $errorCode") + } + + override fun onServiceResolved(serviceInfo: NsdServiceInfo) { + Log.v(TAG, "Resolve Succeeded: $serviceInfo") + addOrUpdate(serviceInfo.serviceName, arrayOf(serviceInfo.host), serviceInfo.port, serviceInfo.attributes) + } + }) + } + } + } + + private val _registrationListener = object : NsdManager.RegistrationListener { + override fun onServiceRegistered(serviceInfo: NsdServiceInfo) { + Log.v(TAG, "onServiceRegistered: ${serviceInfo.serviceName}") + } + + override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.v(TAG, "onRegistrationFailed: ${serviceInfo.serviceName} (error code: $errorCode)") + } + + override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) { + Log.v(TAG, "onServiceUnregistered: ${serviceInfo.serviceName}") + } + + override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.v(TAG, "onUnregistrationFailed: ${serviceInfo.serviceName} (error code: $errorCode)") + } + } + + var keyPair: DHState? = null + var publicKey: String? = null + + var onAuthorized: ((SyncSession, Boolean, Boolean) -> Unit)? = null + var onUnauthorized: ((SyncSession) -> Unit)? = null + var onConnectedChanged: ((SyncSession, Boolean) -> Unit)? = null + var onClose: ((SyncSession) -> Unit)? = null + var onData: ((SyncSession, UByte, UByte, ByteBuffer) -> Unit)? = null + var authorizePrompt: ((String, (Boolean) -> Unit) -> Unit)? = null + + fun start(context: Context, onServerBindFail: (() -> Unit)? = null) { + if (_started) { + Logger.i(TAG, "Already started.") + return + } + _started = true + _scope = CoroutineScope(Dispatchers.IO) + + try { + val syncKeyPair = database.getSyncKeyPair() ?: throw Exception("SyncKeyPair not found") + val p = Noise.createDH(dh) + p.setPublicKey(syncKeyPair.publicKey.base64ToByteArray(), 0) + p.setPrivateKey(syncKeyPair.privateKey.base64ToByteArray(), 0) + keyPair = p + } catch (e: Throwable) { + //Sync key pair non-existing, invalid or lost + val p = Noise.createDH(dh) + p.generateKeyPair() + + val publicKey = ByteArray(p.publicKeyLength) + p.getPublicKey(publicKey, 0) + val privateKey = ByteArray(p.privateKeyLength) + p.getPrivateKey(privateKey, 0) + + val syncKeyPair = SyncKeyPair(1, publicKey.toBase64(), privateKey.toBase64()) + database.setSyncKeyPair(syncKeyPair) + + Logger.e(TAG, "Failed to load existing key pair", e) + keyPair = p + } + + publicKey = keyPair?.let { + val pkey = ByteArray(it.publicKeyLength) + it.getPublicKey(pkey, 0) + return@let pkey.toBase64() + } + + _nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager + if (settings.mdnsConnectDiscovered) { + _nsdManager?.apply { + discoverServices(serviceName, NsdManager.PROTOCOL_DNS_SD, _discoveryListener) + } + } + + if (settings.mdnsBroadcast) { + val pk = publicKey + val nsdManager = _nsdManager + + if (pk != null && nsdManager != null) { + val sn = serviceName + val serviceInfo = NsdServiceInfo().apply { + serviceName = getDeviceName() + serviceType = sn + port = settings.listenerPort + setAttribute("pk", pk.replace('+', '-').replace('/', '_').replace("=", "")) + } + + nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, _registrationListener) + } + } + + Logger.i(TAG, "Sync key pair initialized (public key = $publicKey)") + + if (settings.bindListener) { + startListener(onServerBindFail) + } + + if (settings.relayEnabled) { + startRelayLoop() + } + + if (settings.connectLastKnown) { + startConnectLastLoop() + } + } + + private fun startListener(onServerBindFail: (() -> Unit)? = null) { + serverSocketFailedToStart = false + _thread = Thread { + try { + val serverSocket = ServerSocket(settings.listenerPort) + _serverSocket = serverSocket + + Log.i(TAG, "Running on port ${settings.listenerPort} (TCP)") + + while (_started) { + val socket = serverSocket.accept() + val session = createSocketSession(socket, true) + session.startAsResponder() + } + } catch (e: Throwable) { + Logger.e(TAG, "Failed to bind server socket to port ${settings.listenerPort}", e) + serverSocketFailedToStart = true + onServerBindFail?.invoke() + } + }.apply { start() } + } + + private fun startConnectLastLoop() { + _connectThread = Thread { + Log.i(TAG, "Running auto reconnector") + + while (_started) { + val authorizedDevices = database.getAllAuthorizedDevices() ?: arrayOf() + val addressesToConnect = authorizedDevices.mapNotNull { + val connected = isConnected(it) + if (connected) { + return@mapNotNull null + } + + val lastKnownAddress = database.getLastAddress(it) ?: return@mapNotNull null + return@mapNotNull Pair(it, lastKnownAddress) + } + + for (connectPair in addressesToConnect) { + try { + val now = System.currentTimeMillis() + val lastConnectTime = synchronized(_lastConnectTimesIp) { + _lastConnectTimesIp[connectPair.first] ?: 0 + } + + //Connect once every 30 seconds, max + if (now - lastConnectTime > 30000) { + synchronized(_lastConnectTimesIp) { + _lastConnectTimesIp[connectPair.first] = now + } + + Logger.i(TAG, "Attempting to connect to authorized device by last known IP '${connectPair.first}' with pkey=${connectPair.first}") + connect(arrayOf(connectPair.second), settings.listenerPort, connectPair.first, null) + } + } catch (e: Throwable) { + Logger.i(TAG, "Failed to connect to " + connectPair.first, e) + } + } + Thread.sleep(5000) + } + }.apply { start() } + } + + private fun startRelayLoop() { + _threadRelay = Thread { + var backoffs: Array = arrayOf(1000, 5000, 10000, 20000) + var backoffIndex = 0; + + while (_started) { + try { + Log.i(TAG, "Starting relay session...") + + var socketClosed = false; + val socket = Socket(relayServer, 9000) + _relaySession = SyncSocketSession( + (socket.remoteSocketAddress as InetSocketAddress).address.hostAddress!!, + keyPair!!, + socket, + isHandshakeAllowed = { linkType, syncSocketSession, publicKey, pairingCode, appId -> isHandshakeAllowed(linkType, syncSocketSession, publicKey, pairingCode, appId) }, + onNewChannel = { _, c -> + val remotePublicKey = c.remotePublicKey + if (remotePublicKey == null) { + Log.e(TAG, "Remote public key should never be null in onNewChannel.") + return@SyncSocketSession + } + + Log.i(TAG, "New channel established from relay (pk: '$remotePublicKey').") + + var session: SyncSession? + synchronized(_sessions) { + session = _sessions[remotePublicKey] + if (session == null) { + val remoteDeviceName = database.getDeviceName(remotePublicKey) + session = createNewSyncSession(remotePublicKey, remoteDeviceName) + _sessions[remotePublicKey] = session!! + } + session!!.addChannel(c) + } + + c.setDataHandler { _, channel, opcode, subOpcode, data -> + session?.handlePacket(opcode, subOpcode, data) + } + c.setCloseHandler { channel -> + session?.removeChannel(channel) + } + }, + onChannelEstablished = { _, channel, isResponder -> + handleAuthorization(channel, isResponder) + }, + onClose = { socketClosed = true }, + onHandshakeComplete = { relaySession -> + backoffIndex = 0 + + Thread { + try { + while (_started && !socketClosed) { + val unconnectedAuthorizedDevices = database.getAllAuthorizedDevices()?.filter { !isConnected(it) }?.toTypedArray() ?: arrayOf() + relaySession.publishConnectionInformation(unconnectedAuthorizedDevices, settings.listenerPort, settings.relayConnectDirect, false, false, settings.relayConnectRelayed) + + Logger.v(TAG, "Requesting ${unconnectedAuthorizedDevices.size} devices connection information") + val connectionInfos = runBlocking { relaySession.requestBulkConnectionInfo(unconnectedAuthorizedDevices) } + Logger.v(TAG, "Received ${connectionInfos.size} devices connection information") + + for ((targetKey, connectionInfo) in connectionInfos) { + val potentialLocalAddresses = connectionInfo.ipv4Addresses + .filter { it != connectionInfo.remoteIp } + if (connectionInfo.allowLocalDirect && Settings.instance.synchronization.connectLocalDirectThroughRelay) { + Thread { + try { + Log.v(TAG, "Attempting to connect directly, locally to '$targetKey'.") + connect(potentialLocalAddresses.map { it }.toTypedArray(), settings.listenerPort, targetKey, null) + } catch (e: Throwable) { + Log.e(TAG, "Failed to start direct connection using connection info with $targetKey.", e) + } + }.start() + } + + if (connectionInfo.allowRemoteDirect) { + // TODO: Implement direct remote connection if needed + } + + if (connectionInfo.allowRemoteHolePunched) { + // TODO: Implement hole punching if needed + } + + if (connectionInfo.allowRemoteRelayed && Settings.instance.synchronization.connectThroughRelay) { + try { + Logger.v(TAG, "Attempting relayed connection with '$targetKey'.") + runBlocking { relaySession.startRelayedChannel(targetKey, appId, null) } + } catch (e: Throwable) { + Logger.e(TAG, "Failed to start relayed channel with $targetKey.", e) + } + } + } + + Thread.sleep(15000) + } + } catch (e: Throwable) { + Logger.e(TAG, "Unhandled exception in relay session.", e) + relaySession.stop() + } + }.start() + } + ) + + _relaySession!!.authorizable = object : IAuthorizable { + override val isAuthorized: Boolean get() = true + } + + _relaySession!!.runAsInitiator(relayPublicKey, appId, null) + + Log.i(TAG, "Started relay session.") + } catch (e: Throwable) { + Log.e(TAG, "Relay session failed.", e) + } finally { + _relaySession?.stop() + _relaySession = null + Thread.sleep(backoffs[min(backoffs.size - 1, backoffIndex++)]) + } + } + }.apply { start() } + } + + private fun createSocketSession(socket: Socket, isResponder: Boolean): SyncSocketSession { + var session: SyncSession? = null + var channelSocket: ChannelSocket? = null + return SyncSocketSession( + (socket.remoteSocketAddress as InetSocketAddress).address.hostAddress!!, + keyPair!!, + socket, + onClose = { s -> + if (channelSocket != null) + session?.removeChannel(channelSocket!!) + }, + isHandshakeAllowed = { linkType, syncSocketSession, publicKey, pairingCode, appId -> isHandshakeAllowed(linkType, syncSocketSession, publicKey, pairingCode, appId) }, + onHandshakeComplete = { s -> + val remotePublicKey = s.remotePublicKey + if (remotePublicKey == null) { + s.stop() + return@SyncSocketSession + } + + Logger.i(TAG, "Handshake complete with (LocalPublicKey = ${s.localPublicKey}, RemotePublicKey = ${s.remotePublicKey})") + + channelSocket = ChannelSocket(s) + + synchronized(_sessions) { + session = _sessions[s.remotePublicKey] + if (session == null) { + val remoteDeviceName = database.getDeviceName(remotePublicKey) + database.setLastAddress(remotePublicKey, s.remoteAddress) + session = createNewSyncSession(remotePublicKey, remoteDeviceName) + _sessions[remotePublicKey] = session!! + } + session!!.addChannel(channelSocket!!) + } + + handleAuthorization(channelSocket!!, isResponder) + }, + onData = { s, opcode, subOpcode, data -> + session?.handlePacket(opcode, subOpcode, data) + } + ) + } + + private fun handleAuthorization(channel: IChannel, isResponder: Boolean) { + val syncSession = channel.syncSession!! + val remotePublicKey = channel.remotePublicKey!! + + if (isResponder) { + val isAuthorized = database.isAuthorized(remotePublicKey) + if (!isAuthorized) { + val ap = this.authorizePrompt + if (ap == null) { + try { + Logger.i(TAG, "$remotePublicKey unauthorized because AuthorizePrompt is null") + syncSession.unauthorize() + } catch (e: Throwable) { + Logger.e(TAG, "Failed to send authorize result.", e) + } + return; + } + + ap.invoke(remotePublicKey) { + try { + _scope?.launch(Dispatchers.IO) { + if (it) { + Logger.i(TAG, "$remotePublicKey manually authorized") + syncSession.authorize() + } else { + Logger.i(TAG, "$remotePublicKey manually unauthorized") + syncSession.unauthorize() + syncSession.close() + } + } + } catch (e: Throwable) { + Logger.e(TAG, "Failed to send authorize result.") + } + } + } else { + //Responder does not need to check because already approved + syncSession.authorize() + Logger.i(TAG, "Connection authorized for $remotePublicKey because already authorized") + } + } else { + //Initiator does not need to check because the manual action of scanning the QR counts as approval + syncSession.authorize() + Logger.i(TAG, "Connection authorized for $remotePublicKey because initiator") + } + } + + private fun isHandshakeAllowed(linkType: LinkType, syncSocketSession: SyncSocketSession, publicKey: String, pairingCode: String?, appId: UInt): Boolean { + Log.v(TAG, "Check if handshake allowed from '$publicKey' (app id: $appId).") + if (publicKey == StateSync.RELAY_PUBLIC_KEY) + return true + + if (database.isAuthorized(publicKey)) { + if (linkType == LinkType.Relayed && !settings.relayHandshakeAllowed) + return false + return true + } + + Log.v(TAG, "Check if handshake allowed with pairing code '$pairingCode' with active pairing code '$_pairingCode' (app id: $appId).") + if (_pairingCode == null || pairingCode.isNullOrEmpty()) + return false + + if (linkType == LinkType.Relayed && !settings.relayPairAllowed) + return false + + return _pairingCode == pairingCode + } + + private fun createNewSyncSession(rpk: String, remoteDeviceName: String?): SyncSession { + val remotePublicKey = rpk.base64ToByteArray().toBase64() + return SyncSession( + remotePublicKey, + onAuthorized = { it, isNewlyAuthorized, isNewSession -> + synchronized(_remotePendingStatusUpdate) { + _remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(true, "Authorized") + } + + if (isNewSession) { + it.remoteDeviceName?.let { remoteDeviceName -> + database.setDeviceName(remotePublicKey, remoteDeviceName) + } + + database.addAuthorizedDevice(remotePublicKey) + } + + onAuthorized?.invoke(it, isNewlyAuthorized, isNewSession) + }, + onUnauthorized = { + synchronized(_remotePendingStatusUpdate) { + _remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(false, "Unauthorized") + } + + onUnauthorized?.invoke(it) + }, + onConnectedChanged = { it, connected -> + Logger.i(TAG, "$remotePublicKey connected: $connected") + onConnectedChanged?.invoke(it, connected) + }, + onClose = { + Logger.i(TAG, "$remotePublicKey closed") + + removeSession(it.remotePublicKey) + synchronized(_remotePendingStatusUpdate) { + _remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(false, "Connection closed") + } + + onClose?.invoke(it) + }, + dataHandler = { it, opcode, subOpcode, data -> + onData?.invoke(it, opcode, subOpcode, data) + }, + remoteDeviceName + ) + } + + fun isConnected(publicKey: String): Boolean = synchronized(_sessions) { _sessions[publicKey]?.connected ?: false } + fun isAuthorized(publicKey: String): Boolean = database.isAuthorized(publicKey) + fun getSession(publicKey: String): SyncSession? = synchronized(_sessions) { _sessions[publicKey] } + fun getSessions(): List = synchronized(_sessions) { _sessions.values.toList() } + fun removeSession(publicKey: String) = synchronized(_sessions) { _sessions.remove(publicKey) } + fun getCachedName(publicKey: String): String? = database.getDeviceName(publicKey) + fun getAuthorizedDeviceCount(): Int = database.getAuthorizedDeviceCount() + fun getAllAuthorizedDevices(): Array? = database.getAllAuthorizedDevices() + fun removeAuthorizedDevice(publicKey: String) = database.removeAuthorizedDevice(publicKey) + + fun connect(deviceInfo: SyncDeviceInfo, onStatusUpdate: ((complete: Boolean?, message: String) -> Unit)? = null) { + try { + connect(deviceInfo.addresses, deviceInfo.port, deviceInfo.publicKey, deviceInfo.pairingCode, onStatusUpdate) + } catch (e: Throwable) { + Logger.e(TAG, "Failed to connect directly", e) + val relaySession = _relaySession + if (relaySession != null && Settings.instance.synchronization.pairThroughRelay) { + onStatusUpdate?.invoke(null, "Connecting via relay...") + + runBlocking { + if (onStatusUpdate != null) { + synchronized(_remotePendingStatusUpdate) { + _remotePendingStatusUpdate[deviceInfo.publicKey.base64ToByteArray().toBase64()] = onStatusUpdate + } + } + relaySession.startRelayedChannel(deviceInfo.publicKey.base64ToByteArray().toBase64(), appId, deviceInfo.pairingCode) + } + } else { + throw e + } + } + } + + fun connect(addresses: Array, port: Int, publicKey: String, pairingCode: String?, onStatusUpdate: ((complete: Boolean?, message: String) -> Unit)? = null): SyncSocketSession { + onStatusUpdate?.invoke(null, "Connecting directly...") + val socket = getConnectedSocket(addresses.map { InetAddress.getByName(it) }, port) ?: throw Exception("Failed to connect") + onStatusUpdate?.invoke(null, "Handshaking...") + + val session = createSocketSession(socket, false) + if (onStatusUpdate != null) { + synchronized(_remotePendingStatusUpdate) { + _remotePendingStatusUpdate[publicKey.base64ToByteArray().toBase64()] = onStatusUpdate + } + } + + session.startAsInitiator(publicKey, appId, pairingCode) + return session + } + + fun stop() { + _scope?.cancel() + _scope = null + _relaySession?.stop() + _relaySession = null + _serverSocket?.close() + _serverSocket = null + synchronized(_sessions) { + _sessions.values.forEach { it.close() } + _sessions.clear() + } + } + + private fun getDeviceName(): String { + val manufacturer = Build.MANUFACTURER.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + val model = Build.MODEL + + return if (model.startsWith(manufacturer, ignoreCase = true)) { + model.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + } else { + "$manufacturer $model".replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + } + } + + companion object { + val dh = "25519" + val pattern = "IK" + val cipher = "ChaChaPoly" + val hash = "BLAKE2b" + var protocolName = "Noise_${pattern}_${dh}_${cipher}_${hash}" + + private const val TAG = "SyncService" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt index 3dc81334..fb4e920f 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt @@ -1,9 +1,6 @@ package com.futo.platformplayer.sync.internal import android.os.Build -import com.futo.platformplayer.LittleEndianDataInputStream -import com.futo.platformplayer.LittleEndianDataOutputStream -import com.futo.platformplayer.copyToOutputStream import com.futo.platformplayer.ensureNotMainThread import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.noise.protocol.CipherStatePair @@ -11,7 +8,6 @@ import com.futo.platformplayer.noise.protocol.DHState import com.futo.platformplayer.noise.protocol.HandshakeState import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateSync -import com.futo.platformplayer.sync.internal.ChannelRelayed.Companion import com.futo.polycentric.core.base64ToByteArray import com.futo.polycentric.core.toBase64 import kotlinx.coroutines.CompletableDeferred @@ -34,9 +30,7 @@ import java.util.Locale import java.util.concurrent.ConcurrentHashMap import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream -import kotlin.math.min import kotlin.system.measureTimeMillis -import kotlin.time.measureTime class SyncSocketSession { private val _socket: Socket @@ -257,7 +251,7 @@ class SyncSocketSession { private fun handshakeAsInitiator(remotePublicKey: String, appId: UInt, pairingCode: String?) { performVersionCheck() - val initiator = HandshakeState(StateSync.protocolName, HandshakeState.INITIATOR) + val initiator = HandshakeState(SyncService.protocolName, HandshakeState.INITIATOR) initiator.localKeyPair.copyFrom(_localKeyPair) initiator.remotePublicKey.setPublicKey(Base64.getDecoder().decode(remotePublicKey), 0) initiator.start() @@ -311,7 +305,7 @@ class SyncSocketSession { private fun handshakeAsResponder(): Boolean { performVersionCheck() - val responder = HandshakeState(StateSync.protocolName, HandshakeState.RESPONDER) + val responder = HandshakeState(SyncService.protocolName, HandshakeState.RESPONDER) responder.localKeyPair.copyFrom(_localKeyPair) responder.start()