Untitled

Java boxlabs 30 Views Size: 11.88 KB Posted on: Jan 1, 26 @ 9:12 PM
  1. package com.boxlabs.hexdroidirc
  2.  
  3. import android.content.Context
  4. import android.net.Uri
  5. import androidx.core.content.FileProvider
  6. import kotlin.concurrent.thread
  7. import kotlinx.coroutines.Dispatchers
  8. import kotlinx.coroutines.withContext
  9. import java.io.BufferedOutputStream
  10. import java.io.File
  11. import java.io.FileOutputStream
  12. import java.io.IOException
  13. import java.nio.ByteOrder
  14. import java.net.Inet4Address
  15. import java.net.NetworkInterface
  16. import java.net.ServerSocket
  17. import java.net.Socket
  18. import java.nio.ByteBuffer
  19. import java.util.Collections
  20. import java.util.concurrent.atomic.AtomicLong
  21.  
  22. /**
  23.  * Parsed CTCP DCC SEND offer.
  24.  *
  25.  * Supports classic (active) DCC and passive/reverse DCC (token + port 0 handshake).
  26.  */
  27. data class DccOffer(
  28.     /** Network id this offer belongs to (filled by the ViewModel). */
  29.     val netId: String = "",
  30.     val from: String,
  31.     val filename: String,
  32.     val ip: String,
  33.     val port: Int,
  34.     val size: Long,
  35.     // Passive/reverse DCC token
  36.     val token: Long? = null,
  37.     // Turbo DCC / TSEND: do not send ACKs back to the sender.
  38.     val turbo: Boolean = false
  39. ) {
  40.     val isPassive: Boolean get() = port == 0 && token != null
  41. }
  42.  
  43. sealed class DccTransferState {
  44.     data class Incoming(
  45.         val offer: DccOffer,
  46.         val received: Long = 0,
  47.         val done: Boolean = false,
  48.         val error: String? = null,
  49.         val savedPath: String? = null
  50.     ) : DccTransferState()
  51.  
  52.     data class Outgoing(
  53.         val target: String,
  54.         val filename: String,
  55.         val bytesSent: Long = 0,
  56.         val done: Boolean = false,
  57.         val error: String? = null
  58.     ) : DccTransferState()
  59. }
  60.  
  61. class DccManager(ctx: Context) {
  62.  
  63.     // Avoid leaking an Activity context.
  64.     private val ctx: Context = ctx.applicationContext
  65.  
  66.     fun dccDir(): File = File(ctx.filesDir, "dcc").apply { mkdirs() }
  67.  
  68.     // local IPv4 in dotted notation (may be private RFC1918)
  69.     fun localIpv4OrNull(): String? {
  70.         return try {
  71.             val ifaces = Collections.list(NetworkInterface.getNetworkInterfaces())
  72.             for (iface in ifaces) {
  73.                 if (!iface.isUp || iface.isLoopback) continue
  74.                 val addrs = Collections.list(iface.inetAddresses)
  75.                 for (a in addrs) {
  76.                     if (a is Inet4Address && !a.isLoopbackAddress && !a.isLinkLocalAddress) {
  77.                         return a.hostAddress
  78.                     }
  79.                 }
  80.             }
  81.             null
  82.         } catch (_: Throwable) {
  83.             null
  84.         }
  85.     }
  86.  
  87.     // local IPv4 as an unsigned 32-bit integer (decimal string in CTCP)
  88.     fun localIpv4AsInt(): Long {
  89.         val ip = localIpv4OrNull() ?: return 0L
  90.         return ipv4ToLongBestEffort(ip)
  91.     }
  92.  
  93.     // Standard DCC RECEIVE (we connect to sender's ip:port).
  94.     suspend fun receive(
  95.         offer: DccOffer,
  96.         onProgress: (Long, Long) -> Unit
  97.     ): File = withContext(Dispatchers.IO) {
  98.         val safeName = offer.filename.substringAfterLast('/').substringAfterLast('\\')
  99.         val outFile = File(dccDir(), safeName)
  100.  
  101.         Socket(offer.ip, offer.port).use { sock ->
  102.             receiveFromSocket(sock, outFile, offer.size, offer.turbo, onProgress)
  103.         }
  104.  
  105.         outFile
  106.     }
  107.  
  108.     /**
  109.      * Passive/Reverse DCC RECEIVE.
  110.      *
  111.      * The remote sender offered port 0 + token. We open a listening port, reply with the port,
  112.      * then accept the incoming connection and receive.
  113.      */
  114.     suspend fun receivePassive(
  115.         offer: DccOffer,
  116.         portMin: Int,
  117.         portMax: Int,
  118.         onListening: suspend (ipAsInt: Long, port: Int, size: Long, token: Long) -> Unit,
  119.         onProgress: (Long, Long) -> Unit
  120.     ): File = withContext(Dispatchers.IO) {
  121.         val token = offer.token ?: throw IllegalArgumentException("Passive DCC missing token")
  122.         val safeName = offer.filename.substringAfterLast('/').substringAfterLast('\\')
  123.         val outFile = File(dccDir(), safeName)
  124.  
  125.         val ss = bindFirstAvailable(portMin, portMax)
  126.         try {
  127.             ss.soTimeout = 45_000
  128.             val ipInt = localIpv4AsInt()
  129.             onListening(ipInt, ss.localPort, offer.size, token)
  130.  
  131.             val sock = try {
  132.                 ss.accept()
  133.             } catch (_: java.net.SocketTimeoutException) {
  134.                 throw RuntimeException("DCC RECEIVE timed out waiting for sender to connect")
  135.             }
  136.  
  137.             sock.use { s ->
  138.                 receiveFromSocket(s, outFile, offer.size, offer.turbo, onProgress)
  139.             }
  140.         } finally {
  141.             runCatching { ss.close() }
  142.         }
  143.  
  144.         outFile
  145.     }
  146.  
  147.    
  148.     // Active DCC SEND: we listen on a port in portMin/portMax and send when peer connects.
  149.     suspend fun sendFile(
  150.         file: File,
  151.         portMin: Int,
  152.         portMax: Int,
  153.         onClient: suspend (ipAsInt: Long, port: Int, size: Long) -> Unit,
  154.         onProgress: (Long, Long) -> Unit
  155.     ): Unit = withContext(Dispatchers.IO) {
  156.         val size = file.length()
  157.         val ss = bindFirstAvailable(portMin, portMax)
  158.         try {
  159.             val ipInt = localIpv4AsInt()
  160.             onClient(ipInt, ss.localPort, size)
  161.  
  162.             ss.soTimeout = 45_000
  163.             val sock = try {
  164.                 ss.accept()
  165.             } catch (_: java.net.SocketTimeoutException) {
  166.                 throw RuntimeException("DCC SEND timed out waiting for peer to connect")
  167.             }
  168.  
  169.             sock.use { s ->
  170.                 sendOverSocket(s, file, size, onProgress)
  171.             }
  172.         } finally {
  173.             runCatching { ss.close() }
  174.         }
  175.     }
  176.  
  177.     //Passive/Reverse DCC SEND: peer opened a port; we connect out and send.
  178.     suspend fun sendFileConnect(
  179.         file: File,
  180.         host: String,
  181.         port: Int,
  182.         onProgress: (Long, Long) -> Unit
  183.     ): Unit = withContext(Dispatchers.IO) {
  184.         val size = file.length()
  185.         Socket(host, port).use { s ->
  186.             sendOverSocket(s, file, size, onProgress)
  187.         }
  188.     }
  189.  
  190.     private fun receiveFromSocket(
  191.         sock: Socket,
  192.         outFile: File,
  193.         expectedSize: Long,
  194.         turbo: Boolean,
  195.         onProgress: (Long, Long) -> Unit
  196.     ) {
  197.         sock.tcpNoDelay = true
  198.         sock.getInputStream().use { inp ->
  199.             FileOutputStream(outFile).use { fos ->
  200.                 val buf = ByteArray(32 * 1024)
  201.                 var total = 0L
  202.                 val expected: Long? = expectedSize.takeIf { it > 0L }
  203.  
  204.                 while (true) {
  205.                     val n = inp.read(buf)
  206.                     if (n <= 0) break
  207.  
  208.                     fos.write(buf, 0, n)
  209.                     total += n
  210.                     onProgress(total, expectedSize)
  211.                     if (!turbo) {
  212.                         // DCC SEND expects an ACK of total bytes received (4-byte unsigned int, network byte order).
  213.                         val ackInt = total.coerceAtMost(0xFFFFFFFFL).toInt()
  214.                         val ack = ByteBuffer.allocate(4).putInt(ackInt).array()
  215.                         runCatching { sock.getOutputStream().write(ack) }
  216.                     }
  217.  
  218.                     if (expected != null && total >= expected) break
  219.                 }
  220.             }
  221.         }
  222.     }
  223.  
  224.     private fun sendOverSocket(
  225.         sock: Socket,
  226.         file: File,
  227.         size: Long,
  228.         onProgress: (Long, Long) -> Unit
  229.     ) {
  230.         sock.tcpNoDelay = true
  231.         // Used by the ACK reader thread.
  232.         sock.soTimeout = 1_000
  233.  
  234.         val acked = AtomicLong(0L)
  235.  
  236.         fun u32be(b: ByteArray): Long =
  237.             (ByteBuffer.wrap(b).order(ByteOrder.BIG_ENDIAN).int.toLong() and 0xFFFFFFFFL)
  238.  
  239.         fun u32le(b: ByteArray): Long =
  240.             (ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).int.toLong() and 0xFFFFFFFFL)
  241.  
  242.         fun chooseAck(be: Long, le: Long, last: Long): Long? {
  243.             // DCC ACKs are commonly network byte order, but some clients historically send host order.
  244.             // We choose the value that is monotonic and plausible for this transfer size.
  245.             val cands = sequenceOf(be, le).distinct().filter { it >= last }.toList()
  246.             if (cands.isEmpty()) return null
  247.             if (size > 0L) {
  248.                 // Prefer candidates <= size.
  249.                 val inRange = cands.filter { it <= size }
  250.                 if (inRange.isNotEmpty()) return inRange.maxOrNull()
  251.  
  252.                 // Some clients may overshoot slightly; clamp.
  253.                 val near = cands.filter { it <= size + 1024 * 1024L }
  254.                 if (near.isNotEmpty()) return size
  255.             }
  256.             return cands.maxOrNull()
  257.         }
  258.  
  259.         val ackThread = thread(start = true, isDaemon = true, name = "dcc-ack-reader") {
  260.             val inp = sock.getInputStream()
  261.             val b = ByteArray(4)
  262.             var off = 0
  263.             var last = 0L
  264.             while (!Thread.currentThread().isInterrupted) {
  265.                 try {
  266.                     val n = inp.read(b, off, 4 - off)
  267.                     if (n < 0) break
  268.                     off += n
  269.                     if (off == 4) {
  270.                         val be = u32be(b)
  271.                         val le = u32le(b)
  272.                         val chosen = chooseAck(be, le, last)
  273.                         if (chosen != null) {
  274.                             last = chosen
  275.                             acked.set(chosen)
  276.                         }
  277.                         off = 0
  278.                     }
  279.                 } catch (_: java.net.SocketTimeoutException) {
  280.                     // keep polling
  281.                 } catch (_: Throwable) {
  282.                     break
  283.                 }
  284.             }
  285.         }
  286.  
  287.         var sent = 0L
  288.         try {
  289.             val outRaw = sock.getOutputStream()
  290.             val out = BufferedOutputStream(outRaw, 64 * 1024)
  291.             val buf = ByteArray(32 * 1024)
  292.  
  293.             file.inputStream().use { fin ->
  294.                 while (true) {
  295.                     val n = fin.read(buf)
  296.                     if (n <= 0) break
  297.                     try {
  298.                         out.write(buf, 0, n)
  299.                         sent += n
  300.                         onProgress(sent, size)
  301.                     } catch (io: IOException) {
  302.                         // If the peer already ACKed the full size, treat as success.
  303.                         if (size > 0L && acked.get() >= size) break
  304.                         throw io
  305.                     }
  306.                 }
  307.             }
  308.             out.flush()
  309.  
  310.             // Half-close so receiver sees EOF; then wait briefly for final ACK/peer close.
  311.             runCatching { sock.shutdownOutput() }
  312.  
  313.             val deadline = System.currentTimeMillis() + 10_000L
  314.             while (size > 0L && acked.get() < size && System.currentTimeMillis() < deadline) {
  315.                 // If the receiver closed, the ACK thread will stop.
  316.                 if (!ackThread.isAlive) break
  317.                 Thread.sleep(50)
  318.             }
  319.         } finally {
  320.             runCatching { ackThread.interrupt() }
  321.         }
  322. }
  323.  
  324.     private fun bindFirstAvailable(min: Int, max: Int): ServerSocket {
  325.         val a = min.coerceIn(1, 65535)
  326.         val b = max.coerceIn(1, 65535)
  327.         for (p in a..b) {
  328.             try {
  329.                 return ServerSocket(p)
  330.             } catch (_: Throwable) {
  331.                 // try next
  332.             }
  333.         }
  334.         throw IllegalStateException("No free port in $a..$b")
  335.     }
  336.  
  337.     private fun ipv4ToLongBestEffort(ip: String): Long {
  338.         val parts = ip.split(".")
  339.         if (parts.size != 4) return 0L
  340.         return try {
  341.             var out = 0L
  342.             for (p in parts) out = (out shl 8) or (p.toLong() and 0xFFL)
  343.             out
  344.         } catch (_: Throwable) {
  345.             0L
  346.         }
  347.     }
  348.  
  349.     @Suppress("unused")
  350.     fun contentUriForFile(file: File): Uri =
  351.         FileProvider.getUriForFile(ctx, "${ctx.packageName}.fileprovider", file)
  352. }

Raw Paste

Comments 0
Login to post a comment.
  • No comments yet. Be the first.
Login to post a comment. Login or Register
We use cookies. To comply with GDPR in the EU and the UK we have to show you these.

We use cookies and similar technologies to keep this website functional (including spam protection via Google reCAPTCHA or Cloudflare Turnstile), and — with your consent — to measure usage and show ads. See Privacy.