Initial commit
This commit is contained in:
15 files changed
+750
No files matched your search
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright © 2025 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 9/29/25
|
||||
*/
|
||||
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
fun randomMacAddress(): String = macAddresses.random()
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright © 2025 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 9/28/25
|
||||
*/
|
||||
|
||||
|
||||
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
|
||||
|
||||
data class NMDNSServer(
|
||||
/**
|
||||
* coroutine dispatcher for running server
|
||||
*/
|
||||
val dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
/**
|
||||
* service type
|
||||
* default airplay
|
||||
*/
|
||||
val serviceType: String = "_airplay._tcp.local.",
|
||||
/**
|
||||
* service name
|
||||
* finally: $serviceName.$serviceType
|
||||
*/
|
||||
val serviceName: String = "APS",
|
||||
/**
|
||||
* local hostname
|
||||
*/
|
||||
val hostname: String,
|
||||
/**
|
||||
* service ip, lan ip mostly
|
||||
*/
|
||||
val ipAddress: String,
|
||||
/**
|
||||
* service port
|
||||
*/
|
||||
val port: Int = 7004,
|
||||
/**
|
||||
* mdns server port
|
||||
*/
|
||||
val mdnsPort: Int = 3025,
|
||||
/**
|
||||
* Txt record content
|
||||
*/
|
||||
val txtRecords: List<String> = listOf(
|
||||
"deviceid=01:23:45:67:89:AB",
|
||||
"model=AppleTV3,2C",
|
||||
"features=0x5A7FFFF7,0x1E",
|
||||
"srcvers=220.68",
|
||||
"pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"
|
||||
),
|
||||
/**
|
||||
* mdns socket server
|
||||
*/
|
||||
val server: BoundDatagramSocket,
|
||||
/**
|
||||
* srv packet
|
||||
*/
|
||||
val packet: Buffer,
|
||||
)
|
||||
|
||||
suspend fun createService(
|
||||
serviceType: String,
|
||||
serviceName: String,
|
||||
hostname: String,
|
||||
ipAddress: String,
|
||||
port: Int,
|
||||
mdnsPort: Int = 9872,
|
||||
bindAddress: String,
|
||||
txtRecords: List<String> = listOf(
|
||||
"deviceid=${randomMacAddress()}",
|
||||
"model=AppleTV3,2C",
|
||||
"features=0x5A7FFFF7,0x1E",
|
||||
"srcvers=220.68",
|
||||
"pk=f3769a660475d27b4f6040381d784645e13e21c53e6d2da6a8c3d757086fc336"
|
||||
),
|
||||
): NMDNSServer {
|
||||
val manager = SelectorManager(Dispatchers.Default)
|
||||
val serverSocket = aSocket(manager).udp()
|
||||
.bind(InetSocketAddress(bindAddress, mdnsPort))
|
||||
return NMDNSServer(
|
||||
Dispatchers.Default, serviceType, serviceName,
|
||||
hostname, ipAddress, port, mdnsPort,
|
||||
txtRecords, serverSocket, buildPacket(serviceType, "$serviceName.$serviceType", hostname, ipAddress, port, txtRecords)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun NMDNSServer.broadcast() {
|
||||
this.server.send(Datagram(packet.copy(), InetSocketAddress("224.0.0.251", 5353)))
|
||||
}
|
||||
|
||||
internal fun buildPacket(
|
||||
serviceType: String,
|
||||
serviceName: String,
|
||||
hostname: String,
|
||||
ip: String,
|
||||
port: Int,
|
||||
txtRecords: List<String>,
|
||||
): Buffer {
|
||||
val records = mutableListOf<ByteArray>()
|
||||
records += encodeRR(serviceType, 12, encodeName(serviceName))
|
||||
|
||||
val srvData = ByteArray(6) { 0 }
|
||||
srvData[4] = (port shr 8).toByte()
|
||||
srvData[5] = (port and 0xFF).toByte()
|
||||
records += encodeRR(serviceName, 33, srvData + encodeName(hostname))
|
||||
|
||||
val ipParts = ip.split(".").map { it.toInt().toByte() }
|
||||
records += encodeRR(hostname, 1, ipParts.toByteArray())
|
||||
|
||||
if (txtRecords.isNotEmpty()) {
|
||||
val txtData = encodeTxt(txtRecords)
|
||||
records += encodeRR(serviceName, 16, txtData)
|
||||
}
|
||||
|
||||
val header = ByteArray(12)
|
||||
header[2] = 0x84.toByte()
|
||||
header[3] = 0x00
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
internal fun encodeRR(name: String, type: Int, data: ByteArray): ByteArray {
|
||||
val nameBytes = encodeName(name)
|
||||
val rr = ByteArray(10)
|
||||
rr[0] = 0x00; rr[1] = type.toByte()
|
||||
rr[2] = 0x00; rr[3] = 0x01
|
||||
rr[4] = 0x00; rr[5] = 0x00; rr[6] = 0x00; rr[7] = 0x78
|
||||
rr[8] = (data.size shr 8).toByte()
|
||||
rr[9] = (data.size and 0xFF).toByte()
|
||||
return nameBytes + rr + data
|
||||
}
|
||||
|
||||
internal fun encodeName(name: String): ByteArray {
|
||||
val parts = name.trimEnd('.').split(".")
|
||||
val bytes = mutableListOf<Byte>()
|
||||
for (p in parts) {
|
||||
bytes += p.length.toByte()
|
||||
bytes += p.encodeToByteArray().toList()
|
||||
}
|
||||
bytes += 0
|
||||
return bytes.toByteArray()
|
||||
}
|
||||
|
||||
internal fun encodeTxt(records: List<String>): ByteArray {
|
||||
val bytes = mutableListOf<Byte>()
|
||||
for (txt in records) {
|
||||
val txtBytes = txt.encodeToByteArray()
|
||||
if (txtBytes.size > 255) continue
|
||||
bytes += txtBytes.size.toByte()
|
||||
bytes += txtBytes.toList()
|
||||
}
|
||||
return bytes.toByteArray()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* Copyright © 2025 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 9/29/25
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.nmdns
|
||||
|
||||
internal expect fun sleep1(time: Int)
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright © 2025 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 9/29/25
|
||||
*/
|
||||
|
||||
package cn.rtast.nmdns
|
||||
|
||||
actual fun sleep1(time: Int) {
|
||||
Thread.sleep(time * 1000L)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright © 2025 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 9/29/25
|
||||
*/
|
||||
|
||||
|
||||
package test
|
||||
|
||||
import cn.rtast.nmdns.broadcast
|
||||
import cn.rtast.nmdns.createService
|
||||
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 = createService(
|
||||
serviceType = "_airplay._tcp.local.",
|
||||
serviceName = "AP@RTAST",
|
||||
hostname = "rtakland.local.",
|
||||
ipAddress = "192.168.10.104",
|
||||
port = 7001,
|
||||
bindAddress = "192.168.10.104",
|
||||
mdnsPort = 5356
|
||||
)
|
||||
while (true) {
|
||||
service.broadcast()
|
||||
delay(2000L)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright © 2025 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 9/29/25
|
||||
*/
|
||||
|
||||
|
||||
package test
|
||||
|
||||
import cn.rtast.nmdns.broadcast
|
||||
import cn.rtast.nmdns.createService
|
||||
import cn.rtast.nmdns.sleep1
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
|
||||
class LinuxTest {
|
||||
@Test
|
||||
fun `test on linux`() {
|
||||
runBlocking {
|
||||
val service = createService(
|
||||
serviceType = "_airplay._tcp.local.",
|
||||
serviceName = "AP@RTAST",
|
||||
hostname = "rtakland.local.",
|
||||
ipAddress = "192.168.10.104",
|
||||
port = 7001,
|
||||
bindAddress = "192.168.10.104",
|
||||
mdnsPort = 5356
|
||||
)
|
||||
while (true) {
|
||||
service.broadcast()
|
||||
sleep1(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright © 2025 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 9/29/25
|
||||
*/
|
||||
|
||||
package cn.rtast.nmdns
|
||||
|
||||
import platform.posix.sleep
|
||||
|
||||
actual fun sleep1(time: Int) {
|
||||
sleep(time.toUInt())
|
||||
}
|
||||
Reference in New Issue
Block a user