diff --git a/.gitea/workflows/main.yaml b/.gitea/workflows/main.yaml deleted file mode 100644 index 04b02a7..0000000 --- a/.gitea/workflows/main.yaml +++ /dev/null @@ -1,43 +0,0 @@ -on: - push: - branches: - - main - -env: - INFENDRO__TOKEN: ${{ secrets.TOKEN }} - -jobs: - release: - runs-on: linux - steps: - - uses: actions/checkout@v4 - - - uses: actions/cache/restore@v4 - with: - key: gradle - path: | - ~/.gradle/caches/ - ~/.gradle/wrapper/ - ~/.konan/ - - - uses: actions/setup-java@v4 - with: - java-version: '24' - distribution: 'temurin' - - - run: | - ./gradlew assemble - mv ./build/bin/linuxX64/releaseExecutable/ddns.kexe ./ddns - - - uses: actions/upload-artifact@v3 - with: - name: ddns - path: ./ddns - - - uses: actions/cache/save@v4 - with: - key: gradle - path: | - ~/.gradle/caches/ - ~/.gradle/wrapper/ - ~/.konan/ diff --git a/README.md b/README.md index 773daf2..f1404c6 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The resulting binary will be located in `./build/bin/releaseExecutable/`. The following command updates all A and AAAA DNS records in Porkbun for `example.com` and `test.example.com`. ``` -ddns porkbun \ +ddns porkbun update \ --key= \ --secret-key= \ --domain=example.com \ @@ -27,12 +27,18 @@ Alternatively, a file can be used to declare one or more configurations. The above example can be declared as follows. ``` -porkbun -key= -secret-key= -domain=example.com -subdomain= -subdomain=test +[ + { + "type": "porkbun", + "key": "", + "secretKey": "" + "domain": "example.com", + "subdomains": [ + "", + "test" + ] + } +] ``` To use this config file, execute the command `ddns --config=`. diff --git a/build.gradle.kts b/build.gradle.kts index ea37003..d15ef94 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ import com.infendro.plugin.infendro group = "com.infendro" -version = "1.0.0" +version = "1.1.0" plugins { alias(libs.plugins.infendro) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 17c87d9..6d1cd66 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,11 @@ [versions] -infendro = "1.0.0" -kotlin = "2.2.21" -io = "0.8.2" +infendro = "1.1.0" +kotlin = "2.3.0" +io = "0.9.0" coroutines = "1.10.2" -serialization = "1.9.0" -cli = "1.3.0" -ktor = "3.3.3" +serialization = "1.10.0" +cli = "1.4.0" +ktor = "3.4.0" [plugins] infendro = { id = "com.infendro.plugin", version.ref = "infendro" } diff --git a/src/commonMain/kotlin/com/infendro/ddns/Main.kt b/src/commonMain/kotlin/com/infendro/ddns/Main.kt index 4407eb0..d7d2d3e 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/Main.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/Main.kt @@ -1,110 +1,5 @@ package com.infendro.ddns -import com.infendro.cli.argument.string -import com.infendro.cli.argument.stringOrElse -import com.infendro.cli.argument.strings -import com.infendro.cli.command.cli -import com.infendro.ddns.Argument.CONFIG -import com.infendro.ddns.Argument.DOMAIN -import com.infendro.ddns.Argument.IP_SERVICE -import com.infendro.ddns.Argument.KEY -import com.infendro.ddns.Argument.SECRET_KEY -import com.infendro.ddns.Argument.SUBDOMAIN -import com.infendro.ddns.argument.ipService -import com.infendro.ddns.config.Config -import com.infendro.ddns.dns.DnsService -import com.infendro.ddns.dns.DnsService.Companion.PORKBUN -import com.infendro.ddns.dns.model.DnsRecord -import com.infendro.ddns.dns.porkbun.Porkbun -import com.infendro.ddns.ip.IpService +import com.infendro.ddns.command.ddns -private object Argument { - val CONFIG = stringOrElse("config", "/etc/ddns.conf") - - val DOMAIN = string("domain") - val SUBDOMAIN = strings("subdomain", min = 1) - val IP_SERVICE = ipService("ip-service") - - // Porkbun - val KEY = string("key") - val SECRET_KEY = string("secret-key") -} - -fun main( - args: Array, -) { - cli { - arguments(CONFIG) - - execute { - val config by CONFIG - - Config.get(config) - .run { - val domain by DOMAIN - val subdomains by SUBDOMAIN - val ipService by IP_SERVICE - - val dnsService = when (name) { - PORKBUN -> { - val key by KEY - val secretkey by SECRET_KEY - - Porkbun(key, secretkey) - } - - else -> throw Exception() - } - - dnsService.update(ipService, domain, subdomains) - } - } - - command(PORKBUN) { - arguments( - DOMAIN, - SUBDOMAIN, - IP_SERVICE, - KEY, - SECRET_KEY, - ) - - execute { - val domain by DOMAIN - val subdomains by SUBDOMAIN - val ipService by IP_SERVICE - - val key by KEY - val secretkey by SECRET_KEY - val dnsService = Porkbun(key, secretkey) - - dnsService.update(ipService, domain, subdomains) - } - } - }.run(args) -} - -fun Config.run( - block: Config.Service.() -> Unit, -) { - for (service in services) { - service.block() - } -} - -fun DnsService.update( - ipService: IpService, - domain: String, - subdomains: List, -) { - val (ipv4, ipv6) = ipService.ip - - val records = getRecords(domain, subdomains) - val aRecords = records - .filter { it.type == DnsRecord.Type.A } - val aaaaRecords = records - .filter { it.type == DnsRecord.Type.AAAA } - - updateRecords(aRecords, ipv4) - updateRecords(aaaaRecords, ipv6) -} +fun main(args: Array) = ddns.run(*args) diff --git a/src/commonMain/kotlin/com/infendro/ddns/argument/IpService.kt b/src/commonMain/kotlin/com/infendro/ddns/argument/IpService.kt deleted file mode 100644 index c0ccf8b..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/argument/IpService.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.infendro.ddns.argument - -import com.infendro.cli.argument.Argument -import com.infendro.cli.command.Command -import com.infendro.ddns.ip.IpService -import com.infendro.ddns.ip.ipify.Ipify - -object IpServiceParser : Argument.Parser { - override fun parse( - text: String?, - ): IpService { - return when (text) { - IpService.IPIFY -> Ipify - else -> throw Exception() - } - } -} - -class IpServiceOrDefaultArgument( - override val name: String, -) : Argument.SingleOrElse() { - override val other: IpService - get() = IpService.DEFAULT - - override val parser: Parser - get() = IpServiceParser -} - -fun ipService( - name: String, -) = IpServiceOrDefaultArgument(name) - -fun Command.Builder.ipService( - name: String, -) = IpServiceOrDefaultArgument(name) - .also { argument(it) } diff --git a/src/commonMain/kotlin/com/infendro/ddns/command/Parameters.kt b/src/commonMain/kotlin/com/infendro/ddns/command/Parameters.kt new file mode 100644 index 0000000..0f45952 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/ddns/command/Parameters.kt @@ -0,0 +1,12 @@ +package com.infendro.ddns.command + +import com.infendro.cli.command.argument.argument +import com.infendro.cli.command.option.option +import com.infendro.ddns.ip.ipify.Ipify +import com.infendro.ddns.parameter.ipService + +internal object Parameters { + val DOMAIN = argument.string("domain") + val SUBDOMAINS = argument.string("subdomains").variable(min = 1) + val IP_SERVICE = option.ipService("ip-service").orElse(Ipify) +} diff --git a/src/commonMain/kotlin/com/infendro/ddns/command/ddns.kt b/src/commonMain/kotlin/com/infendro/ddns/command/ddns.kt new file mode 100644 index 0000000..f13da0e --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/ddns/command/ddns.kt @@ -0,0 +1,9 @@ +package com.infendro.ddns.command + +import com.infendro.cli.command.command + +val ddns = command("ddns") { + execute { help() } + +update + +porkbun +} diff --git a/src/commonMain/kotlin/com/infendro/ddns/command/porkbun.kt b/src/commonMain/kotlin/com/infendro/ddns/command/porkbun.kt new file mode 100644 index 0000000..1f39bb4 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/ddns/command/porkbun.kt @@ -0,0 +1,29 @@ +package com.infendro.ddns.command + +import com.infendro.cli.command.command +import com.infendro.cli.command.option.option +import com.infendro.ddns.command.Parameters.DOMAIN +import com.infendro.ddns.command.Parameters.IP_SERVICE +import com.infendro.ddns.command.Parameters.SUBDOMAINS +import com.infendro.ddns.dns.porkbun.Porkbun + +val porkbun = command("porkbun") { + execute { help() } + + +command("update") { + val key = +option.string("key") + val secretKey = +option.string("secret-key") + register(DOMAIN, SUBDOMAINS) + register(IP_SERVICE) + + execute { + val key by key + val secretKey by secretKey + val domain by DOMAIN + val subdomains by SUBDOMAINS + val ipService by IP_SERVICE + + Porkbun(key, secretKey).update(domain, subdomains, ipService) + } + } +} diff --git a/src/commonMain/kotlin/com/infendro/ddns/command/update.kt b/src/commonMain/kotlin/com/infendro/ddns/command/update.kt new file mode 100644 index 0000000..d6707cd --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/ddns/command/update.kt @@ -0,0 +1,35 @@ +package com.infendro.ddns.command + +import com.infendro.cli.command.command +import com.infendro.cli.command.option.option +import com.infendro.ddns.config.Config +import com.infendro.ddns.ip.ipify.Ipify +import com.infendro.ddns.parameter.path +import kotlinx.io.buffered +import kotlinx.io.files.Path +import kotlinx.io.files.SystemFileSystem +import kotlinx.io.readString +import kotlinx.serialization.json.Json + +val update = command("update") { + val config = +option.path("config").orElse(Path("/etc/ddns.conf")) + + execute { + val config by config + + val source = SystemFileSystem.source(config).buffered() + val text = source.readString() + + val entries: List = Json.decodeFromString(text) + update(entries) + } +} + +private fun update(entries: List) { + for (entry in entries) { + val domain = entry.domain + val subdomains = entry.subdomains + val ipService = entry.ipService ?: Ipify + entry.dnsService.update(domain, subdomains, ipService) + } +} diff --git a/src/commonMain/kotlin/com/infendro/ddns/config/Config.kt b/src/commonMain/kotlin/com/infendro/ddns/config/Config.kt index bab4f17..2f5b683 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/config/Config.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/config/Config.kt @@ -1,128 +1,29 @@ package com.infendro.ddns.config -import com.infendro.cli.argument.Argument.* -import com.infendro.ddns.config.Regex.ARGUMENT -import com.infendro.ddns.config.Regex.EMPTY_LINES -import kotlin.reflect.KProperty -import kotlinx.io.buffered -import kotlinx.io.files.Path -import kotlinx.io.files.SystemFileSystem -import kotlinx.io.readString +import com.infendro.ddns.dns.DnsService +import com.infendro.ddns.ip.IpService +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable -class Config( - val services: List, -) { - class Service( - val name: String, - val arguments: List, - ) { - class Argument( - val name: String, - val value: String, - ) +interface Config { + @Serializable + sealed class Entry { + abstract val domain: String + abstract val subdomains: List + abstract val ipService: IpService? + abstract val dnsService: DnsService - operator fun Single.getValue( - thisRef: Any?, - property: KProperty<*>, - ): T { - return parser.parse( - arguments - .find { it.name == name }!! - .value - ) - } - - operator fun SingleOrElse.getValue( - thisRef: Any?, - property: KProperty<*>, - ): T { - return arguments - .find { it.name == name } - ?.let { parser.parse(it.value) } - ?: other - } - - operator fun SingleOrNull.getValue( - thisRef: Any?, - property: KProperty<*>, - ): T? { - return arguments - .find { it.name == name } - ?.let { parser.parse(it.value) } - } - - operator fun Multiple.getValue( - thisRef: Any?, - property: KProperty<*>, - ): List { - return arguments - .filter { it.name == name } - .map { parser.parse(it.value) } - } - } - - companion object { - fun get( - path: String, - ): Config { - val path = Path(path) - val source = SystemFileSystem.source(path).buffered() - val text = source - .readString() - .process() - - return parse(text) - } - - private fun String.process(): String { - val builder = StringBuilder() - for (line in lines()) { - val comment = line.indexOf("""//""") - if (comment != -1) { - builder.appendLine( - line - .slice(0.., + @Serializable(with = IpServiceSerializer::class) + override val ipService: IpService? = null, + ) : Entry() { + override val dnsService: DnsService = com.infendro.ddns.dns.porkbun.Porkbun(key, secretKey) } } } diff --git a/src/commonMain/kotlin/com/infendro/ddns/config/IpServiceSerializer.kt b/src/commonMain/kotlin/com/infendro/ddns/config/IpServiceSerializer.kt new file mode 100644 index 0000000..648aa0a --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/ddns/config/IpServiceSerializer.kt @@ -0,0 +1,25 @@ +package com.infendro.ddns.config + +import com.infendro.ddns.ip.IpService +import com.infendro.ddns.ip.ipify.Ipify +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +object IpServiceSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("IpService", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: IpService) = throw NotImplementedError() + + override fun deserialize(decoder: Decoder): IpService { + val value = decoder.decodeString() + return when (value) { + "ipify" -> Ipify + else -> throw Exception() + } + } +} diff --git a/src/commonMain/kotlin/com/infendro/ddns/config/Regex.kt b/src/commonMain/kotlin/com/infendro/ddns/config/Regex.kt deleted file mode 100644 index f82045a..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/config/Regex.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.infendro.ddns.config - -object Regex { - val EMPTY_LINES = Regex("""\n{2,}""") - val ARGUMENT = Regex("""(?[a-z]+(?:-[a-z]+)*)(?:=(?.*))?""") -} diff --git a/src/commonMain/kotlin/com/infendro/ddns/dns/DnsService.kt b/src/commonMain/kotlin/com/infendro/ddns/dns/DnsService.kt index 6b716a5..bab5e8a 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/dns/DnsService.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/dns/DnsService.kt @@ -1,29 +1,30 @@ package com.infendro.ddns.dns import com.infendro.ddns.dns.model.DnsRecord +import com.infendro.ddns.ip.IpService interface DnsService { - fun getRecords( - domain: String, - subdomains: List, - ): List + fun getRecords(domain: String, subdomains: List): List - fun updateRecord( - record: DnsRecord, - value: String, - ) + fun updateRecord(record: DnsRecord, value: String) - fun updateRecords( - records: List, - value: String, - ) { + fun updateRecords(records: List, value: String) { for (record in records) { if (record.value == value) continue updateRecord(record, value) } } - companion object { - const val PORKBUN = "porkbun" + fun update(domain: String, subdomains: List, ipService: IpService) { + val (ipv4, ipv6) = ipService.ip + + val records = getRecords(domain, subdomains) + val aRecords = records + .filter { it.type == DnsRecord.Type.A } + val aaaaRecords = records + .filter { it.type == DnsRecord.Type.AAAA } + + updateRecords(aRecords, ipv4) + updateRecords(aaaaRecords, ipv6) } } diff --git a/src/commonMain/kotlin/com/infendro/ddns/dns/model/DnsRecord.kt b/src/commonMain/kotlin/com/infendro/ddns/dns/model/DnsRecord.kt index 25a17f6..adbd74e 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/dns/model/DnsRecord.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/dns/model/DnsRecord.kt @@ -13,10 +13,9 @@ data class DnsRecord( val domain: String, ) { val full: String - get() = if (subdomain != null) { - "$subdomain.$domain" - } else { - domain + get() = when { + subdomain != null -> "$subdomain.$domain" + else -> domain } } diff --git a/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/Porkbun.kt b/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/Porkbun.kt index c1a4579..bffa772 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/Porkbun.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/Porkbun.kt @@ -15,10 +15,7 @@ class Porkbun( val key: String, val secretkey: String, ) : DnsService { - override fun getRecords( - domain: String, - subdomains: List, - ): List = runBlocking { + override fun getRecords(domain: String, subdomains: List): List = runBlocking { val response = http.post("https://api.porkbun.com/api/json/v3/dns/retrieve/$domain") { val request = GetRecordsRequest( apikey = key, @@ -50,10 +47,7 @@ class Porkbun( .filter { it.type in DnsRecord.Type.address && (it.domain.subdomain ?: "") in subdomains } } - override fun updateRecord( - record: DnsRecord, - value: String, - ): Unit = runBlocking { + override fun updateRecord(record: DnsRecord, value: String): Unit = runBlocking { http.post("https://api.porkbun.com/api/json/v3/dns/edit/${record.domain.domain}/${record.id}") { val request = UpdateRecordRequest( apikey = key, diff --git a/src/commonMain/kotlin/com/infendro/ddns/ip/IpService.kt b/src/commonMain/kotlin/com/infendro/ddns/ip/IpService.kt index 8e21661..7098ac1 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/ip/IpService.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/ip/IpService.kt @@ -1,16 +1,9 @@ package com.infendro.ddns.ip -import com.infendro.ddns.ip.ipify.Ipify - interface IpService { val ipv4: String val ipv6: String val ip: Pair get() = Pair(ipv4, ipv6) - - companion object { - const val IPIFY = "ipify" - val DEFAULT = Ipify - } } diff --git a/src/commonMain/kotlin/com/infendro/ddns/ip/ipify/Ipify.kt b/src/commonMain/kotlin/com/infendro/ddns/ip/ipify/Ipify.kt index 20d83cf..7619db1 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/ip/ipify/Ipify.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/ip/ipify/Ipify.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.runBlocking object Ipify : IpService { override val ipv4: String get() = runBlocking { - val response = http.get("https://api.ipify.org?format=json") + val response = http.get("https://api4.ipify.org?format=json") val body = response.body() body.ip } diff --git a/src/commonMain/kotlin/com/infendro/ddns/parameter/IpService.kt b/src/commonMain/kotlin/com/infendro/ddns/parameter/IpService.kt new file mode 100644 index 0000000..0e4be88 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/ddns/parameter/IpService.kt @@ -0,0 +1,25 @@ +package com.infendro.ddns.parameter + +import com.infendro.cli.command.argument.ArgumentFactory +import com.infendro.cli.command.argument.argument +import com.infendro.cli.command.option.OptionFactory +import com.infendro.cli.command.option.option +import com.infendro.cli.parser.Parser +import com.infendro.ddns.ip.IpService +import com.infendro.ddns.ip.ipify.Ipify + +object IpServiceParser : Parser(IpService::class) { + override fun parse(text: String): Result { + val service = when (text) { + "ipify" -> Ipify + else -> return failure(text) + } + return success(service) + } +} + +fun ArgumentFactory.ipService(name: String, description: String? = null) = + argument(IpServiceParser, name, description) + +fun OptionFactory.ipService(vararg names: String, description: String? = null) = + option(IpServiceParser, *names, description = description) diff --git a/src/commonMain/kotlin/com/infendro/ddns/parameter/Path.kt b/src/commonMain/kotlin/com/infendro/ddns/parameter/Path.kt new file mode 100644 index 0000000..29c5929 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/ddns/parameter/Path.kt @@ -0,0 +1,19 @@ +package com.infendro.ddns.parameter + +import com.infendro.cli.command.argument.ArgumentFactory +import com.infendro.cli.command.argument.argument +import com.infendro.cli.command.option.OptionFactory +import com.infendro.cli.command.option.option +import com.infendro.cli.parser.Parser +import kotlinx.io.files.Path + +object PathParser : Parser(Path::class) { + override fun parse(text: String) = + success(Path(text)) +} + +fun ArgumentFactory.path(name: String, description: String? = null) = + argument(PathParser, name, description) + +fun OptionFactory.path(vararg names: String, description: String? = null) = + option(PathParser, *names, description = description)