1package com.peachpasswords.sync23import kotlinx.coroutines.sync.Mutex4import kotlinx.coroutines.sync.withLock5import javax.inject.Inject6import javax.inject.Singleton789interface RelayPolicy {10 fun isRelayEnabled(): Boolean11}12131415161718192021class RelayLink internal constructor(22 val peerKeyHex: String,23 private val sendPlaintext: suspend (ByteArray) -> Unit,24) {25 internal enum class Mode { IDLE, SYNCING, RELAYING }2627 private val writeLock = Mutex()28 internal var mode: Mode = Mode.IDLE29 internal var partner: RelayLink? = null3031 internal suspend fun send(message: RelayMessage) = sendRaw(message.encodeToBytes())3233 34 internal suspend fun sendRaw(frame: ByteArray) = writeLock.withLock { sendPlaintext(frame) }35}36373839404142434445464748495051525354@Singleton55class RelayHub @Inject constructor(56 private val policy: RelayPolicy,57) {58 private val lock = Mutex()59 private val links = mutableMapOf<String, RelayLink>()6061 62 suspend fun register(peerKeyHex: String, sendPlaintext: suspend (ByteArray) -> Unit): RelayLink {63 val link = RelayLink(peerKeyHex.lowercase(), sendPlaintext)64 lock.withLock { links[link.peerKeyHex] = link }65 return link66 }6768 suspend fun unregister(link: RelayLink) {69 val partner = lock.withLock {70 if (links[link.peerKeyHex] === link) links.remove(link.peerKeyHex)71 detachLocked(link)72 }73 74 75 partner?.let { runCatching { it.send(RelayMessage.Close) } }76 }7778 79 suspend fun beginSync(link: RelayLink): Boolean = lock.withLock {80 if (link.mode != RelayLink.Mode.IDLE) return@withLock false81 link.mode = RelayLink.Mode.SYNCING82 true83 }8485 suspend fun endSync(link: RelayLink) = lock.withLock {86 if (link.mode == RelayLink.Mode.SYNCING) link.mode = RelayLink.Mode.IDLE87 }8889 90919293 suspend fun onRequest(from: RelayLink, targetKeyHex: String) {94 if (!policy.isRelayEnabled()) {95 from.send(RelayMessage.Disabled)96 return97 }98 val target = lock.withLock {99 val candidate = links[targetKeyHex.lowercase()]100 if (candidate == null ||101 candidate === from ||102 candidate.mode != RelayLink.Mode.IDLE ||103 from.mode != RelayLink.Mode.IDLE104 ) {105 null106 } else {107 from.partner = candidate108 candidate.partner = from109 from.mode = RelayLink.Mode.RELAYING110 candidate.mode = RelayLink.Mode.RELAYING111 candidate112 }113 }114 if (target == null) {115 from.send(RelayMessage.Unavailable)116 return117 }118 119 120 121 try {122 target.send(RelayMessage.Open(from.peerKeyHex))123 } catch (_: Exception) {124 lock.withLock { detachLocked(from) }125 from.send(RelayMessage.Unavailable)126 return127 }128 from.send(RelayMessage.Accept)129 }130131 132133134135 suspend fun forward(from: RelayLink, frame: ByteArray): Boolean {136 val partner = lock.withLock { from.partner } ?: return false137 return try {138 partner.sendRaw(frame)139 true140 } catch (_: Exception) {141 closeChannel(from)142 false143 }144 }145146 147 suspend fun closeChannel(link: RelayLink) {148 val partner = lock.withLock { detachLocked(link) }149 partner?.let { runCatching { it.send(RelayMessage.Close) } }150 }151152 153 private fun detachLocked(link: RelayLink): RelayLink? {154 val partner = link.partner155 link.partner = null156 if (link.mode == RelayLink.Mode.RELAYING) link.mode = RelayLink.Mode.IDLE157 if (partner != null) {158 partner.partner = null159 if (partner.mode == RelayLink.Mode.RELAYING) partner.mode = RelayLink.Mode.IDLE160 }161 return partner162 }163}