diff --git a/README.md b/README.md index 7ce6977..7db7aaa 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # native-mdns A Kotlin multiplatform library for mdns announcer(mdns server), only broadcast is supported, -it depends on ktor-network.About Also, nmdns can be compiled to shared/static lib for other language calling, see [build.gradle.kts](build.gradle.kts) @@ -63,4 +62,51 @@ while (true) { service.broadcast() delay(2000L) // or use cn.rtast.nmdns.sleep1(2) sleep 2 seconds } +``` + +# C++ + +```cpp +#include +#include +#include "libnmdns_api.h" + +volatile sig_atomic_t keepRunning = 1; + +void handle_sigint(int sig) { + std::printf("\nCaught signal %d, exiting...\n", sig); + keepRunning = 0; +} + +int main() { + libnmdns_kref_kotlin_collections_List txt = create_txt_records(); + + add_txt_record(txt, "deviceid=ef:42:0d:76:f1:97"); + add_txt_record(txt, "model=AppleTV3,2C"); + add_txt_record(txt, "features=0x5A7FFFF7,0x1E"); + add_txt_record(txt, "srcvers=220.68"); + add_txt_record(txt, "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"); + + libnmdns_kref_cn_rtast_nmdns_NMDNSAnnouncer service = register_service( + "_airplay._tcp.local.", + "AP@RTAST", + "rtakland.local.", + "192.168.10.104", + 7001, 3652, "192.168.10.104", + txt + ); + + std::signal(SIGINT, handle_sigint); + std::signal(SIGTERM, handle_sigint); + std::signal(SIGHUP, handle_sigint); + std::signal(SIGQUIT, handle_sigint); + + std::printf("Running... Press Ctrl+C to exit\n"); + + while (keepRunning) { + broadcast(service); + sleep1(2); + } + return 0; +} ``` \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 8e26d3d..3aaaa0e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,12 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - kotlin("multiplatform") version "2.2.10" + kotlin("multiplatform") version "2.2.0" id("maven-publish") } group = "cn.rtast.nmdns" -version = "0.0.2" +version = "0.1.0" repositories { mavenCentral() @@ -32,9 +32,13 @@ kotlin { } } + compilerOptions { freeCompilerArgs.addAll("-Xexpect-actual-classes") } + + explicitApi() + sourceSets { commonMain.dependencies { - implementation("io.ktor:ktor-network:3.2.3") + } commonTest.dependencies { diff --git a/src/appleMain/kotlin/cn/rtast/nmdns/socket.apple.kt b/src/appleMain/kotlin/cn/rtast/nmdns/socket.apple.kt new file mode 100644 index 0000000..504c795 --- /dev/null +++ b/src/appleMain/kotlin/cn/rtast/nmdns/socket.apple.kt @@ -0,0 +1,69 @@ +/* + * Copyright © 2025 RTAkland + * Author: RTAkland + * Date: 9/30/25 + */ + + +@file:OptIn(ExperimentalForeignApi::class) +@file:Suppress("unused") + +package cn.rtast.nmdns + +import kotlinx.cinterop.* +import platform.darwin.inet_addr +import platform.posix.* + +public actual class Socket1 internal actual constructor() { + /** + * socket fd + */ + private var socket: Int = -1 + + public actual fun bind(ip: String, port: Int): Socket1 { + socket = socket(AF_INET, SOCK_DGRAM, 0) + if (socket < 0) perror("Failed to create udp socket") + memScoped { + val reuse = alloc() + reuse.value = 1 + if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, reuse.ptr, sizeOf().convert()) < 0) { + perror("Failed to setsocketopt SO_REUSEADDR") + } + } + memScoped { + val addr = alloc() + addr.sin_family = AF_INET.convert() + addr.sin_port = htons(port.toUShort()) + addr.sin_addr.s_addr = htonl(INADDR_ANY) + if (bind(socket, addr.ptr.reinterpret(), sizeOf().convert()) < 0) { + perror("Failed to bind") + close(socket) + } + } + memScoped { + val mreq = alloc() + mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.251") + mreq.imr_interface.s_addr = htonl(INADDR_ANY) + if (setsockopt(socket, IPPROTO_TP, IP_ADD_MEMBERSHIP, mreq.ptr, sizeOf().convert()) < 0) { + perror("setsockopt IP_ADD_MEMBERSHIP") + } + } + + return this + } + + public actual fun send(packet: ByteArray) { + memScoped { + val addr = alloc() + addr.sin_family = AF_INET.convert() + addr.sin_port = htons(5353u) + addr.sin_addr.s_addr = inet_addr("224.0.0.251") + val cbuf = packet.refTo(0).getPointer(this) + sendto(socket, cbuf, packet.size.convert(), 0, addr.ptr.reinterpret(), sizeOf().convert()) + } + } + + public actual fun destroy() { + close(socket) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/CExport.kt b/src/commonMain/kotlin/cn/rtast/nmdns/CExport.kt new file mode 100644 index 0000000..020c398 --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/nmdns/CExport.kt @@ -0,0 +1,13 @@ +/* + * Copyright © 2025 RTAkland + * Author: RTAkland + * Date: 9/30/25 + */ + + +package cn.rtast.nmdns + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.BINARY) +@RequiresOptIn +public annotation class CExport \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/mac.kt b/src/commonMain/kotlin/cn/rtast/nmdns/mac.kt index d2e700d..3034afa 100644 --- a/src/commonMain/kotlin/cn/rtast/nmdns/mac.kt +++ b/src/commonMain/kotlin/cn/rtast/nmdns/mac.kt @@ -7,40 +7,16 @@ package cn.rtast.nmdns -private val macAddresses = listOf( - "ef:42:0d:76:f1:97", - "b4:91:7c:be:84:7b", - "38:d0:d3:f5:63:d1", - "90:d0:d5:3e:5b:fa", - "4d:84:d4:dd:7e:b4", - "11:1c:fb:06:6a:4a", - "cf:97:be:b3:18:5b", - "0c:34:bf:d8:55:71", - "95:a0:71:10:31:ab", - "4c:33:b8:4a:60:8e", - "fc:89:a5:f2:b6:3c", - "a7:42:99:34:b5:21", - "9a:55:3f:5d:bb:9d", - "2b:35:ce:77:a0:7c", - "7d:51:21:72:c5:b4", - "79:7d:cc:e1:30:9b", - "aa:a2:cf:b5:e3:3c", - "8d:73:3f:8e:ef:d7", - "21:3c:a4:56:80:c5", - "c1:fa:ca:cb:ec:f3", - "51:76:e2:12:2f:af", - "a8:03:22:2d:b9:80", - "2a:84:3b:ad:9c:d9", - "24:79:c3:ab:0f:b5", - "bc:44:e3:c4:9e:be", - "cb:1b:84:c2:94:ba", - "9d:66:ce:d2:6e:4f", - "43:bf:e2:b2:e3:78", - "48:25:03:bb:9d:35", - "c3:c9:1c:3d:ae:dd" -) +import kotlin.experimental.ExperimentalNativeApi +import kotlin.native.CName +import kotlin.random.Random /** - * get random mac address + * generate random mac address */ -fun randomMacAddress(): String = macAddresses.random() \ No newline at end of file +@OptIn(ExperimentalNativeApi::class) +@CName("random_mac_address") +public fun randomMacAddress(): String { + val bytes = ByteArray(6) { Random.nextInt(0, 256).toByte() } + return bytes.joinToString(":") { it.toHexString() } +} diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/nmdns_server.kt b/src/commonMain/kotlin/cn/rtast/nmdns/nmdns_announcer.kt similarity index 64% rename from src/commonMain/kotlin/cn/rtast/nmdns/nmdns_server.kt rename to src/commonMain/kotlin/cn/rtast/nmdns/nmdns_announcer.kt index a0fed84..78e01ef 100644 --- a/src/commonMain/kotlin/cn/rtast/nmdns/nmdns_server.kt +++ b/src/commonMain/kotlin/cn/rtast/nmdns/nmdns_announcer.kt @@ -5,19 +5,14 @@ */ +@file:OptIn(ExperimentalNativeApi::class) + package cn.rtast.nmdns -import io.ktor.network.selector.* -import io.ktor.network.sockets.* -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.io.Buffer +import kotlin.experimental.ExperimentalNativeApi +import kotlin.native.CName -data class NMDNSAnnouncer( - /** - * coroutine dispatcher for running server - */ - internal val dispatcher: CoroutineDispatcher = Dispatchers.Default, +public data class NMDNSAnnouncer( /** * service type * airplay service type: _airplay._tcp.local. @@ -50,12 +45,60 @@ data class NMDNSAnnouncer( /** * mdns socket server */ - internal val server: BoundDatagramSocket, + internal val server: Socket1, /** * srv packet */ - internal val packet: Buffer, -) + internal val packet: ByteArray, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as NMDNSAnnouncer + + if (port != other.port) return false + if (mdnsPort != other.mdnsPort) return false + if (serviceType != other.serviceType) return false + if (serviceName != other.serviceName) return false + if (hostname != other.hostname) return false + if (ipAddress != other.ipAddress) return false + if (txtRecords != other.txtRecords) return false + if (server != other.server) return false + if (!packet.contentEquals(other.packet)) return false + + return true + } + + override fun hashCode(): Int { + var result = port + result = 31 * result + mdnsPort + result = 31 * result + serviceType.hashCode() + result = 31 * result + serviceName.hashCode() + result = 31 * result + hostname.hashCode() + result = 31 * result + ipAddress.hashCode() + result = 31 * result + txtRecords.hashCode() + result = 31 * result + server.hashCode() + result = 31 * result + packet.contentHashCode() + return result + } + + /** + * broadcast packet + */ + public fun broadcast() { + this.server.send(packet) + } +} + +/** + * export for c + */ +@CExport +@CName("broadcast") +public fun broadcast1(server: NMDNSAnnouncer) { + server.server.send(server.packet) +} /** * register service @@ -68,7 +111,8 @@ data class NMDNSAnnouncer( * "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336" * ) */ -suspend fun registerService( +@CName("register_service") +public fun registerService( serviceType: String, serviceName: String, hostname: String, @@ -78,11 +122,9 @@ suspend fun registerService( bindAddress: String, txtRecords: List, ): NMDNSAnnouncer { - val manager = SelectorManager(Dispatchers.Default) - val serverSocket = aSocket(manager).udp() - .bind(InetSocketAddress(bindAddress, mdnsPort)) + val server = createSocket() + .bind(bindAddress, port) return NMDNSAnnouncer( - Dispatchers.Default, serviceType, serviceName, hostname, @@ -90,18 +132,11 @@ suspend fun registerService( port, mdnsPort, txtRecords, - serverSocket, + server, buildPacket(serviceType, "$serviceName.$serviceType", hostname, ipAddress, port, txtRecords) ) } -/** - * broadcast packet - */ -suspend fun NMDNSAnnouncer.broadcast() { - this.server.send(Datagram(packet.copy(), InetSocketAddress("224.0.0.251", 5353))) -} - internal fun buildPacket( serviceType: String, serviceName: String, @@ -109,7 +144,7 @@ internal fun buildPacket( ip: String, port: Int, txtRecords: List, -): Buffer { +): ByteArray { val records = mutableListOf() records += encodeRR(serviceType, 12, encodeName(serviceName)) @@ -133,8 +168,7 @@ internal fun buildPacket( header[6] = ((records.size shr 8) and 0xFF).toByte() header[7] = (records.size and 0xFF).toByte() - val buffer = Buffer().apply { write(header + records.reduce { acc, r -> acc + r }) } - return buffer + return header + records.reduce { acc, r -> acc + r } } internal fun encodeRR(name: String, type: Int, data: ByteArray): ByteArray { diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/sleep1.kt b/src/commonMain/kotlin/cn/rtast/nmdns/sleep1.kt index bcc17c8..7068c1d 100644 --- a/src/commonMain/kotlin/cn/rtast/nmdns/sleep1.kt +++ b/src/commonMain/kotlin/cn/rtast/nmdns/sleep1.kt @@ -11,4 +11,5 @@ package cn.rtast.nmdns * block current thread * unit: second */ -internal expect fun sleep1(time: Int) \ No newline at end of file + +public expect fun sleep1(time: Int) \ No newline at end of file diff --git a/src/commonMain/kotlin/cn/rtast/nmdns/socket.kt b/src/commonMain/kotlin/cn/rtast/nmdns/socket.kt new file mode 100644 index 0000000..ef4ed85 --- /dev/null +++ b/src/commonMain/kotlin/cn/rtast/nmdns/socket.kt @@ -0,0 +1,38 @@ +/* + * Copyright © 2025 RTAkland + * Author: RTAkland + * Date: 9/30/25 + */ + +@file:Suppress("unused") + + +package cn.rtast.nmdns + +public expect class Socket1 internal constructor() { + /** + * bind to ip + */ + public fun bind(ip: String, port: Int): Socket1 + + /** + * send packet + */ + public fun send(packet: ByteArray) + + /** + * close + */ + public fun destroy() +} + +internal fun createSocket(): Socket1 = Socket1() + +public fun htons(value: UShort): UShort = + (((value.toInt() and 0xFF) shl 8) or ((value.toInt() ushr 8) and 0xFF)).toUShort() + +public fun htonl(value: UInt): UInt = + ((value and 0xFFu) shl 24) or + ((value and 0xFF00u) shl 8) or + ((value and 0xFF0000u) shr 8) or + ((value and 0xFF000000u) shr 24) \ No newline at end of file diff --git a/src/jvmMain/kotlin/cn/rtast/nmdns/sleep1.jvm.kt b/src/jvmMain/kotlin/cn/rtast/nmdns/sleep1.jvm.kt index 404f63d..6253fd8 100644 --- a/src/jvmMain/kotlin/cn/rtast/nmdns/sleep1.jvm.kt +++ b/src/jvmMain/kotlin/cn/rtast/nmdns/sleep1.jvm.kt @@ -6,6 +6,6 @@ package cn.rtast.nmdns -actual fun sleep1(time: Int) { +public actual fun sleep1(time: Int) { Thread.sleep(time * 1000L) } \ No newline at end of file diff --git a/src/jvmMain/kotlin/cn/rtast/nmdns/socket.jvm.kt b/src/jvmMain/kotlin/cn/rtast/nmdns/socket.jvm.kt new file mode 100644 index 0000000..fd3dc7d --- /dev/null +++ b/src/jvmMain/kotlin/cn/rtast/nmdns/socket.jvm.kt @@ -0,0 +1,32 @@ +/* + * Copyright © 2025 RTAkland + * Author: RTAkland + * Date: 9/30/25 + */ + + +package cn.rtast.nmdns + +import java.net.DatagramPacket +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.MulticastSocket + +public actual class Socket1 internal actual constructor() { + private lateinit var socket: MulticastSocket + private val group: InetAddress = InetAddress.getByName("224.0.0.251") + + public actual fun bind(ip: String, port: Int): Socket1 { + socket = MulticastSocket(InetSocketAddress(ip, port)) + socket.joinGroup(group) + return this + } + + public actual fun send(packet: ByteArray) { + socket.send(DatagramPacket(packet, packet.size, group, 5353)) + } + + public actual fun destroy() { + socket.close() + } +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/test/JvmTest.kt b/src/jvmTest/kotlin/test/JvmTest.kt deleted file mode 100644 index 9bff286..0000000 --- a/src/jvmTest/kotlin/test/JvmTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright © 2025 RTAkland - * Author: RTAkland - * Date: 9/29/25 - */ - - -package test - -import cn.rtast.nmdns.broadcast -import cn.rtast.nmdns.randomMacAddress -import cn.rtast.nmdns.registerService -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import org.junit.Test - -class JvmTest { - @Test - fun `test on jvm`() { - runBlocking { -// val manager = SelectorManager(Dispatchers.Default) -// val serverSocket = aSocket(manager).udp() -// .bind(InetSocketAddress("192.168.10.104", 5356)) -// val txtRecords = listOf( -// "deviceid=01:23:45:67:89:AB", -// "model=AppleTV3,2C", -// "features=0x5A7FFFF7,0x1E", -// "srcvers=220.68", -// "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336" -// ) -// val packet = buildPacket( -// serviceType = "_airplay._tcp.local.", -// serviceName = "MyService._airplay._tcp.local.", -// hostname = "rtakland.local.", -// ip = "192.168.10.104", -// port = 8080, -// txtRecords = txtRecords -// ) -// while (true) { -// serverSocket.send(Datagram(packet.copy(), InetSocketAddress("224.0.0.251", 5353))) -// println("sent") -// delay(2000L) -// } - val service = registerService( - serviceType = "_airplay._tcp.local.", - serviceName = "AP@RTAST", - hostname = "rtakland.local.", - ipAddress = "192.168.10.104", - port = 7001, - bindAddress = "192.168.10.104", - mdnsPort = 5356, - txtRecords = listOf( - "deviceid=${randomMacAddress()}", - "model=AppleTV3,2C", - "features=0x5A7FFFF7,0x1E", - "srcvers=220.68", - "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336" - ) - ) - while (true) { - service.broadcast() - delay(2000L) - } - } - } -} \ No newline at end of file diff --git a/src/jvmTest/kotlin/test/TestJvm.kt b/src/jvmTest/kotlin/test/TestJvm.kt new file mode 100644 index 0000000..e650301 --- /dev/null +++ b/src/jvmTest/kotlin/test/TestJvm.kt @@ -0,0 +1,39 @@ +/* + * Copyright © 2025 RTAkland + * Author: RTAkland + * Date: 9/29/25 + */ + + +package test + +import cn.rtast.nmdns.randomMacAddress +import cn.rtast.nmdns.registerService +import cn.rtast.nmdns.sleep1 +import org.junit.Test + +class TestJvm { + @Test + fun `test on jvm`() { + val service = registerService( + serviceType = "_airplay._tcp.local.", + serviceName = "AP@RTAST", + hostname = "rtakland.local.", + ipAddress = "192.168.10.104", + port = 7001, + bindAddress = "192.168.10.104", + mdnsPort = 5356, + txtRecords = listOf( + "deviceid=${randomMacAddress()}", + "model=AppleTV3,2C", + "features=0x5A7FFFF7,0x1E", + "srcvers=220.68", + "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336" + ) + ) + while (true) { + service.broadcast() + sleep1(2) + } + } +} \ No newline at end of file diff --git a/src/linuxMain/kotlin/cn/rtast/nmdns/socket.linux.kt b/src/linuxMain/kotlin/cn/rtast/nmdns/socket.linux.kt new file mode 100644 index 0000000..f68adf8 --- /dev/null +++ b/src/linuxMain/kotlin/cn/rtast/nmdns/socket.linux.kt @@ -0,0 +1,68 @@ +/* + * Copyright © 2025 RTAkland + * Author: RTAkland + * Date: 9/30/25 + */ + +@file:OptIn(ExperimentalForeignApi::class) +@file:Suppress("unused") + +package cn.rtast.nmdns + +import kotlinx.cinterop.* +import platform.linux.inet_addr +import platform.posix.* + +public actual class Socket1 internal actual constructor() { + /** + * socket fd + */ + private var socket: Int = -1 + + public actual fun bind(ip: String, port: Int): Socket1 { + socket = socket(AF_INET, SOCK_DGRAM, 0) + if (socket < 0) perror("Failed to create udp socket") + memScoped { + val reuse = alloc() + reuse.value = 1 + if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, reuse.ptr, sizeOf().convert()) < 0) { + perror("Failed to setsocketopt SO_REUSEADDR") + } + } + memScoped { + val addr = alloc() + addr.sin_family = AF_INET.convert() + addr.sin_port = htons(port.toUShort()) + addr.sin_addr.s_addr = inet_addr(ip) + if (bind(socket, addr.ptr.reinterpret(), sizeOf().convert()) < 0) { + perror("Failed to bind") + close(socket) + } + } + memScoped { + val mreq = alloc() + mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.251") + mreq.imr_interface.s_addr = htonl(INADDR_ANY) + if (setsockopt(socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, mreq.ptr, sizeOf().convert()) < 0) { + perror("setsockopt IP_ADD_MEMBERSHIP") + } + } + + return this + } + + public actual fun send(packet: ByteArray) { + memScoped { + val addr = alloc() + addr.sin_family = AF_INET.convert() + addr.sin_port = htons(5353u) + addr.sin_addr.s_addr = inet_addr("224.0.0.251") + val cbuf = packet.refTo(0).getPointer(this) + sendto(socket, cbuf, packet.size.convert(), 0, addr.ptr.reinterpret(), sizeOf().convert()) + } + } + + public actual fun destroy() { + close(socket) + } +} \ No newline at end of file diff --git a/src/linuxTest/kotlin/test/LinuxTest.kt b/src/linuxTest/kotlin/test/LinuxTest.kt deleted file mode 100644 index a65381d..0000000 --- a/src/linuxTest/kotlin/test/LinuxTest.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* -* Copyright © 2025 RTAkland -* Author: RTAkland -* Date: 9/29/25 -*/ - - -package test - -import cn.rtast.nmdns.broadcast -import cn.rtast.nmdns.randomMacAddress -import cn.rtast.nmdns.registerService -import cn.rtast.nmdns.sleep1 -import kotlinx.coroutines.runBlocking -import kotlin.test.Test - -class LinuxTest { - @Test - fun `test on linux`() { - runBlocking { - val service = registerService( - serviceType = "_airplay._tcp.local.", - serviceName = "AP@RTAST", - hostname = "rtakland.local.", - ipAddress = "192.168.10.104", - port = 7001, - bindAddress = "192.168.10.104", - mdnsPort = 5356, - txtRecords = listOf( - "deviceid=${randomMacAddress()}", - "model=AppleTV3,2C", - "features=0x5A7FFFF7,0x1E", - "srcvers=220.68", - "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336" - ) - ) - while (true) { - service.broadcast() - sleep1(2) - } - } - } -} \ No newline at end of file diff --git a/src/linuxTest/kotlin/test/TestLinux.kt b/src/linuxTest/kotlin/test/TestLinux.kt new file mode 100644 index 0000000..40032f9 --- /dev/null +++ b/src/linuxTest/kotlin/test/TestLinux.kt @@ -0,0 +1,39 @@ +/* +* Copyright © 2025 RTAkland +* Author: RTAkland +* Date: 9/29/25 +*/ + + +package test + +import cn.rtast.nmdns.randomMacAddress +import cn.rtast.nmdns.registerService +import cn.rtast.nmdns.sleep1 +import kotlin.test.Test + +class TestLinux { + @Test + fun `test on linux`() { + val service = registerService( + serviceType = "_airplay._tcp.local.", + serviceName = "AP@RTAST", + hostname = "rtakland.local.", + ipAddress = "192.168.10.104", + port = 7001, + bindAddress = "192.168.10.104", + mdnsPort = 5356, + txtRecords = listOf( + "deviceid=${randomMacAddress()}", + "model=AppleTV3,2C", + "features=0x5A7FFFF7,0x1E", + "srcvers=220.68", + "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336" + ) + ) + while (true) { + service.broadcast() + sleep1(2) + } + } +} \ No newline at end of file diff --git a/src/linuxTest/kotlin/test/TestMacAddr.kt b/src/linuxTest/kotlin/test/TestMacAddr.kt new file mode 100644 index 0000000..db82df6 --- /dev/null +++ b/src/linuxTest/kotlin/test/TestMacAddr.kt @@ -0,0 +1,19 @@ +/* + * Copyright © 2025 RTAkland + * Author: RTAkland + * Date: 9/30/25 + */ + + +package test + +import cn.rtast.nmdns.randomMacAddress +import kotlin.test.Test + +class TestMacAddr { + + @Test + fun `test generate mac address`() { + println(randomMacAddress()) + } +} \ No newline at end of file diff --git a/src/linuxTest/kotlin/test/TestNative.kt b/src/linuxTest/kotlin/test/TestNative.kt new file mode 100644 index 0000000..2264e55 --- /dev/null +++ b/src/linuxTest/kotlin/test/TestNative.kt @@ -0,0 +1,42 @@ +/* + * Copyright © 2025 RTAkland + * Author: RTAkland + * Date: 9/30/25 + */ + + +package test + +import cn.rtast.nmdns.buildPacket +import cn.rtast.nmdns.createSocket +import cn.rtast.nmdns.randomMacAddress +import cn.rtast.nmdns.sleep1 +import kotlin.test.Test + +class TestNative { + + @Test + fun `test native socket`() { + val packet = buildPacket( + serviceType = "_airplay._tcp.local.", + serviceName = "AP@RTAST._airplay._tcp.local.", + hostname = "rtakland.local.", + ip = "192.168.10.104", + port = 7001, + txtRecords = listOf( + "deviceid=${randomMacAddress()}", + "model=AppleTV3,2C", + "features=0x5A7FFFF7,0x1E", + "srcvers=220.68", + "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336" + ) + ) + + val service = createSocket() + .bind("192.168.10.104", 4546) + while (true) { + service.send(packet.copyOf()) + sleep1(2) + } + } +} \ No newline at end of file diff --git a/src/mingwMain/kotlin/cn/rtast/nmdns/socket.mingw.kt b/src/mingwMain/kotlin/cn/rtast/nmdns/socket.mingw.kt new file mode 100644 index 0000000..5b038c1 --- /dev/null +++ b/src/mingwMain/kotlin/cn/rtast/nmdns/socket.mingw.kt @@ -0,0 +1,89 @@ +/* + * Copyright © 2025 RTAkland + * Author: RTAkland + * Date: 9/30/25 + */ + +@file:OptIn(ExperimentalForeignApi::class) +@file:Suppress("unused") + +package cn.rtast.nmdns + +import kotlinx.cinterop.* +import platform.posix.* +import platform.windows.IPPROTO_UDP +import platform.windows.SOCK_DGRAM +import platform.windows.WSAStartup +import platform.windows.inet_addr +import platform.windows.setsockopt + +public actual class Socket1 internal actual constructor() { + + /** + * socket fd in windows + */ + private var sock: SOCKET = INVALID_SOCKET + private var bound: Boolean = false + + public actual fun bind(ip: String, port: Int): Socket1 = memScoped { + val wsaData = nativeHeap.alloc() + if (WSAStartup(0x202u, wsaData.ptr) != 0) { + perror("Failed to create socket") + } + sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) + if (sock == INVALID_SOCKET) { + perror("Failed to create socket") + WSACleanup() + } + + val reuse = nativeHeap.alloc() + reuse.value = 1 + val result = setsockopt( + sock, + SOL_SOCKET, + SO_REUSEADDR, + reuse.ptr.reinterpret().toKString(), + sizeOf().convert() + ) + if (result != 0) println("setsockopt failed") + + val addr = alloc() + addr.sin_family = AF_INET.convert() + addr.sin_port = htons(port.toUShort()) + addr.sin_addr.S_un.S_addr = inet_addr(ip) + + if (bind(sock, addr.ptr.reinterpret(), sizeOf().convert()) == SOCKET_ERROR) { + println("Bind failed") + closesocket(sock) + WSACleanup() + } + + bound = true + this@Socket1 + } + + public actual fun send(packet: ByteArray): Unit = memScoped { + if (!bound) throw IllegalStateException("Socket not bound") + val dest = alloc() + dest.sin_family = AF_INET.convert() + dest.sin_port = 5353u + dest.sin_addr.S_un.S_addr = inet_addr("224.0.0.251") + packet.usePinned { pinned -> + val sent = sendto( + sock, pinned.addressOf(0), + packet.size.convert(), + 0, dest.ptr.reinterpret(), + sizeOf().convert() + ) + if (sent == SOCKET_ERROR) perror("Send failed") + } + } + + public actual fun destroy() { + if (sock != INVALID_SOCKET) { + closesocket(sock) + sock = INVALID_SOCKET + } + WSACleanup() + } +} \ No newline at end of file diff --git a/src/mingwTest/kotlin/test/TestWindows.kt b/src/mingwTest/kotlin/test/TestWindows.kt new file mode 100644 index 0000000..501d06e --- /dev/null +++ b/src/mingwTest/kotlin/test/TestWindows.kt @@ -0,0 +1,39 @@ +/* + * Copyright © 2025 RTAkland + * Author: RTAkland + * Date: 9/30/25 + */ + + +package test + +import cn.rtast.nmdns.randomMacAddress +import cn.rtast.nmdns.registerService +import cn.rtast.nmdns.sleep1 +import kotlin.test.Test + +class TestWindows { + @Test + fun `test on windows`() { + val service = registerService( + serviceType = "_airplay._tcp.local.", + serviceName = "AP@RTAST", + hostname = "rtakland.local.", + ipAddress = "192.168.10.104", + port = 7001, + bindAddress = "192.168.10.104", + mdnsPort = 5356, + txtRecords = listOf( + "deviceid=${randomMacAddress()}", + "model=AppleTV3,2C", + "features=0x5A7FFFF7,0x1E", + "srcvers=220.68", + "pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336" + ) + ) + while (true) { + service.broadcast() + sleep1(2) + } + } +} \ No newline at end of file diff --git a/src/nativeMain/kotlin/cn/rtast/nmdns/list.kt b/src/nativeMain/kotlin/cn/rtast/nmdns/list.kt new file mode 100644 index 0000000..cbbdf57 --- /dev/null +++ b/src/nativeMain/kotlin/cn/rtast/nmdns/list.kt @@ -0,0 +1,28 @@ +/* + * Copyright © 2025 RTAkland + * Author: RTAkland + * Date: 9/30/25 + */ + + +@file:OptIn(ExperimentalNativeApi::class) + +package cn.rtast.nmdns + +import kotlin.experimental.ExperimentalNativeApi + +/** + * create an empty list in c + */ +@CExport +@CName("create_txt_records") +public fun createTXTRecords(): List = listOf() + +/** + * add txt record to list + */ +@CExport +@CName("add_txt_record") +public fun addTxtRecord(list: List, value: String) { + list.toMutableList().add(value) +} \ No newline at end of file diff --git a/src/nativeMain/kotlin/cn/rtast/nmdns/sleep1.native.kt b/src/nativeMain/kotlin/cn/rtast/nmdns/sleep1.native.kt index 3773e61..27057da 100644 --- a/src/nativeMain/kotlin/cn/rtast/nmdns/sleep1.native.kt +++ b/src/nativeMain/kotlin/cn/rtast/nmdns/sleep1.native.kt @@ -7,7 +7,10 @@ package cn.rtast.nmdns import platform.posix.sleep +import kotlin.experimental.ExperimentalNativeApi -actual fun sleep1(time: Int) { +@OptIn(ExperimentalNativeApi::class) +@CName("sleep1") +public actual fun sleep1(time: Int) { sleep(time.toUInt()) } \ No newline at end of file