From cc37d6bcb037b5396ab1c4a47de7f293cb54448e Mon Sep 17 00:00:00 2001 From: Infendro Date: Sat, 13 Dec 2025 02:01:33 +0100 Subject: [PATCH 1/6] use common plugin --- .gitea/workflows/main.yaml | 2 +- build.gradle.kts | 13 ++++++++----- gradle/libs.versions.toml | 12 +++++++----- settings.gradle.kts | 5 +++++ src/commonMain/kotlin/com/infendro/ddns/Main.kt | 2 +- .../kotlin/com/infendro/ddns/argument/IpService.kt | 5 ++--- 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/.gitea/workflows/main.yaml b/.gitea/workflows/main.yaml index 648b3e2..04b02a7 100644 --- a/.gitea/workflows/main.yaml +++ b/.gitea/workflows/main.yaml @@ -4,7 +4,7 @@ on: - main env: - GIT__TOKEN: ${{ secrets.TOKEN }} + INFENDRO__TOKEN: ${{ secrets.TOKEN }} jobs: release: diff --git a/build.gradle.kts b/build.gradle.kts index 1d769cd..ea37003 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,10 @@ +import com.infendro.plugin.infendro + group = "com.infendro" version = "1.0.0" -repositories { - maven("https://git.infendro.com/api/packages/Infendro/maven") - mavenCentral() -} - plugins { + alias(libs.plugins.infendro) alias(libs.plugins.multiplatform) alias(libs.plugins.serialization) } @@ -18,6 +16,11 @@ kotlin { } } + repositories { + infendro() + mavenCentral() + } + sourceSets { commonMain.dependencies { implementation(libs.io) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index af8c527..17c87d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,14 @@ [versions] -kotlin = "2.1.21" -io = "0.7.0" +infendro = "1.0.0" +kotlin = "2.2.21" +io = "0.8.2" coroutines = "1.10.2" -serialization = "1.8.1" -cli = "1.2.2" -ktor = "3.1.2" +serialization = "1.9.0" +cli = "1.3.0" +ktor = "3.3.3" [plugins] +infendro = { id = "com.infendro.plugin", version.ref = "infendro" } multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index f231f76..ae9dc1b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,6 @@ rootProject.name = "ddns" + +pluginManagement.repositories { + maven("https://git.infendro.com/api/packages/Infendro/maven") + mavenCentral() +} diff --git a/src/commonMain/kotlin/com/infendro/ddns/Main.kt b/src/commonMain/kotlin/com/infendro/ddns/Main.kt index e92c772..0645597 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/Main.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/Main.kt @@ -3,7 +3,7 @@ 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.cli +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 diff --git a/src/commonMain/kotlin/com/infendro/ddns/argument/IpService.kt b/src/commonMain/kotlin/com/infendro/ddns/argument/IpService.kt index 6fbe3fe..f84d9e0 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/argument/IpService.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/argument/IpService.kt @@ -1,12 +1,11 @@ package com.infendro.ddns.argument -import com.infendro.cli.Command import com.infendro.cli.argument.Argument -import com.infendro.cli.argument.Parser +import com.infendro.cli.command.Command import com.infendro.ddns.ip.IpService import com.infendro.ddns.ip.IpServices -object IpServiceParser : Parser { +object IpServiceParser : Argument.Parser { override fun parse( text: String?, ): IpService { From 4a8244466eb3319797be77a67842931bd2837917 Mon Sep 17 00:00:00 2001 From: Infendro Date: Sun, 14 Dec 2025 23:03:10 +0100 Subject: [PATCH 2/6] refactor --- .../kotlin/com/infendro/ddns/Http.kt | 14 ++- .../kotlin/com/infendro/ddns/Main.kt | 2 +- .../com/infendro/ddns/argument/IpService.kt | 9 +- .../com/infendro/ddns/dns/DnsService.kt | 10 ++- .../com/infendro/ddns/dns/DnsServices.kt | 5 -- .../kotlin/com/infendro/ddns/dns/Regex.kt | 5 -- .../com/infendro/ddns/dns/porkbun/Porkbun.kt | 90 ++++++++----------- .../kotlin/com/infendro/ddns/ip/IpService.kt | 17 ++-- .../kotlin/com/infendro/ddns/ip/IpServices.kt | 17 ---- .../com/infendro/ddns/ip/ipify/Ipify.kt | 12 +-- 10 files changed, 74 insertions(+), 107 deletions(-) delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/dns/DnsServices.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/dns/Regex.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/ip/IpServices.kt diff --git a/src/commonMain/kotlin/com/infendro/ddns/Http.kt b/src/commonMain/kotlin/com/infendro/ddns/Http.kt index c2b03b2..503ffca 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/Http.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/Http.kt @@ -1,13 +1,11 @@ package com.infendro.ddns -import io.ktor.client.HttpClient -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.serialization.kotlinx.json.json +import io.ktor.client.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.serialization.kotlinx.json.* -object Http { - val client = HttpClient { - install(ContentNegotiation) { - json() - } +val http = HttpClient { + install(ContentNegotiation) { + json() } } diff --git a/src/commonMain/kotlin/com/infendro/ddns/Main.kt b/src/commonMain/kotlin/com/infendro/ddns/Main.kt index 0645597..4407eb0 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/Main.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/Main.kt @@ -13,7 +13,7 @@ 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.DnsServices.PORKBUN +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 diff --git a/src/commonMain/kotlin/com/infendro/ddns/argument/IpService.kt b/src/commonMain/kotlin/com/infendro/ddns/argument/IpService.kt index f84d9e0..c0ccf8b 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/argument/IpService.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/argument/IpService.kt @@ -3,13 +3,16 @@ 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.IpServices +import com.infendro.ddns.ip.ipify.Ipify object IpServiceParser : Argument.Parser { override fun parse( text: String?, ): IpService { - return IpServices.get(text!!) + return when (text) { + IpService.IPIFY -> Ipify + else -> throw Exception() + } } } @@ -17,7 +20,7 @@ class IpServiceOrDefaultArgument( override val name: String, ) : Argument.SingleOrElse() { override val other: IpService - get() = IpServices.DEFAULT + get() = IpService.DEFAULT override val parser: Parser get() = IpServiceParser diff --git a/src/commonMain/kotlin/com/infendro/ddns/dns/DnsService.kt b/src/commonMain/kotlin/com/infendro/ddns/dns/DnsService.kt index 815a42c..6b716a5 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/dns/DnsService.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/dns/DnsService.kt @@ -2,13 +2,13 @@ package com.infendro.ddns.dns import com.infendro.ddns.dns.model.DnsRecord -abstract class DnsService { - abstract fun getRecords( +interface DnsService { + fun getRecords( domain: String, subdomains: List, ): List - abstract fun updateRecord( + fun updateRecord( record: DnsRecord, value: String, ) @@ -22,4 +22,8 @@ abstract class DnsService { updateRecord(record, value) } } + + companion object { + const val PORKBUN = "porkbun" + } } diff --git a/src/commonMain/kotlin/com/infendro/ddns/dns/DnsServices.kt b/src/commonMain/kotlin/com/infendro/ddns/dns/DnsServices.kt deleted file mode 100644 index d06c37c..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/dns/DnsServices.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.infendro.ddns.dns - -object DnsServices { - const val PORKBUN = "porkbun" -} diff --git a/src/commonMain/kotlin/com/infendro/ddns/dns/Regex.kt b/src/commonMain/kotlin/com/infendro/ddns/dns/Regex.kt deleted file mode 100644 index b13d312..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/dns/Regex.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.infendro.ddns.dns - -object Regex { - val DOMAIN = Regex("""(?:(?(?:\w+\.)*\w+)\.)?(?\w+\.\w+)""") -} 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 c02302b..c1a4579 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/Porkbun.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/Porkbun.kt @@ -1,36 +1,52 @@ package com.infendro.ddns.dns.porkbun -import com.infendro.ddns.Http.client -import com.infendro.ddns.dns.Regex.DOMAIN import com.infendro.ddns.dns.DnsService import com.infendro.ddns.dns.model.DnsRecord import com.infendro.ddns.dns.porkbun.dto.request.GetRecordsRequest import com.infendro.ddns.dns.porkbun.dto.request.UpdateRecordRequest import com.infendro.ddns.dns.porkbun.dto.response.GetRecordsResponse -import io.ktor.client.call.body -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.http.ContentType -import io.ktor.http.contentType +import com.infendro.ddns.http +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* import kotlinx.coroutines.runBlocking class Porkbun( val key: String, val secretkey: String, -) : DnsService() { +) : DnsService { override fun getRecords( domain: String, subdomains: List, ): List = runBlocking { - val response = client.post("https://api.porkbun.com/api/json/v3/dns/retrieve/$domain") { - val request = getRecordsRequest() + val response = http.post("https://api.porkbun.com/api/json/v3/dns/retrieve/$domain") { + val request = GetRecordsRequest( + apikey = key, + secretapikey = secretkey, + ) contentType(ContentType.Application.Json) setBody(request) } val body = response.body() body.records - .map { model(it) } + .map { + val labels = it.name.split('.') + val subdomain = labels.dropLast(2).joinToString(".") + val domain = labels.takeLast(2).joinToString(".") + + DnsRecord( + id = it.id, + type = DnsRecord.Type.valueOf(it.type), + domain = DnsRecord.Domain( + subdomain = subdomain, + domain = domain, + ), + value = it.content, + ttl = it.ttl.toInt(), + priority = it.prio?.toInt(), + ) + } .filter { it.type in DnsRecord.Type.address && (it.domain.subdomain ?: "") in subdomains } } @@ -38,51 +54,17 @@ class Porkbun( record: DnsRecord, value: String, ): Unit = runBlocking { - client.post("https://api.porkbun.com/api/json/v3/dns/edit/${record.domain.domain}/${record.id}") { - val request = updateRecordRequest(record, value) + http.post("https://api.porkbun.com/api/json/v3/dns/edit/${record.domain.domain}/${record.id}") { + val request = UpdateRecordRequest( + apikey = key, + secretapikey = secretkey, + type = record.type.toString(), + name = record.domain.subdomain ?: "", + content = value, + ttl = record.ttl.toString(), + ) contentType(ContentType.Application.Json) setBody(request) } } - - private fun model( - record: GetRecordsResponse.Record, - ): DnsRecord { - val match = DOMAIN.matchEntire(record.name)!! - val domain = match.groups["domain"]!!.value - val subdomain = match.groups["subdomain"]?.value - - return DnsRecord( - id = record.id, - type = DnsRecord.Type.valueOf(record.type), - domain = DnsRecord.Domain( - subdomain = subdomain, - domain = domain, - ), - value = record.content, - ttl = record.ttl.toInt(), - priority = record.prio?.toInt(), - ) - } - - private fun getRecordsRequest(): GetRecordsRequest { - return GetRecordsRequest( - apikey = key, - secretapikey = secretkey, - ) - } - - private fun updateRecordRequest( - record: DnsRecord, - ip: String, - ): UpdateRecordRequest { - return UpdateRecordRequest( - apikey = key, - secretapikey = secretkey, - type = record.type.toString(), - name = record.domain.subdomain ?: "", - content = ip, - ttl = record.ttl.toString(), - ) - } } diff --git a/src/commonMain/kotlin/com/infendro/ddns/ip/IpService.kt b/src/commonMain/kotlin/com/infendro/ddns/ip/IpService.kt index 8fe5795..8e21661 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/ip/IpService.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/ip/IpService.kt @@ -1,9 +1,16 @@ package com.infendro.ddns.ip -abstract class IpService { - abstract val ipv4: String - abstract val ipv6: String +import com.infendro.ddns.ip.ipify.Ipify - val ip: List - get() = listOf(ipv4, ipv6) +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/IpServices.kt b/src/commonMain/kotlin/com/infendro/ddns/ip/IpServices.kt deleted file mode 100644 index 5391205..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/ip/IpServices.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.infendro.ddns.ip - -import com.infendro.ddns.ip.ipify.Ipify - -object IpServices { - const val IPIFY = "ipify" - val DEFAULT = Ipify - - fun get( - name: String, - ): IpService { - return when (name) { - IPIFY -> Ipify - else -> throw Exception() - } - } -} 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 b1bff21..20d83cf 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/ip/ipify/Ipify.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/ip/ipify/Ipify.kt @@ -1,23 +1,23 @@ package com.infendro.ddns.ip.ipify -import com.infendro.ddns.Http +import com.infendro.ddns.http import com.infendro.ddns.ip.IpService import com.infendro.ddns.ip.ipify.dto.response.GetIpResponse -import io.ktor.client.call.body -import io.ktor.client.request.get +import io.ktor.client.call.* +import io.ktor.client.request.* import kotlinx.coroutines.runBlocking -object Ipify : IpService() { +object Ipify : IpService { override val ipv4: String get() = runBlocking { - val response = Http.client.get("https://api.ipify.org?format=json") + val response = http.get("https://api.ipify.org?format=json") val body = response.body() body.ip } override val ipv6: String get() = runBlocking { - val response = Http.client.get("https://api64.ipify.org?format=json") + val response = http.get("https://api6.ipify.org?format=json") val body = response.body() body.ip } From 90b8f3f08a0bb03bbdce7d4e23966a2d337ff4aa Mon Sep 17 00:00:00 2001 From: Infendro Date: Sun, 14 Dec 2025 23:10:52 +0100 Subject: [PATCH 3/6] update README --- README.md | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 685fa72..773daf2 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,39 @@ # DDNS -This application enables automatically updating DNS records for various providers. +DDNS is an application that enables the automatic updating of DNS records for various providers. -## Features - -* Automatically detect the current IP. - * Both IPv4 and IPv6 are supported. - * IP resolution depends on the selected service. - * ipify (default) -* Update DNS records in the selected provider. - * Porkbun -* Native (Linux) +Supported providers: +- Porkbun ## Build Execute `./gradlew assemble`. The resulting binary will be located in `./build/bin/releaseExecutable/`. -Removing the extension from the binary and adding it to `/usr/local/bin/` enables the use of `ddns` in the terminal. - ## Usage The following command updates all A and AAAA DNS records in Porkbun for `example.com` and `test.example.com`. ``` ddns porkbun \ - --key=... \ - --secret-key=... \ + --key= \ + --secret-key= \ --domain=example.com \ --subdomain= \ --subdomain=test ``` -Optionally, a file can be used to declare one or more configurations. +Alternatively, a file can be used to declare one or more configurations. The above example can be declared as follows. ``` porkbun -key=... -secret-key=... +key= +secret-key= domain=example.com subdomain= subdomain=test ``` -To use this config file, execute the command `ddns --config=...`. +To use this config file, execute the command `ddns --config=`. By default, the config file is `/etc/ddns.conf`. - -This process can be automated using systemd or cron. From 67509e9062360459fc8839097bdd5656d4f3519a Mon Sep 17 00:00:00 2001 From: Infendro Date: Sat, 14 Mar 2026 12:08:44 +0100 Subject: [PATCH 4/6] Refactor and Bump version to 1.1.0 --- .gitea/workflows/main.yaml | 43 ------ README.md | 20 ++- build.gradle.kts | 2 +- gradle/libs.versions.toml | 12 +- .../kotlin/com/infendro/ddns/Main.kt | 109 +------------ .../com/infendro/ddns/argument/IpService.kt | 36 ----- .../com/infendro/ddns/command/Parameters.kt | 12 ++ .../kotlin/com/infendro/ddns/command/ddns.kt | 9 ++ .../com/infendro/ddns/command/porkbun.kt | 29 ++++ .../com/infendro/ddns/command/update.kt | 35 +++++ .../kotlin/com/infendro/ddns/config/Config.kt | 143 +++--------------- .../ddns/config/IpServiceSerializer.kt | 25 +++ .../kotlin/com/infendro/ddns/config/Regex.kt | 6 - .../com/infendro/ddns/dns/DnsService.kt | 29 ++-- .../com/infendro/ddns/dns/model/DnsRecord.kt | 7 +- .../com/infendro/ddns/dns/porkbun/Porkbun.kt | 10 +- .../kotlin/com/infendro/ddns/ip/IpService.kt | 7 - .../com/infendro/ddns/ip/ipify/Ipify.kt | 2 +- .../com/infendro/ddns/parameter/IpService.kt | 25 +++ .../com/infendro/ddns/parameter/Path.kt | 19 +++ 20 files changed, 219 insertions(+), 361 deletions(-) delete mode 100644 .gitea/workflows/main.yaml delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/argument/IpService.kt create mode 100644 src/commonMain/kotlin/com/infendro/ddns/command/Parameters.kt create mode 100644 src/commonMain/kotlin/com/infendro/ddns/command/ddns.kt create mode 100644 src/commonMain/kotlin/com/infendro/ddns/command/porkbun.kt create mode 100644 src/commonMain/kotlin/com/infendro/ddns/command/update.kt create mode 100644 src/commonMain/kotlin/com/infendro/ddns/config/IpServiceSerializer.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/config/Regex.kt create mode 100644 src/commonMain/kotlin/com/infendro/ddns/parameter/IpService.kt create mode 100644 src/commonMain/kotlin/com/infendro/ddns/parameter/Path.kt 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) From aa7f37500fef14562825e530f04c228aa3d8e723 Mon Sep 17 00:00:00 2001 From: Infendro Date: Sun, 15 Mar 2026 16:55:31 +0100 Subject: [PATCH 5/6] Refactor --- src/commonMain/kotlin/com/infendro/ddns/command/Parameters.kt | 2 +- .../com/infendro/ddns/{ => command}/parameter/IpService.kt | 2 +- .../kotlin/com/infendro/ddns/{ => command}/parameter/Path.kt | 2 +- src/commonMain/kotlin/com/infendro/ddns/command/update.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/commonMain/kotlin/com/infendro/ddns/{ => command}/parameter/IpService.kt (95%) rename src/commonMain/kotlin/com/infendro/ddns/{ => command}/parameter/Path.kt (93%) diff --git a/src/commonMain/kotlin/com/infendro/ddns/command/Parameters.kt b/src/commonMain/kotlin/com/infendro/ddns/command/Parameters.kt index 0f45952..c059004 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/command/Parameters.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/command/Parameters.kt @@ -3,7 +3,7 @@ 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 +import com.infendro.ddns.command.parameter.ipService internal object Parameters { val DOMAIN = argument.string("domain") diff --git a/src/commonMain/kotlin/com/infendro/ddns/parameter/IpService.kt b/src/commonMain/kotlin/com/infendro/ddns/command/parameter/IpService.kt similarity index 95% rename from src/commonMain/kotlin/com/infendro/ddns/parameter/IpService.kt rename to src/commonMain/kotlin/com/infendro/ddns/command/parameter/IpService.kt index 0e4be88..1e7756b 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/parameter/IpService.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/command/parameter/IpService.kt @@ -1,4 +1,4 @@ -package com.infendro.ddns.parameter +package com.infendro.ddns.command.parameter import com.infendro.cli.command.argument.ArgumentFactory import com.infendro.cli.command.argument.argument diff --git a/src/commonMain/kotlin/com/infendro/ddns/parameter/Path.kt b/src/commonMain/kotlin/com/infendro/ddns/command/parameter/Path.kt similarity index 93% rename from src/commonMain/kotlin/com/infendro/ddns/parameter/Path.kt rename to src/commonMain/kotlin/com/infendro/ddns/command/parameter/Path.kt index 29c5929..990299e 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/parameter/Path.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/command/parameter/Path.kt @@ -1,4 +1,4 @@ -package com.infendro.ddns.parameter +package com.infendro.ddns.command.parameter import com.infendro.cli.command.argument.ArgumentFactory import com.infendro.cli.command.argument.argument diff --git a/src/commonMain/kotlin/com/infendro/ddns/command/update.kt b/src/commonMain/kotlin/com/infendro/ddns/command/update.kt index d6707cd..d2c34cd 100644 --- a/src/commonMain/kotlin/com/infendro/ddns/command/update.kt +++ b/src/commonMain/kotlin/com/infendro/ddns/command/update.kt @@ -4,7 +4,7 @@ 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 com.infendro.ddns.command.parameter.path import kotlinx.io.buffered import kotlinx.io.files.Path import kotlinx.io.files.SystemFileSystem From 540d990aebb841c38c4fd0aad51f14208dd04453 Mon Sep 17 00:00:00 2001 From: Infendro Date: Sat, 29 Aug 2026 14:36:14 +0200 Subject: [PATCH 6/6] rewrite in rust --- .gitignore | 12 +- Cargo.lock | 1747 +++++++++++++++++ Cargo.toml | 10 + LICENSE | 674 ------- README.md | 45 - build.gradle.kts | 38 - gradle/libs.versions.toml | 25 - gradle/wrapper/gradle-wrapper.jar | Bin 43764 -> 0 bytes gradle/wrapper/gradle-wrapper.properties | 7 - gradlew | 251 --- gradlew.bat | 94 - settings.gradle.kts | 6 - .../kotlin/com/infendro/ddns/Http.kt | 11 - .../kotlin/com/infendro/ddns/Main.kt | 5 - .../com/infendro/ddns/command/Parameters.kt | 12 - .../kotlin/com/infendro/ddns/command/ddns.kt | 9 - .../ddns/command/parameter/IpService.kt | 25 - .../infendro/ddns/command/parameter/Path.kt | 19 - .../com/infendro/ddns/command/porkbun.kt | 29 - .../com/infendro/ddns/command/update.kt | 35 - .../kotlin/com/infendro/ddns/config/Config.kt | 29 - .../ddns/config/IpServiceSerializer.kt | 25 - .../com/infendro/ddns/dns/DnsService.kt | 30 - .../com/infendro/ddns/dns/model/DnsRecord.kt | 30 - .../com/infendro/ddns/dns/porkbun/Porkbun.kt | 64 - .../porkbun/dto/request/GetRecordsRequest.kt | 9 - .../dto/request/UpdateRecordRequest.kt | 13 - .../dto/response/GetRecordsResponse.kt | 21 - .../kotlin/com/infendro/ddns/ip/IpService.kt | 9 - .../com/infendro/ddns/ip/ipify/Ipify.kt | 24 - .../ip/ipify/dto/response/GetIpResponse.kt | 8 - src/config.rs | 27 + src/dns.rs | 93 + src/ip.rs | 18 + src/main.rs | 52 + 35 files changed, 1949 insertions(+), 1557 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 build.gradle.kts delete mode 100644 gradle/libs.versions.toml delete mode 100644 gradle/wrapper/gradle-wrapper.jar delete mode 100644 gradle/wrapper/gradle-wrapper.properties delete mode 100755 gradlew delete mode 100644 gradlew.bat delete mode 100644 settings.gradle.kts delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/Http.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/Main.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/command/Parameters.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/command/ddns.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/command/parameter/IpService.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/command/parameter/Path.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/command/porkbun.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/command/update.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/config/Config.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/config/IpServiceSerializer.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/dns/DnsService.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/dns/model/DnsRecord.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/Porkbun.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/dto/request/GetRecordsRequest.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/dto/request/UpdateRecordRequest.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/dto/response/GetRecordsResponse.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/ip/IpService.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/ip/ipify/Ipify.kt delete mode 100644 src/commonMain/kotlin/com/infendro/ddns/ip/ipify/dto/response/GetIpResponse.kt create mode 100644 src/config.rs create mode 100644 src/dns.rs create mode 100644 src/ip.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore index 769753d..4b67358 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,2 @@ -# IDE -/.idea/ - -# Gradle -/.gradle/ -/gradle.properties - -# Kotlin -/.kotlin/ -/build/ +# Rust +/target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e451fe7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1747 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "ddns" +version = "1.0.0" +dependencies = [ + "reqwest", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5f2db05 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ddns" +version = "1.0.0" +edition = "2024" + +[dependencies] +reqwest = { version = "0.13.4", features = ["json"] } +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" +tokio = { version = "1.53.1", features = ["full"] } diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702..0000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/README.md b/README.md deleted file mode 100644 index f1404c6..0000000 --- a/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# DDNS - -DDNS is an application that enables the automatic updating of DNS records for various providers. - -Supported providers: -- Porkbun - -## Build - -Execute `./gradlew assemble`. -The resulting binary will be located in `./build/bin/releaseExecutable/`. - -## Usage - -The following command updates all A and AAAA DNS records in Porkbun for `example.com` and `test.example.com`. - -``` -ddns porkbun update \ - --key= \ - --secret-key= \ - --domain=example.com \ - --subdomain= \ - --subdomain=test -``` - -Alternatively, a file can be used to declare one or more configurations. -The above example can be declared as follows. - -``` -[ - { - "type": "porkbun", - "key": "", - "secretKey": "" - "domain": "example.com", - "subdomains": [ - "", - "test" - ] - } -] -``` - -To use this config file, execute the command `ddns --config=`. -By default, the config file is `/etc/ddns.conf`. diff --git a/build.gradle.kts b/build.gradle.kts deleted file mode 100644 index d15ef94..0000000 --- a/build.gradle.kts +++ /dev/null @@ -1,38 +0,0 @@ -import com.infendro.plugin.infendro - -group = "com.infendro" -version = "1.1.0" - -plugins { - alias(libs.plugins.infendro) - alias(libs.plugins.multiplatform) - alias(libs.plugins.serialization) -} - -kotlin { - linuxX64 { - binaries.executable { - entryPoint = "com.infendro.ddns.main" - } - } - - repositories { - infendro() - mavenCentral() - } - - sourceSets { - commonMain.dependencies { - implementation(libs.io) - implementation(libs.serialization.json) - implementation(libs.coroutines) - implementation(libs.cli) - implementation(libs.ktor.client) - implementation(libs.ktor.client.contentNegotiation) - implementation(libs.ktor.client.serialization.json) - } - nativeMain.dependencies { - implementation(libs.ktor.client.curl) - } - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml deleted file mode 100644 index 6d1cd66..0000000 --- a/gradle/libs.versions.toml +++ /dev/null @@ -1,25 +0,0 @@ -[versions] -infendro = "1.1.0" -kotlin = "2.3.0" -io = "0.9.0" -coroutines = "1.10.2" -serialization = "1.10.0" -cli = "1.4.0" -ktor = "3.4.0" - -[plugins] -infendro = { id = "com.infendro.plugin", version.ref = "infendro" } -multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } -serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } - -[libraries] -io = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "io" } -coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } -serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } - -cli = { module = "com.infendro:cli", version.ref = "cli" } - -ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } -ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor" } -ktor-client-contentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } -ktor-client-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 1b33c55baabb587c669f562ae36f953de2481846..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43764 zcma&OWmKeVvL#I6?i3D%6z=Zs?ofE*?rw#G$eqJB ziT4y8-Y@s9rkH0Tz>ll(^xkcTl)CY?rS&9VNd66Yc)g^6)JcWaY(5$5gt z8gr3SBXUTN;~cBgz&})qX%#!Fxom2Yau_`&8)+6aSN7YY+pS410rRUU*>J}qL0TnJ zRxt*7QeUqTh8j)Q&iavh<}L+$Jqz))<`IfKussVk%%Ah-Ti?Eo0hQH!rK%K=#EAw0 zwq@@~XNUXRnv8$;zv<6rCRJ6fPD^hfrh;0K?n z=p!u^3xOgWZ%f3+?+>H)9+w^$Tn1e;?UpVMJb!!;f)`6f&4|8mr+g)^@x>_rvnL0< zvD0Hu_N>$(Li7|Jgu0mRh&MV+<}`~Wi*+avM01E)Jtg=)-vViQKax!GeDc!xv$^mL z{#OVBA$U{(Zr8~Xm|cP@odkHC*1R8z6hcLY#N@3E-A8XEvpt066+3t9L_6Zg6j@9Q zj$$%~yO-OS6PUVrM2s)(T4#6=JpI_@Uz+!6=GdyVU?`!F=d;8#ZB@(5g7$A0(`eqY z8_i@3w$0*es5mrSjhW*qzrl!_LQWs4?VfLmo1Sd@Ztt53+etwzAT^8ow_*7Jp`Y|l z*UgSEwvxq+FYO!O*aLf-PinZYne7Ib6ny3u>MjQz=((r3NTEeU4=-i0LBq3H-VJH< z^>1RE3_JwrclUn9vb7HcGUaFRA0QHcnE;6)hnkp%lY1UII#WPAv?-;c?YH}LWB8Nl z{sx-@Z;QxWh9fX8SxLZk8;kMFlGD3Jc^QZVL4nO)1I$zQwvwM&_!kW+LMf&lApv#< zur|EyC|U@5OQuph$TC_ZU`{!vJp`13e9alaR0Dbn5ikLFH7>eIz4QbV|C=%7)F=qo z_>M&5N)d)7G(A%c>}UCrW!Ql_6_A{?R7&CL`;!KOb3 z8Z=$YkV-IF;c7zs{3-WDEFJzuakFbd*4LWd<_kBE8~BFcv}js_2OowRNzWCtCQ6&k z{&~Me92$m*@e0ANcWKuz)?YjB*VoSTx??-3Cc0l2U!X^;Bv@m87eKHukAljrD54R+ zE;@_w4NPe1>3`i5Qy*3^E9x#VB6?}v=~qIprrrd5|DFkg;v5ixo0IsBmik8=Y;zv2 z%Bcf%NE$a44bk^`i4VwDLTbX=q@j9;JWT9JncQ!+Y%2&HHk@1~*L8-{ZpY?(-a9J-1~<1ltr9i~D9`P{XTIFWA6IG8c4;6bFw*lzU-{+?b&%OcIoCiw00n>A1ra zFPE$y@>ebbZlf(sN_iWBzQKDV zmmaLX#zK!@ZdvCANfwV}9@2O&w)!5gSgQzHdk2Q`jG6KD7S+1R5&F)j6QTD^=hq&7 zHUW+r^da^%V(h(wonR(j?BOiC!;y=%nJvz?*aW&5E87qq;2z`EI(f zBJNNSMFF9U{sR-af5{IY&AtoGcoG)Iq-S^v{7+t0>7N(KRoPj;+2N5;9o_nxIGjJ@ z7bYQK)bX)vEhy~VL%N6g^NE@D5VtV+Q8U2%{ji_=6+i^G%xeskEhH>Sqr194PJ$fB zu1y^){?9Vkg(FY2h)3ZHrw0Z<@;(gd_dtF#6y_;Iwi{yX$?asr?0N0_B*CifEi7<6 zq`?OdQjCYbhVcg+7MSgIM|pJRu~`g?g3x?Tl+V}#$It`iD1j+!x+!;wS0+2e>#g?Z z*EA^k7W{jO1r^K~cD#5pamp+o@8&yw6;%b|uiT?{Wa=4+9<}aXWUuL#ZwN1a;lQod zW{pxWCYGXdEq9qAmvAB904}?97=re$>!I%wxPV#|f#@A*Y=qa%zHlDv^yWbR03%V0 zprLP+b(#fBqxI%FiF*-n8HtH6$8f(P6!H3V^ysgd8de-N(@|K!A< z^qP}jp(RaM9kQ(^K(U8O84?D)aU(g?1S8iWwe)gqpHCaFlJxb*ilr{KTnu4_@5{K- z)n=CCeCrPHO0WHz)dDtkbZfUfVBd?53}K>C5*-wC4hpDN8cGk3lu-ypq+EYpb_2H; z%vP4@&+c2p;thaTs$dc^1CDGlPG@A;yGR5@$UEqk6p58qpw#7lc<+W(WR;(vr(D>W z#(K$vE#uBkT=*q&uaZwzz=P5mjiee6>!lV?c}QIX%ZdkO1dHg>Fa#xcGT6~}1*2m9 zkc7l3ItD6Ie~o_aFjI$Ri=C!8uF4!Ky7iG9QTrxVbsQroi|r)SAon#*B*{}TB-?=@ z8~jJs;_R2iDd!$+n$%X6FO&PYS{YhDAS+U2o4su9x~1+U3z7YN5o0qUK&|g^klZ6X zj_vrM5SUTnz5`*}Hyts9ADwLu#x_L=nv$Z0`HqN`Zo=V>OQI)fh01n~*a%01%cx%0 z4LTFVjmW+ipVQv5rYcn3;d2o4qunWUY!p+?s~X~(ost@WR@r@EuDOSs8*MT4fiP>! zkfo^!PWJJ1MHgKS2D_hc?Bs?isSDO61>ebl$U*9*QY(b=i&rp3@3GV@z>KzcZOxip z^dzA~44;R~cnhWz7s$$v?_8y-k!DZys}Q?4IkSyR!)C0j$(Gm|t#e3|QAOFaV2}36 z?dPNY;@I=FaCwylc_;~kXlZsk$_eLkNb~TIl8QQ`mmH&$*zwwR8zHU*sId)rxHu*K z;yZWa8UmCwju%aSNLwD5fBl^b0Ux1%q8YR*uG`53Mi<`5uA^Dc6Ync)J3N7;zQ*75)hf%a@{$H+%S?SGT)ks60)?6j$ zspl|4Ad6@%-r1t*$tT(en!gIXTUDcsj?28ZEzz)dH)SV3bZ+pjMaW0oc~rOPZP@g! zb9E+ndeVO_Ib9c_>{)`01^`ZS198 z)(t=+{Azi11$eu%aU7jbwuQrO`vLOixuh~%4z@mKr_Oc;F%Uq01fA)^W&y+g16e?rkLhTxV!EqC%2}sx_1u7IBq|}Be&7WI z4I<;1-9tJsI&pQIhj>FPkQV9{(m!wYYV@i5h?A0#BN2wqlEwNDIq06|^2oYVa7<~h zI_OLan0Do*4R5P=a3H9`s5*>xU}_PSztg`+2mv)|3nIy=5#Z$%+@tZnr> zLcTI!Mxa`PY7%{;KW~!=;*t)R_sl<^b>eNO@w#fEt(tPMg_jpJpW$q_DoUlkY|uo> z0-1{ouA#;t%spf*7VjkK&$QrvwUERKt^Sdo)5@?qAP)>}Y!h4(JQ!7{wIdkA+|)bv z&8hBwoX4v|+fie}iTslaBX^i*TjwO}f{V)8*!dMmRPi%XAWc8<_IqK1jUsApk)+~R zNFTCD-h>M5Y{qTQ&0#j@I@tmXGj%rzhTW5%Bkh&sSc=$Fv;M@1y!zvYG5P2(2|(&W zlcbR1{--rJ&s!rB{G-sX5^PaM@3EqWVz_y9cwLR9xMig&9gq(voeI)W&{d6j1jh&< zARXi&APWE1FQWh7eoZjuP z;vdgX>zep^{{2%hem;e*gDJhK1Hj12nBLIJoL<=0+8SVEBx7!4Ea+hBY;A1gBwvY<)tj~T=H`^?3>zeWWm|LAwo*S4Z%bDVUe z6r)CH1H!(>OH#MXFJ2V(U(qxD{4Px2`8qfFLG+=a;B^~Te_Z!r3RO%Oc#ZAHKQxV5 zRYXxZ9T2A%NVJIu5Pu7!Mj>t%YDO$T@M=RR(~mi%sv(YXVl`yMLD;+WZ{vG9(@P#e zMo}ZiK^7^h6TV%cG+;jhJ0s>h&VERs=tuZz^Tlu~%d{ZHtq6hX$V9h)Bw|jVCMudd zwZ5l7In8NT)qEPGF$VSKg&fb0%R2RnUnqa){)V(X(s0U zkCdVZe6wy{+_WhZh3qLp245Y2RR$@g-!9PjJ&4~0cFSHMUn=>dapv)hy}|y91ZWTV zCh=z*!S3_?`$&-eZ6xIXUq8RGl9oK0BJw*TdU6A`LJqX9eS3X@F)g$jLkBWFscPhR zpCv8#KeAc^y>>Y$k^=r|K(DTC}T$0#jQBOwB#@`P6~*IuW_8JxCG}J4va{ zsZzt}tt+cv7=l&CEuVtjD6G2~_Meh%p4RGuY?hSt?(sreO_F}8r7Kp$qQdvCdZnDQ zxzc*qchE*E2=WK)^oRNa>Ttj`fpvF-JZ5tu5>X1xw)J@1!IqWjq)ESBG?J|ez`-Tc zi5a}GZx|w-h%5lNDE_3ho0hEXMoaofo#Z;$8|2;EDF&*L+e$u}K=u?pb;dv$SXeQM zD-~7P0i_`Wk$#YP$=hw3UVU+=^@Kuy$>6?~gIXx636jh{PHly_a2xNYe1l60`|y!7 z(u%;ILuW0DDJ)2%y`Zc~hOALnj1~txJtcdD#o4BCT68+8gZe`=^te6H_egxY#nZH&P*)hgYaoJ^qtmpeea`35Fw)cy!w@c#v6E29co8&D9CTCl%^GV|X;SpneSXzV~LXyRn-@K0Df z{tK-nDWA!q38M1~`xUIt_(MO^R(yNY#9@es9RQbY@Ia*xHhD&=k^T+ zJi@j2I|WcgW=PuAc>hs`(&CvgjL2a9Rx zCbZyUpi8NWUOi@S%t+Su4|r&UoU|ze9SVe7p@f1GBkrjkkq)T}X%Qo1g!SQ{O{P?m z-OfGyyWta+UCXH+-+(D^%kw#A1-U;?9129at7MeCCzC{DNgO zeSqsV>W^NIfTO~4({c}KUiuoH8A*J!Cb0*sp*w-Bg@YfBIPZFH!M}C=S=S7PLLcIG zs7K77g~W)~^|+mx9onzMm0qh(f~OsDTzVmRtz=aZTllgR zGUn~_5hw_k&rll<4G=G+`^Xlnw;jNYDJz@bE?|r866F2hA9v0-8=JO3g}IHB#b`hy zA42a0>{0L7CcabSD+F7?pGbS1KMvT{@1_@k!_+Ki|5~EMGt7T%u=79F)8xEiL5!EJ zzuxQ`NBliCoJMJdwu|);zRCD<5Sf?Y>U$trQ-;xj6!s5&w=9E7)%pZ+1Nh&8nCCwM zv5>Ket%I?cxr3vVva`YeR?dGxbG@pi{H#8@kFEf0Jq6~K4>kt26*bxv=P&jyE#e$| zDJB_~imk^-z|o!2njF2hL*|7sHCnzluhJjwLQGDmC)Y9 zr9ZN`s)uCd^XDvn)VirMgW~qfn1~SaN^7vcX#K1G`==UGaDVVx$0BQnubhX|{e z^i0}>k-;BP#Szk{cFjO{2x~LjK{^Upqd&<+03_iMLp0$!6_$@TbX>8U-f*-w-ew1?`CtD_0y_Lo|PfKi52p?`5$Jzx0E8`M0 zNIb?#!K$mM4X%`Ry_yhG5k@*+n4||2!~*+&pYLh~{`~o(W|o64^NrjP?-1Lgu?iK^ zTX6u3?#$?R?N!{599vg>G8RGHw)Hx&=|g4599y}mXNpM{EPKKXB&+m?==R3GsIq?G zL5fH={=zawB(sMlDBJ+{dgb)Vx3pu>L=mDV0{r1Qs{0Pn%TpopH{m(By4;{FBvi{I z$}x!Iw~MJOL~&)p93SDIfP3x%ROjg}X{Sme#hiJ&Yk&a;iR}V|n%PriZBY8SX2*;6 z4hdb^&h;Xz%)BDACY5AUsV!($lib4>11UmcgXKWpzRL8r2Srl*9Y(1uBQsY&hO&uv znDNff0tpHlLISam?o(lOp#CmFdH<6HmA0{UwfU#Y{8M+7od8b8|B|7ZYR9f<#+V|ZSaCQvI$~es~g(Pv{2&m_rKSB2QQ zMvT}$?Ll>V+!9Xh5^iy3?UG;dF-zh~RL#++roOCsW^cZ&({6q|?Jt6`?S8=16Y{oH zp50I7r1AC1(#{b`Aq5cw>ypNggHKM9vBx!W$eYIzD!4KbLsZGr2o8>g<@inmS3*>J zx8oG((8f!ei|M@JZB`p7+n<Q}?>h249<`7xJ?u}_n;Gq(&km#1ULN87CeTO~FY zS_Ty}0TgQhV zOh3T7{{x&LSYGQfKR1PDIkP!WnfC1$l+fs@Di+d4O=eVKeF~2fq#1<8hEvpwuqcaH z4A8u~r^gnY3u6}zj*RHjk{AHhrrDqaj?|6GaVJbV%o-nATw}ASFr!f`Oz|u_QPkR# z0mDudY1dZRlk@TyQ?%Eti=$_WNFtLpSx9=S^be{wXINp%MU?a`F66LNU<c;0&ngifmP9i;bj6&hdGMW^Kf8e6ZDXbQD&$QAAMo;OQ)G zW(qlHh;}!ZP)JKEjm$VZjTs@hk&4{?@+NADuYrr!R^cJzU{kGc1yB?;7mIyAWwhbeA_l_lw-iDVi7wcFurf5 z#Uw)A@a9fOf{D}AWE%<`s1L_AwpZ?F!Vac$LYkp<#A!!`XKaDC{A%)~K#5z6>Hv@V zBEqF(D5?@6r3Pwj$^krpPDCjB+UOszqUS;b2n>&iAFcw<*im2(b3|5u6SK!n9Sg4I z0KLcwA6{Mq?p%t>aW0W!PQ>iUeYvNjdKYqII!CE7SsS&Rj)eIw-K4jtI?II+0IdGq z2WT|L3RL?;GtGgt1LWfI4Ka`9dbZXc$TMJ~8#Juv@K^1RJN@yzdLS8$AJ(>g!U9`# zx}qr7JWlU+&m)VG*Se;rGisutS%!6yybi%B`bv|9rjS(xOUIvbNz5qtvC$_JYY+c& za*3*2$RUH8p%pSq>48xR)4qsp!Q7BEiJ*`^>^6INRbC@>+2q9?x(h0bpc>GaNFi$K zPH$6!#(~{8@0QZk=)QnM#I=bDx5vTvjm$f4K}%*s+((H2>tUTf==$wqyoI`oxI7>C z&>5fe)Yg)SmT)eA(|j@JYR1M%KixxC-Eceknf-;N=jJTwKvk#@|J^&5H0c+%KxHUI z6dQbwwVx3p?X<_VRVb2fStH?HH zFR@Mp=qX%#L3XL)+$PXKV|o|#DpHAoqvj6uQKe@M-mnhCSou7Dj4YuO6^*V`m)1lf z;)@e%1!Qg$10w8uEmz{ENb$^%u}B;J7sDd zump}onoD#!l=agcBR)iG!3AF0-63%@`K9G(CzKrm$VJ{v7^O9Ps7Zej|3m= zVXlR&yW6=Y%mD30G@|tf=yC7-#L!16Q=dq&@beWgaIL40k0n% z)QHrp2Jck#evLMM1RGt3WvQ936ZC9vEje0nFMfvmOHVI+&okB_K|l-;|4vW;qk>n~ z+|kk8#`K?x`q>`(f6A${wfw9Cx(^)~tX7<#TpxR#zYG2P+FY~mG{tnEkv~d6oUQA+ z&hNTL=~Y@rF`v-RZlts$nb$3(OL1&@Y11hhL9+zUb6)SP!;CD)^GUtUpCHBE`j1te zAGud@miCVFLk$fjsrcpjsadP__yj9iEZUW{Ll7PPi<$R;m1o!&Xdl~R_v0;oDX2z^!&8}zNGA}iYG|k zmehMd1%?R)u6R#<)B)1oe9TgYH5-CqUT8N7K-A-dm3hbm_W21p%8)H{O)xUlBVb+iUR}-v5dFaCyfSd zC6Bd7=N4A@+Bna=!-l|*_(nWGDpoyU>nH=}IOrLfS+-d40&(Wo*dDB9nQiA2Tse$R z;uq{`X7LLzP)%Y9aHa4YQ%H?htkWd3Owv&UYbr5NUDAH^<l@Z0Cx%`N+B*i!!1u>D8%;Qt1$ zE5O0{-`9gdDxZ!`0m}ywH!;c{oBfL-(BH<&SQ~smbcobU!j49O^f4&IIYh~f+hK*M zZwTp%{ZSAhMFj1qFaOA+3)p^gnXH^=)`NTYgTu!CLpEV2NF=~-`(}7p^Eof=@VUbd z_9U|8qF7Rueg&$qpSSkN%%%DpbV?8E8ivu@ensI0toJ7Eas^jyFReQ1JeY9plb^{m z&eQO)qPLZQ6O;FTr*aJq=$cMN)QlQO@G&%z?BKUs1&I^`lq>=QLODwa`(mFGC`0H< zOlc*|N?B5&!U6BuJvkL?s1&nsi$*5cCv7^j_*l&$-sBmRS85UIrE--7eD8Gr3^+o? zqG-Yl4S&E;>H>k^a0GdUI(|n1`ws@)1%sq2XBdK`mqrNq_b4N{#VpouCXLzNvjoFv zo9wMQ6l0+FT+?%N(ka*;%m~(?338bu32v26!{r)|w8J`EL|t$}TA4q_FJRX5 zCPa{hc_I(7TGE#@rO-(!$1H3N-C0{R$J=yPCXCtGk{4>=*B56JdXU9cQVwB`6~cQZ zf^qK21x_d>X%dT!!)CJQ3mlHA@ z{Prkgfs6=Tz%63$6Zr8CO0Ak3A)Cv#@BVKr&aiKG7RYxY$Yx>Bj#3gJk*~Ps-jc1l z;4nltQwwT4@Z)}Pb!3xM?+EW0qEKA)sqzw~!C6wd^{03-9aGf3Jmt=}w-*!yXupLf z;)>-7uvWN4Unn8b4kfIza-X=x*e4n5pU`HtgpFFd))s$C@#d>aUl3helLom+RYb&g zI7A9GXLRZPl}iQS*d$Azxg-VgcUr*lpLnbPKUV{QI|bsG{8bLG<%CF( zMoS4pRDtLVYOWG^@ox^h8xL~afW_9DcE#^1eEC1SVSb1BfDi^@g?#f6e%v~Aw>@w- zIY0k+2lGWNV|aA*e#`U3=+oBDmGeInfcL)>*!w|*;mWiKNG6wP6AW4-4imN!W)!hE zA02~S1*@Q`fD*+qX@f3!2yJX&6FsEfPditB%TWo3=HA;T3o2IrjS@9SSxv%{{7&4_ zdS#r4OU41~GYMiib#z#O;zohNbhJknrPPZS6sN$%HB=jUnlCO_w5Gw5EeE@KV>soy z2EZ?Y|4RQDDjt5y!WBlZ(8M)|HP<0YyG|D%RqD+K#e7-##o3IZxS^wQ5{Kbzb6h(i z#(wZ|^ei>8`%ta*!2tJzwMv+IFHLF`zTU8E^Mu!R*45_=ccqI};Zbyxw@U%a#2}%f zF>q?SrUa_a4H9l+uW8JHh2Oob>NyUwG=QH~-^ZebU*R@67DcXdz2{HVB4#@edz?B< z5!rQH3O0>A&ylROO%G^fimV*LX7>!%re{_Sm6N>S{+GW1LCnGImHRoF@csnFzn@P0 zM=jld0z%oz;j=>c7mMwzq$B^2mae7NiG}%>(wtmsDXkWk{?BeMpTrIt3Mizq?vRsf zi_WjNp+61uV(%gEU-Vf0;>~vcDhe(dzWdaf#4mH3o^v{0EWhj?E?$5v02sV@xL0l4 zX0_IMFtQ44PfWBbPYN#}qxa%=J%dlR{O!KyZvk^g5s?sTNycWYPJ^FK(nl3k?z-5t z39#hKrdO7V(@!TU)LAPY&ngnZ1MzLEeEiZznn7e-jLCy8LO zu^7_#z*%I-BjS#Pg-;zKWWqX-+Ly$T!4`vTe5ZOV0j?TJVA*2?*=82^GVlZIuH%9s zXiV&(T(QGHHah=s&7e|6y?g+XxZGmK55`wGV>@1U)Th&=JTgJq>4mI&Av2C z)w+kRoj_dA!;SfTfkgMPO>7Dw6&1*Hi1q?54Yng`JO&q->^CX21^PrU^JU#CJ_qhV zSG>afB%>2fx<~g8p=P8Yzxqc}s@>>{g7}F!;lCXvF#RV)^fyYb_)iKVCz1xEq=fJ| z0a7DMCK*FuP=NM*5h;*D`R4y$6cpW-E&-i{v`x=Jbk_xSn@2T3q!3HoAOB`@5Vg6) z{PW|@9o!e;v1jZ2{=Uw6S6o{g82x6g=k!)cFSC*oemHaVjg?VpEmtUuD2_J^A~$4* z3O7HsbA6wxw{TP5Kk)(Vm?gKo+_}11vbo{Tp_5x79P~#F)ahQXT)tSH5;;14?s)On zel1J>1x>+7;g1Iz2FRpnYz;sD0wG9Q!vuzE9yKi3@4a9Nh1!GGN?hA)!mZEnnHh&i zf?#ZEN2sFbf~kV;>K3UNj1&vFhc^sxgj8FCL4v>EOYL?2uuT`0eDH}R zmtUJMxVrV5H{L53hu3#qaWLUa#5zY?f5ozIn|PkMWNP%n zWB5!B0LZB0kLw$k39=!akkE9Q>F4j+q434jB4VmslQ;$ zKiO#FZ`p|dKS716jpcvR{QJkSNfDVhr2%~eHrW;fU45>>snr*S8Vik-5eN5k*c2Mp zyxvX&_cFbB6lODXznHHT|rsURe2!swomtrqc~w5 zymTM8!w`1{04CBprR!_F{5LB+2_SOuZN{b*!J~1ZiPpP-M;);!ce!rOPDLtgR@Ie1 zPreuqm4!H)hYePcW1WZ0Fyaqe%l}F~Orr)~+;mkS&pOhP5Ebb`cnUt!X_QhP4_4p( z8YKQCDKGIy>?WIFm3-}Br2-N`T&FOi?t)$hjphB9wOhBXU#Hb+zm&We_-O)s(wc`2 z8?VsvU;J>Ju7n}uUb3s1yPx_F*|FlAi=Ge=-kN?1;`~6szP%$3B0|8Sqp%ebM)F8v zADFrbeT0cgE>M0DMV@_Ze*GHM>q}wWMzt|GYC%}r{OXRG3Ij&<+nx9;4jE${Fj_r* z`{z1AW_6Myd)i6e0E-h&m{{CvzH=Xg!&(bLYgRMO_YVd8JU7W+7MuGWNE=4@OvP9+ zxi^vqS@5%+#gf*Z@RVyU9N1sO-(rY$24LGsg1>w>s6ST^@)|D9>cT50maXLUD{Fzf zt~tp{OSTEKg3ZSQyQQ5r51){%=?xlZ54*t1;Ow)zLe3i?8tD8YyY^k%M)e`V*r+vL zPqUf&m)U+zxps+NprxMHF{QSxv}>lE{JZETNk1&F+R~bp{_T$dbXL2UGnB|hgh*p4h$clt#6;NO~>zuyY@C-MD@)JCc5XrYOt`wW7! z_ti2hhZBMJNbn0O-uTxl_b6Hm313^fG@e;RrhIUK9@# z+DHGv_Ow$%S8D%RB}`doJjJy*aOa5mGHVHz0e0>>O_%+^56?IkA5eN+L1BVCp4~m=1eeL zb;#G!#^5G%6Mw}r1KnaKsLvJB%HZL)!3OxT{k$Yo-XrJ?|7{s4!H+S2o?N|^Z z)+?IE9H7h~Vxn5hTis^3wHYuOU84+bWd)cUKuHapq=&}WV#OxHpLab`NpwHm8LmOo zjri+!k;7j_?FP##CpM+pOVx*0wExEex z@`#)K<-ZrGyArK;a%Km`^+We|eT+#MygHOT6lXBmz`8|lyZOwL1+b+?Z$0OhMEp3R z&J=iRERpv~TC=p2-BYLC*?4 zxvPs9V@g=JT0>zky5Poj=fW_M!c)Xxz1<=&_ZcL=LMZJqlnO1P^xwGGW*Z+yTBvbV z-IFe6;(k1@$1;tS>{%pXZ_7w+i?N4A2=TXnGf=YhePg8bH8M|Lk-->+w8Y+FjZ;L=wSGwxfA`gqSn)f(XNuSm>6Y z@|#e-)I(PQ^G@N`%|_DZSb4_pkaEF0!-nqY+t#pyA>{9^*I-zw4SYA1_z2Bs$XGUZbGA;VeMo%CezHK0lO={L%G)dI-+8w?r9iexdoB{?l zbJ}C?huIhWXBVs7oo{!$lOTlvCLZ_KN1N+XJGuG$rh<^eUQIqcI7^pmqhBSaOKNRq zrx~w^?9C?*&rNwP_SPYmo;J-#!G|{`$JZK7DxsM3N^8iR4vvn>E4MU&Oe1DKJvLc~ zCT>KLZ1;t@My zRj_2hI^61T&LIz)S!+AQIV23n1>ng+LUvzv;xu!4;wpqb#EZz;F)BLUzT;8UA1x*6vJ zicB!3Mj03s*kGV{g`fpC?V^s(=JG-k1EMHbkdP4P*1^8p_TqO|;!Zr%GuP$8KLxuf z=pv*H;kzd;P|2`JmBt~h6|GxdU~@weK5O=X&5~w$HpfO}@l-T7@vTCxVOwCkoPQv8 z@aV_)I5HQtfs7^X=C03zYmH4m0S!V@JINm6#(JmZRHBD?T!m^DdiZJrhKpBcur2u1 zf9e4%k$$vcFopK5!CC`;ww(CKL~}mlxK_Pv!cOsFgVkNIghA2Au@)t6;Y3*2gK=5d z?|@1a)-(sQ%uFOmJ7v2iG&l&m^u&^6DJM#XzCrF%r>{2XKyxLD2rgWBD;i(!e4InDQBDg==^z;AzT2z~OmV0!?Z z0S9pX$+E;w3WN;v&NYT=+G8hf=6w0E1$0AOr61}eOvE8W1jX%>&Mjo7&!ulawgzLH zbcb+IF(s^3aj12WSi#pzIpijJJzkP?JzRawnxmNDSUR#7!29vHULCE<3Aa#be}ie~d|!V+ z%l~s9Odo$G&fH!t!+`rUT0T9DulF!Yq&BfQWFZV1L9D($r4H(}Gnf6k3^wa7g5|Ws zj7%d`!3(0bb55yhC6@Q{?H|2os{_F%o=;-h{@Yyyn*V7?{s%Grvpe!H^kl6tF4Zf5 z{Jv1~yZ*iIWL_9C*8pBMQArfJJ0d9Df6Kl#wa}7Xa#Ef_5B7=X}DzbQXVPfCwTO@9+@;A^Ti6il_C>g?A-GFwA0#U;t4;wOm-4oS})h z5&on>NAu67O?YCQr%7XIzY%LS4bha9*e*4bU4{lGCUmO2UQ2U)QOqClLo61Kx~3dI zmV3*(P6F_Tr-oP%x!0kTnnT?Ep5j;_IQ^pTRp=e8dmJtI4YgWd0}+b2=ATkOhgpXe z;jmw+FBLE}UIs4!&HflFr4)vMFOJ19W4f2^W(=2)F%TAL)+=F>IE$=e=@j-*bFLSg z)wf|uFQu+!=N-UzSef62u0-C8Zc7 zo6@F)c+nZA{H|+~7i$DCU0pL{0Ye|fKLuV^w!0Y^tT$isu%i1Iw&N|tX3kwFKJN(M zXS`k9js66o$r)x?TWL}Kxl`wUDUpwFx(w4Yk%49;$sgVvT~n8AgfG~HUcDt1TRo^s zdla@6heJB@JV z!vK;BUMznhzGK6PVtj0)GB=zTv6)Q9Yt@l#fv7>wKovLobMV-+(8)NJmyF8R zcB|_K7=FJGGn^X@JdFaat0uhKjp3>k#^&xE_}6NYNG?kgTp>2Iu?ElUjt4~E-?`Du z?mDCS9wbuS%fU?5BU@Ijx>1HG*N?gIP+<~xE4u=>H`8o((cS5M6@_OK%jSjFHirQK zN9@~NXFx*jS{<|bgSpC|SAnA@I)+GB=2W|JJChLI_mx+-J(mSJ!b)uUom6nH0#2^(L@JBlV#t zLl?j54s`Y3vE^c_3^Hl0TGu*tw_n?@HyO@ZrENxA+^!)OvUX28gDSF*xFtQzM$A+O zCG=n#6~r|3zt=8%GuG} z<#VCZ%2?3Q(Ad#Y7GMJ~{U3>E{5e@z6+rgZLX{Cxk^p-7dip^d29;2N1_mm4QkASo z-L`GWWPCq$uCo;X_BmGIpJFBlhl<8~EG{vOD1o|X$aB9KPhWO_cKiU*$HWEgtf=fn zsO%9bp~D2c@?*K9jVN@_vhR03>M_8h!_~%aN!Cnr?s-!;U3SVfmhRwk11A^8Ns`@KeE}+ zN$H}a1U6E;*j5&~Og!xHdfK5M<~xka)x-0N)K_&e7AjMz`toDzasH+^1bZlC!n()crk9kg@$(Y{wdKvbuUd04N^8}t1iOgsKF zGa%%XWx@WoVaNC1!|&{5ZbkopFre-Lu(LCE5HWZBoE#W@er9W<>R=^oYxBvypN#x3 zq#LC8&q)GFP=5^-bpHj?LW=)-g+3_)Ylps!3^YQ{9~O9&K)xgy zMkCWaApU-MI~e^cV{Je75Qr7eF%&_H)BvfyKL=gIA>;OSq(y z052BFz3E(Prg~09>|_Z@!qj}@;8yxnw+#Ej0?Rk<y}4ghbD569B{9hSFr*^ygZ zr6j7P#gtZh6tMk6?4V$*Jgz+#&ug;yOr>=qdI#9U&^am2qoh4Jy}H2%a|#Fs{E(5r z%!ijh;VuGA6)W)cJZx+;9Bp1LMUzN~x_8lQ#D3+sL{be-Jyeo@@dv7XguJ&S5vrH` z>QxOMWn7N-T!D@1(@4>ZlL^y5>m#0!HKovs12GRav4z!>p(1~xok8+_{| z#Ae4{9#NLh#Vj2&JuIn5$d6t@__`o}umFo(n0QxUtd2GKCyE+erwXY?`cm*h&^9*8 zJ+8x6fRZI-e$CRygofIQN^dWysCxgkyr{(_oBwwSRxZora1(%(aC!5BTtj^+YuevI zx?)H#(xlALUp6QJ!=l9N__$cxBZ5p&7;qD3PsXRFVd<({Kh+mShFWJNpy`N@ab7?9 zv5=klvCJ4bx|-pvOO2-+G)6O?$&)ncA#Urze2rlBfp#htudhx-NeRnJ@u%^_bfw4o z4|{b8SkPV3b>Wera1W(+N@p9H>dc6{cnkh-sgr?e%(YkWvK+0YXVwk0=d`)}*47*B z5JGkEdVix!w7-<%r0JF~`ZMMPe;f0EQHuYHxya`puazyph*ZSb1mJAt^k4549BfS; zK7~T&lRb=W{s&t`DJ$B}s-eH1&&-wEOH1KWsKn0a(ZI+G!v&W4A*cl>qAvUv6pbUR z#(f#EKV8~hk&8oayBz4vaswc(?qw1vn`yC zZQDl2PCB-&Uu@g9ZQHhO+v(W0bNig{-k0;;`+wM@#@J)8r?qOYs#&vUna8ILxN7S{ zp1s41KnR8miQJtJtOr|+qk}wrLt+N*z#5o`TmD1)E&QD(Vh&pjZJ_J*0!8dy_ z>^=@v=J)C`x&gjqAYu`}t^S=DFCtc0MkBU2zf|69?xW`Ck~(6zLD)gSE{7n~6w8j_ zoH&~$ED2k5-yRa0!r8fMRy z;QjBYUaUnpd}mf%iVFPR%Dg9!d>g`01m~>2s))`W|5!kc+_&Y>wD@@C9%>-lE`WB0 zOIf%FVD^cj#2hCkFgi-fgzIfOi+ya)MZK@IZhHT5FVEaSbv-oDDs0W)pA0&^nM0TW zmgJmd7b1R7b0a`UwWJYZXp4AJPteYLH>@M|xZFKwm!t3D3&q~av?i)WvAKHE{RqpD{{%OhYkK?47}+}` zrR2(Iv9bhVa;cDzJ%6ntcSbx7v7J@Y4x&+eWSKZ*eR7_=CVIUSB$^lfYe@g+p|LD{ zPSpQmxx@b$%d!05|H}WzBT4_cq?@~dvy<7s&QWtieJ9)hd4)$SZz}#H2UTi$CkFWW|I)v_-NjuH!VypONC=1`A=rm_jfzQ8Fu~1r8i{q-+S_j$ z#u^t&Xnfi5tZtl@^!fUJhx@~Cg0*vXMK}D{>|$#T*+mj(J_@c{jXBF|rm4-8%Z2o! z2z0o(4%8KljCm^>6HDK!{jI7p+RAPcty_~GZ~R_+=+UzZ0qzOwD=;YeZt*?3%UGdr z`c|BPE;yUbnyARUl&XWSNJ<+uRt%!xPF&K;(l$^JcA_CMH6)FZt{>6ah$|(9$2fc~ z=CD00uHM{qv;{Zk9FR0~u|3|Eiqv9?z2#^GqylT5>6JNZwKqKBzzQpKU2_pmtD;CT zi%Ktau!Y2Tldfu&b0UgmF(SSBID)15*r08eoUe#bT_K-G4VecJL2Pa=6D1K6({zj6 za(2Z{r!FY5W^y{qZ}08+h9f>EKd&PN90f}Sc0ejf%kB4+f#T8Q1=Pj=~#pi$U zp#5rMR%W25>k?<$;$x72pkLibu1N|jX4cWjD3q^Pk3js!uK6h7!dlvw24crL|MZs_ zb%Y%?Fyp0bY0HkG^XyS76Ts*|Giw{31LR~+WU5NejqfPr73Rp!xQ1mLgq@mdWncLy z%8}|nzS4P&`^;zAR-&nm5f;D-%yNQPwq4N7&yULM8bkttkD)hVU>h>t47`{8?n2&4 zjEfL}UEagLUYwdx0sB2QXGeRmL?sZ%J!XM`$@ODc2!y|2#7hys=b$LrGbvvjx`Iqi z&RDDm3YBrlKhl`O@%%&rhLWZ*ABFz2nHu7k~3@e4)kO3%$=?GEFUcCF=6-1n!x^vmu+Ai*amgXH+Rknl6U>#9w;A} zn2xanZSDu`4%%x}+~FG{Wbi1jo@wqBc5(5Xl~d0KW(^Iu(U3>WB@-(&vn_PJt9{1`e9Iic@+{VPc`vP776L*viP{wYB2Iff8hB%E3|o zGMOu)tJX!`qJ}ZPzq7>=`*9TmETN7xwU;^AmFZ-ckZjV5B2T09pYliaqGFY|X#E-8 z20b>y?(r-Fn5*WZ-GsK}4WM>@TTqsxvSYWL6>18q8Q`~JO1{vLND2wg@58OaU!EvT z1|o+f1mVXz2EKAbL!Q=QWQKDZpV|jznuJ}@-)1&cdo z^&~b4Mx{*1gurlH;Vhk5g_cM&6LOHS2 zRkLfO#HabR1JD4Vc2t828dCUG#DL}f5QDSBg?o)IYYi@_xVwR2w_ntlpAW0NWk$F1 z$If?*lP&Ka1oWfl!)1c3fl`g*lMW3JOn#)R1+tfwrs`aiFUgz3;XIJ>{QFxLCkK30 zNS-)#DON3yb!7LBHQJ$)4y%TN82DC2-9tOIqzhZ27@WY^<6}vXCWcR5iN{LN8{0u9 zNXayqD=G|e?O^*ms*4P?G%o@J1tN9_76e}E#66mr89%W_&w4n66~R;X_vWD(oArwj z4CpY`)_mH2FvDuxgT+akffhX0b_slJJ*?Jn3O3~moqu2Fs1oL*>7m=oVek2bnprnW zixkaIFU%+3XhNA@@9hyhFwqsH2bM|`P?G>i<-gy>NflhrN{$9?LZ1ynSE_Mj0rADF zhOz4FnK}wpLmQuV zgO4_Oz9GBu_NN>cPLA=`SP^$gxAnj;WjJnBi%Q1zg`*^cG;Q)#3Gv@c^j6L{arv>- zAW%8WrSAVY1sj$=umcAf#ZgC8UGZGoamK}hR7j6}i8#np8ruUlvgQ$j+AQglFsQQq zOjyHf22pxh9+h#n$21&$h?2uq0>C9P?P=Juw0|;oE~c$H{#RGfa>| zj)Iv&uOnaf@foiBJ}_;zyPHcZt1U~nOcNB{)og8Btv+;f@PIT*xz$x!G?u0Di$lo7 zOugtQ$Wx|C($fyJTZE1JvR~i7LP{ zbdIwqYghQAJi9p}V&$=*2Azev$6K@pyblphgpv8^9bN!?V}{BkC!o#bl&AP!3DAjM zmWFsvn2fKWCfjcAQmE+=c3Y7j@#7|{;;0f~PIodmq*;W9Fiak|gil6$w3%b_Pr6K_ zJEG@&!J%DgBZJDCMn^7mk`JV0&l07Bt`1ymM|;a)MOWz*bh2#d{i?SDe9IcHs7 zjCrnyQ*Y5GzIt}>`bD91o#~5H?4_nckAgotN{2%!?wsSl|LVmJht$uhGa+HiH>;av z8c?mcMYM7;mvWr6noUR{)gE!=i7cZUY7e;HXa221KkRoc2UB>s$Y(k%NzTSEr>W(u z<(4mcc)4rB_&bPzX*1?*ra%VF}P1nwiP5cykJ&W{!OTlz&Td0pOkVp+wc z@k=-Hg=()hNg=Q!Ub%`BONH{ z_=ZFgetj@)NvppAK2>8r!KAgi>#%*7;O-o9MOOfQjV-n@BX6;Xw;I`%HBkk20v`qoVd0)}L6_49y1IhR z_OS}+eto}OPVRn*?UHC{eGyFU7JkPz!+gX4P>?h3QOwGS63fv4D1*no^6PveUeE5% zlehjv_3_^j^C({a2&RSoVlOn71D8WwMu9@Nb@=E_>1R*ve3`#TF(NA0?d9IR_tm=P zOP-x;gS*vtyE1Cm zG0L?2nRUFj#aLr-R1fX*$sXhad)~xdA*=hF3zPZhha<2O$Ps+F07w*3#MTe?)T8|A!P!v+a|ot{|^$q(TX`35O{WI0RbU zCj?hgOv=Z)xV?F`@HKI11IKtT^ocP78cqHU!YS@cHI@{fPD?YXL)?sD~9thOAv4JM|K8OlQhPXgnevF=F7GKD2#sZW*d za}ma31wLm81IZxX(W#A9mBvLZr|PoLnP>S4BhpK8{YV_}C|p<)4#yO{#ISbco92^3 zv&kCE(q9Wi;9%7>>PQ!zSkM%qqqLZW7O`VXvcj;WcJ`2~v?ZTYB@$Q&^CTfvy?1r^ z;Cdi+PTtmQwHX_7Kz?r#1>D zS5lWU(Mw_$B&`ZPmqxpIvK<~fbXq?x20k1~9az-Q!uR78mCgRj*eQ>zh3c$W}>^+w^dIr-u{@s30J=)1zF8?Wn|H`GS<=>Om|DjzC{}Jt?{!fSJe*@$H zg>wFnlT)k#T?LslW zu$^7Uy~$SQ21cE?3Ijl+bLfuH^U5P^$@~*UY#|_`uvAIe(+wD2eF}z_y!pvomuVO; zS^9fbdv)pcm-B@CW|Upm<7s|0+$@@<&*>$a{aW+oJ%f+VMO<#wa)7n|JL5egEgoBv zl$BY(NQjE0#*nv=!kMnp&{2Le#30b)Ql2e!VkPLK*+{jv77H7)xG7&=aPHL7LK9ER z5lfHxBI5O{-3S?GU4X6$yVk>lFn;ApnwZybdC-GAvaznGW-lScIls-P?Km2mF>%B2 zkcrXTk+__hj-3f48U%|jX9*|Ps41U_cd>2QW81Lz9}%`mTDIhE)jYI$q$ma7Y-`>% z8=u+Oftgcj%~TU}3nP8&h7k+}$D-CCgS~wtWvM|UU77r^pUw3YCV80Ou*+bH0!mf0 zxzUq4ed6y>oYFz7+l18PGGzhB^pqSt)si=9M>~0(Bx9*5r~W7sa#w+_1TSj3Jn9mW zMuG9BxN=}4645Cpa#SVKjFst;9UUY@O<|wpnZk$kE+to^4!?0@?Cwr3(>!NjYbu?x z1!U-?0_O?k!NdM^-rIQ8p)%?M+2xkhltt*|l=%z2WFJhme7*2xD~@zk#`dQR$6Lmd zb3LOD4fdt$Cq>?1<%&Y^wTWX=eHQ49Xl_lFUA(YQYHGHhd}@!VpYHHm=(1-O=yfK#kKe|2Xc*9}?BDFN zD7FJM-AjVi)T~OG)hpSWqH>vlb41V#^G2B_EvYlWhDB{Z;Q9-0)ja(O+By`31=biA zG&Fs#5!%_mHi|E4Nm$;vVQ!*>=_F;ZC=1DTPB#CICS5fL2T3XmzyHu?bI;m7D4@#; ztr~;dGYwb?m^VebuULtS4lkC_7>KCS)F@)0OdxZIFZp@FM_pHnJes8YOvwB|++#G( z&dm*OP^cz95Wi15vh`Q+yB>R{8zqEhz5of>Po$9LNE{xS<)lg2*roP*sQ}3r3t<}; zPbDl{lk{pox~2(XY5=qg0z!W-x^PJ`VVtz$git7?)!h>`91&&hESZy1KCJ2nS^yMH z!=Q$eTyRi68rKxdDsdt+%J_&lapa{ds^HV9Ngp^YDvtq&-Xp}60B_w@Ma>_1TTC;^ zpbe!#gH}#fFLkNo#|`jcn?5LeUYto%==XBk6Ik0kc4$6Z+L3x^4=M6OI1=z5u#M%0 z0E`kevJEpJjvvN>+g`?gtnbo$@p4VumliZV3Z%CfXXB&wPS^5C+7of2tyVkMwNWBiTE2 z8CdPu3i{*vR-I(NY5syRR}I1TJOV@DJy-Xmvxn^IInF>Tx2e)eE9jVSz69$6T`M9-&om!T+I znia!ZWJRB28o_srWlAxtz4VVft8)cYloIoVF=pL zugnk@vFLXQ_^7;%hn9x;Vq?lzg7%CQR^c#S)Oc-8d=q_!2ZVH764V z!wDKSgP}BrVV6SfCLZnYe-7f;igDs9t+K*rbMAKsp9L$Kh<6Z;e7;xxced zn=FGY<}CUz31a2G}$Q(`_r~75PzM4l_({Hg&b@d8&jC}B?2<+ed`f#qMEWi z`gm!STV9E4sLaQX+sp5Nu9*;9g12naf5?=P9p@H@f}dxYprH+3ju)uDFt^V{G0APn zS;16Dk{*fm6&BCg#2vo?7cbkkI4R`S9SSEJ=#KBk3rl69SxnCnS#{*$!^T9UUmO#&XXKjHKBqLdt^3yVvu8yn|{ zZ#%1CP)8t-PAz(+_g?xyq;C2<9<5Yy<~C74Iw(y>uUL$+$mp(DRcCWbCKiGCZw@?_ zdomfp+C5xt;j5L@VfhF*xvZdXwA5pcdsG>G<8II-|1dhAgzS&KArcb0BD4ZZ#WfiEY{hkCq5%z9@f|!EwTm;UEjKJsUo696V>h zy##eXYX}GUu%t{Gql8vVZKkNhQeQ4C%n|RmxL4ee5$cgwlU+?V7a?(jI#&3wid+Kz5+x^G!bb#$q>QpR#BZ}Xo5UW^ zD&I`;?(a}Oys7-`I^|AkN?{XLZNa{@27Dv^s4pGowuyhHuXc zuctKG2x0{WCvg_sGN^n9myJ}&FXyGmUQnW7fR$=bj$AHR88-q$D!*8MNB{YvTTEyS zn22f@WMdvg5~o_2wkjItJN@?mDZ9UUlat2zCh(zVE=dGi$rjXF7&}*sxac^%HFD`Y zTM5D3u5x**{bW!68DL1A!s&$2XG@ytB~dX-?BF9U@XZABO`a|LM1X3HWCllgl0+uL z04S*PX$%|^WAq%jkzp~%9HyYIF{Ym?k)j3nMwPZ=hlCg9!G+t>tf0o|J2%t1 ztC+`((dUplgm3`+0JN~}&FRRJ3?l*>Y&TfjS>!ShS`*MwO{WIbAZR#<%M|4c4^dY8 z{Rh;-!qhY=dz5JthbWoovLY~jNaw>%tS4gHVlt5epV8ekXm#==Po$)}mh^u*cE>q7*kvX&gq)(AHoItMYH6^s6f(deNw%}1=7O~bTHSj1rm2|Cq+3M z93djjdomWCTCYu!3Slx2bZVy#CWDozNedIHbqa|otsUl+ut?>a;}OqPfQA05Yim_2 zs@^BjPoFHOYNc6VbNaR5QZfSMh2S*`BGwcHMM(1@w{-4jVqE8Eu0Bi%d!E*^Rj?cR z7qgxkINXZR)K^=fh{pc0DCKtrydVbVILI>@Y0!Jm>x-xM!gu%dehm?cC6ok_msDVA*J#{75%4IZt}X|tIVPReZS#aCvuHkZxc zHVMtUhT(wp09+w9j9eRqz~LtuSNi2rQx_QgQ(}jBt7NqyT&ma61ldD(s9x%@q~PQl zp6N*?=N$BtvjQ_xIT{+vhb1>{pM0Arde0!X-y))A4znDrVx8yrP3B1(7bKPE5jR@5 zwpzwT4cu~_qUG#zYMZ_!2Tkl9zP>M%cy>9Y(@&VoB84#%>amTAH{(hL4cDYt!^{8L z645F>BWO6QaFJ-{C-i|-d%j7#&7)$X7pv#%9J6da#9FB5KyDhkA+~)G0^87!^}AP>XaCSScr;kL;Z%RSPD2CgoJ;gpYT5&6NUK$86$T?jRH=w8nI9Z534O?5fk{kd z`(-t$8W|#$3>xoMfXvV^-A(Q~$8SKDE^!T;J+rQXP71XZ(kCCbP%bAQ1|%$%Ov9_a zyC`QP3uPvFoBqr_+$HenHklqyIr>PU_Fk5$2C+0eYy^~7U&(!B&&P2%7#mBUhM!z> z_B$Ko?{Pf6?)gpYs~N*y%-3!1>o-4;@1Zz9VQHh)j5U1aL-Hyu@1d?X;jtDBNk*vMXPn@ z+u@wxHN*{uHR!*g*4Xo&w;5A+=Pf9w#PeZ^x@UD?iQ&${K2c}UQgLRik-rKM#Y5rdDphdcNTF~cCX&9ViRP}`>L)QA4zNXeG)KXFzSDa6 zd^St;inY6J_i=5mcGTx4_^Ys`M3l%Q==f>{8S1LEHn{y(kbxn5g1ezt4CELqy)~TV6{;VW>O9?5^ ztcoxHRa0jQY7>wwHWcxA-BCwzsP>63Kt&3fy*n#Cha687CQurXaRQnf5wc9o8v7Rw zNwGr2fac;Wr-Ldehn7tF^(-gPJwPt@VR1f;AmKgxN&YPL;j=0^xKM{!wuU|^mh3NE zy35quf}MeL!PU;|{OW_x$TBothLylT-J>_x6p}B_jW1L>k)ps6n%7Rh z96mPkJIM0QFNYUM2H}YF5bs%@Chs6#pEnloQhEl?J-)es!(SoJpEPoMTdgA14-#mC zghayD-DJWtUu`TD8?4mR)w5E`^EHbsz2EjH5aQLYRcF{l7_Q5?CEEvzDo(zjh|BKg z3aJl_n#j&eFHsUw4~lxqnr!6NL*se)6H=A+T1e3xUJGQrd}oSPwSy5+$tt{2t5J5@(lFxl43amsARG74iyNC}uuS zd2$=(r6RdamdGx^eatX@F2D8?U23tDpR+Os?0Gq2&^dF+$9wiWf?=mDWfjo4LfRwL zI#SRV9iSz>XCSgEj!cW&9H-njJopYiYuq|2w<5R2!nZ27DyvU4UDrHpoNQZiGPkp@ z1$h4H46Zn~eqdj$pWrv;*t!rTYTfZ1_bdkZmVVIRC21YeU$iS-*XMNK`#p8Z_DJx| zk3Jssf^XP7v0X?MWFO{rACltn$^~q(M9rMYoVxG$15N;nP)A98k^m3CJx8>6}NrUd@wp-E#$Q0uUDQT5GoiK_R{ z<{`g;8s>UFLpbga#DAf%qbfi`WN1J@6IA~R!YBT}qp%V-j!ybkR{uY0X|x)gmzE0J z&)=eHPjBxJvrZSOmt|)hC+kIMI;qgOnuL3mbNR0g^<%|>9x7>{}>a2qYSZAGPt4it?8 zNcLc!Gy0>$jaU?}ZWxK78hbhzE+etM`67*-*x4DN>1_&{@5t7_c*n(qz>&K{Y?10s zXsw2&nQev#SUSd|D8w7ZD2>E<%g^; zV{yE_O}gq?Q|zL|jdqB^zcx7vo(^})QW?QKacx$yR zhG|XH|8$vDZNIfuxr-sYFR{^csEI*IM#_gd;9*C+SysUFejP0{{z7@P?1+&_o6=7V|EJLQun^XEMS)w(=@eMi5&bbH*a0f;iC~2J74V2DZIlLUHD&>mlug5+v z6xBN~8-ovZylyH&gG#ptYsNlT?-tzOh%V#Y33zlsJ{AIju`CjIgf$@gr8}JugRq^c zAVQ3;&uGaVlVw}SUSWnTkH_6DISN&k2QLMBe9YU=sA+WiX@z)FoSYX`^k@B!j;ZeC zf&**P?HQG6Rk98hZ*ozn6iS-dG}V>jQhb3?4NJB*2F?6N7Nd;EOOo;xR7acylLaLy z9)^lykX39d@8@I~iEVar4jmjjLWhR0d=EB@%I;FZM$rykBNN~jf>#WbH4U{MqhhF6 zU??@fSO~4EbU4MaeQ_UXQcFyO*Rae|VAPLYMJEU`Q_Q_%s2*>$#S^)&7er+&`9L=1 z4q4ao07Z2Vsa%(nP!kJ590YmvrWg+YrgXYs_lv&B5EcoD`%uL79WyYA$0>>qi6ov7 z%`ia~J^_l{p39EY zv>>b}Qs8vxsu&WcXEt8B#FD%L%ZpcVtY!rqVTHe;$p9rbb5O{^rFMB>auLn-^;s+-&P1#h~mf~YLg$8M9 zZ4#87;e-Y6x6QO<{McUzhy(%*6| z)`D~A(TJ$>+0H+mct(jfgL4x%^oC^T#u(bL)`E2tBI#V1kSikAWmOOYrO~#-cc_8! zCe|@1&mN2{*ceeiBldHCdrURk4>V}79_*TVP3aCyV*5n@jiNbOm+~EQ_}1#->_tI@ zqXv+jj2#8xJtW508rzFrYcJxoek@iW6SR@1%a%Bux&;>25%`j3UI`0DaUr7l79`B1 zqqUARhW1^h6=)6?;@v>xrZNM;t}{yY3P@|L}ey@gG( z9r{}WoYN(9TW&dE2dEJIXkyHA4&pU6ki=rx&l2{DLGbVmg4%3Dlfvn!GB>EVaY_%3+Df{fBiqJV>~Xf8A0aqUjgpa} zoF8YXO&^_x*Ej}nw-$-F@(ddB>%RWoPUj?p8U{t0=n>gAI83y<9Ce@Q#3&(soJ{64 z37@Vij1}5fmzAuIUnXX`EYe;!H-yTVTmhAy;y8VZeB#vD{vw9~P#DiFiKQ|kWwGFZ z=jK;JX*A;Jr{#x?n8XUOLS;C%f|zj-7vXtlf_DtP7bpurBeX%Hjwr z4lI-2TdFpzkjgiv!8Vfv`=SP+s=^i3+N~1ELNWUbH|ytVu>EyPN_3(4TM^QE1swRo zoV7Y_g)a>28+hZG0e7g%@2^s>pzR4^fzR-El}ARTmtu!zjZLuX%>#OoU3}|rFjJg} zQ2TmaygxJ#sbHVyiA5KE+yH0LREWr%^C*yR|@gM$nK2P zo}M}PV0v))uJh&33N>#aU376@ZH79u(Yw`EQ2hM3SJs9f99+cO6_pNW$j$L-CtAfe zYfM)ccwD!P%LiBk!eCD?fHCGvgMQ%Q2oT_gmf?OY=A>&PaZQOq4eT=lwbaf}33LCH zFD|)lu{K7$8n9gX#w4~URjZxWm@wlH%oL#G|I~Fb-v^0L0TWu+`B+ZG!yII)w05DU z>GO?n(TN+B=>HdxVDSlIH76pta$_LhbBg;eZ`M7OGcqt||qi zogS72W1IN%=)5JCyOHWoFP7pOFK0L*OAh=i%&VW&4^LF@R;+K)t^S!96?}^+5QBIs zjJNTCh)?)4k^H^g1&jc>gysM`y^8Rm3qsvkr$9AeWwYpa$b22=yAd1t<*{ zaowSEFP+{y?Ob}8&cwfqoy4Pb9IA~VnM3u!trIK$&&0Op#Ql4j>(EW?UNUv#*iH1$ z^j>+W{afcd`{e&`-A{g}{JnIzYib)!T56IT@YEs{4|`sMpW3c8@UCoIJv`XsAw!XC z34|Il$LpW}CIHFC5e*)}00I5{%OL*WZRGzC0?_}-9{#ue?-ug^ zLE|uv-~6xnSs_2_&CN9{9vyc!Xgtn36_g^wI0C4s0s^;8+p?|mm;Odt3`2ZjwtK;l zfd6j)*Fr#53>C6Y8(N5?$H0ma;BCF3HCjUs7rpb2Kf*x3Xcj#O8mvs#&33i+McX zQpBxD8!O{5Y8D&0*QjD=Yhl9%M0)&_vk}bmN_Ud^BPN;H=U^bn&(csl-pkA+GyY0Z zKV7sU_4n;}uR78ouo8O%g*V;79KY?3d>k6%gpcmQsKk&@Vkw9yna_3asGt`0Hmj59 z%0yiF*`jXhByBI9QsD=+>big5{)BGe&+U2gAARGe3ID)xrid~QN_{I>k}@tzL!Md_ z&=7>TWciblF@EMC3t4-WX{?!m!G6$M$1S?NzF*2KHMP3Go4=#ZHkeIv{eEd;s-yD# z_jU^Ba06TZqvV|Yd;Z_sN%$X=!T+&?#p+OQIHS%!LO`Hx0q_Y0MyGYFNoM{W;&@0@ zLM^!X4KhdtsET5G<0+|q0oqVXMW~-7LW9Bg}=E$YtNh1#1D^6Mz(V9?2g~I1( zoz9Cz=8Hw98zVLwC2AQvp@pBeKyidn6Xu0-1SY1((^Hu*-!HxFUPs)yJ+i`^BC>PC zjwd0mygOVK#d2pRC9LxqGc6;Ui>f{YW9Bvb>33bp^NcnZoH~w9(lM5@JiIlfa-6|k ziy31UoMN%fvQfhi8^T+=yrP{QEyb-jK~>$A4SZT-N56NYEbpvO&yUme&pWKs3^94D zH{oXnUTb3T@H+RgzML*lejx`WAyw*?K7B-I(VJx($2!NXYm%3`=F~TbLv3H<{>D?A zJo-FDYdSA-(Y%;4KUP2SpHKAIcv9-ld(UEJE7=TKp|Gryn;72?0LHqAN^fk6%8PCW z{g_-t)G5uCIf0I`*F0ZNl)Z>))MaLMpXgqWgj-y;R+@A+AzDjsTqw2Mo9ULKA3c70 z!7SOkMtZb+MStH>9MnvNV0G;pwSW9HgP+`tg}e{ij0H6Zt5zJ7iw`hEnvye!XbA@!~#%vIkzowCOvq5I5@$3wtc*w2R$7!$*?}vg4;eDyJ_1=ixJuEp3pUS27W?qq(P^8$_lU!mRChT}ctvZz4p!X^ zOSp|JOAi~f?UkwH#9k{0smZ7-#=lK6X3OFEMl7%)WIcHb=#ZN$L=aD`#DZKOG4p4r zwlQ~XDZ`R-RbF&hZZhu3(67kggsM-F4Y_tI^PH8PMJRcs7NS9ogF+?bZB*fcpJ z=LTM4W=N9yepVvTj&Hu~0?*vR1HgtEvf8w%Q;U0^`2@e8{SwgX5d(cQ|1(!|i$km! zvY03MK}j`sff;*-%mN~ST>xU$6Bu?*Hm%l@0dk;j@%>}jsgDcQ)Hn*UfuThz9(ww_ zasV`rSrp_^bp-0sx>i35FzJwA!d6cZ5#5#nr@GcPEjNnFHIrtUYm1^Z$;{d&{hQV9 z6EfFHaIS}46p^5I-D_EcwwzUUuO}mqRh&T7r9sfw`)G^Q%oHxEs~+XoM?8e*{-&!7 z7$m$lg9t9KP9282eke608^Q2E%H-xm|oJ8=*SyEo} z@&;TQ3K)jgspgKHyGiKVMCz>xmC=H5Fy3!=TP)-R3|&1S-B)!6q50wfLHKM@7Bq6E z44CY%G;GY>tC`~yh!qv~YdXw! zSkquvYNs6k1r7>Eza?Vkkxo6XRS$W7EzL&A`o>=$HXgBp{L(i^$}t`NcnAxzbH8Ht z2!;`bhKIh`f1hIFcI5bHI=ueKdzmB9)!z$s-BT4ItyY|NaA_+o=jO%MU5as9 zc2)aLP>N%u>wlaXTK!p)r?+~)L+0eCGb5{8WIk7K52$nufnQ+m8YF+GQc&{^(zh-$ z#wyWV*Zh@d!b(WwXqvfhQX)^aoHTBkc;4ossV3&Ut*k>AI|m+{#kh4B!`3*<)EJVj zwrxK>99v^k4&Y&`Awm>|exo}NvewV%E+@vOc>5>%H#BK9uaE2$vje zWYM5fKuOTtn96B_2~~!xJPIcXF>E_;yO8AwpJ4)V`Hht#wbO3Ung~@c%%=FX4)q+9 z99#>VC2!4l`~0WHs9FI$Nz+abUq# zz`Of97})Su=^rGp2S$)7N3rQCj#0%2YO<R&p>$<#lgXcUj=4H_{oAYiT3 z44*xDn-$wEzRw7#@6aD)EGO$0{!C5Z^7#yl1o;k0PhN=aVUQu~eTQ^Xy{z8Ow6tk83 z4{5xe%(hx)%nD&|e*6sTWH`4W&U!Jae#U4TnICheJmsw{l|CH?UA{a6?2GNgpZLyzU2UlFu1ZVwlALmh_DOs03J^Cjh1im`E3?9&zvNmg(MuMw&0^Lu$(#CJ*q6DjlKsY-RMJ^8yIY|{SQZ*9~CH|u9L z`R78^r=EbbR*_>5?-)I+$6i}G)%mN(`!X72KaV(MNUP7Nv3MS9S|Pe!%N2AeOt5zG zVJ;jI4HZ$W->Ai_4X+`9c(~m=@ek*m`ZQbv3ryI-AD#AH=`x$~WeW~M{Js57(K7(v ze5`};LG|%C_tmd>bkufMWmAo&B+DT9ZV~h(4jg0>^aeAqL`PEUzJJtI8W1M!bQWpv zvN(d}E1@nlYa!L!!A*RN!(Q3F%J?5PvQ0udu?q-T)j3JKV~NL>KRb~w-lWc685uS6 z=S#aR&B8Sc8>cGJ!!--?kwsJTUUm`Jk?7`H z7PrO~xgBrSW2_tTlCq1LH8*!o?pj?qxy8}(=r_;G18POrFh#;buWR0qU24+XUaVZ0 z?(sXcr@-YqvkCmHr{U2oPogHL{r#3r49TeR<{SJX1pcUqyWPrkYz^X8#QW~?F)R5i z>p^!i<;qM8Nf{-fd6!_&V*e_9qP6q(s<--&1Ttj01j0w>bXY7y1W*%Auu&p|XSOH=)V7Bd4fUKh&T1)@cvqhuD-d=?w}O zjI%i(f|thk0Go*!d7D%0^ztBfE*V=(ZIN84f5HU}T9?ulmEYzT5usi=DeuI*d|;M~ zp_=Cx^!4k#=m_qSPBr5EK~E?3J{dWWPH&oCcNepYVqL?nh4D5ynfWip$m*YlZ8r^Z zuFEUL-nW!3qjRCLIWPT0x)FDL7>Yt7@8dA?R2kF@WE>ysMY+)lTsgNM#3VbXVGL}F z1O(>q>2a+_`6r5Xv$NZAnp=Kgnr3)cL(^=8ypEeOf3q8(HGe@7Tt59;yFl||w|mnO zHDxg2G3z8=(6wjj9kbcEY@Z0iOd7Gq5GiPS5% z*sF1J<#daxDV2Z8H>wxOF<;yKzMeTaSOp_|XkS9Sfn6Mpe9UBi1cSTieGG5$O;ZLIIJ60Y>SN4vC?=yE_CWlo(EEE$e4j?z&^FM%kNmRtlbEL^dPPgvs9sbK5fGw*r@ z+!EU@u$T8!nZh?Fdf_qk$VuHk^yVw`h`_#KoS*N%epIIOfQUy_&V}VWDGp3tplMbf z5Se1sJUC$7N0F1-9jdV2mmGK{-}fu|Nv;12jDy0<-kf^AmkDnu6j~TPWOgy1MT68|D z=4=50jVbUKdKaQgD`eWGr3I&^<6uhkjz$YwItY8%Yp9{z4-{6g{73<_b*@XJ4Nm3-3z z?BW3{aY_ccRjb@W1)i5nLg|7BnWS!B`_Uo9CWaE`Ij327QH?i)9A}4Ug4wmxVVa^b z-4+m%-wwOl7cKH7+=x&nrCrbEC)Q$fpg&V83#uEH;C=GNMz`ps@^RxK%T*8%OPnC` z{WO~J%nxYJ`x|N%?&i7?;{_8t^jM&=50HlaOQj8fS}_`moH$c;vI<|cruPFnpT8yU zS%rPOCUSd5Zdb(zwk`hqwTQn)*&n)uYsP*F_(~xEWq}C= zv30kFmZFwJZ@ELVX3?$dXQh|icO7UrL*_5G=I^xXjImz`ZPp>?g#tf(ej~KaIU0algsG!IS09;>?MvqGg#c{i+}qY|{P8W~O%#>|gFd z<1dr$-oxyRGN17yZo1OwLnzwYs0|;IS_nymNB0IlSzPQ%-r`?T=;_XQ^~&#}b|AB} zkNbN5uB?-sUB-T5QLlg%Uk3)uHB;>VIzGe9_J9 zaeISkQm!v(9d(0ML^b9fR^sfHFlH?7Mvddt37OuR{|O0{uv)(&-6<87W4 zyO>s!=cPgP3O&7xxU5DlIPw_o3O>6o6Qb?JWs3qw#p3sBc3g$?Dx zi(6D+DYgV;GrUis-CL%Qe{nvZnwaVXmbhH(|GFh|Q)k=1uvA$I@1DXI7bKlQ@8D6P zS?(*?><>)G49q0wr;NajpxP4W2G)kHl6^=Z>hrNEI4Mwd_$O6$1dXF;Q#hE(-eeW6 zz03GJF%Wl?HO=_ztv5*zRlcU~{+{k%#N59mgm~eK>P!QZ6E?#Cu^2)+K8m@ySvZ*5 z|HDT}BkF@3!l(0%75G=1u2hETXEj!^1Z$!)!lyGXlWD!_vqGE$Z)#cUVBqlORW>0^ zDjyVTxwKHKG|0}j-`;!R-p>}qQfBl(?($7pP<+Y8QE#M8SCDq~k<+>Q^Zf@cT_WdX3~BSe z+|KK|7OL5Hm5(NFP~j>Ct3*$wi0n0!xl=(C61`q&cec@mFlH(sy%+RH<=s)8aAPN`SfJdkAQjdv82G5iRdv8 zh{9wHUZaniSEpslXl^_ODh}mypC?b*9FzLjb~H@3DFSe;D(A-K3t3eOTB(m~I6C;(-lKAvit(70k`%@+O*Ztdz;}|_TS~B?Tpmi=QKC^m_ z2YpEaT3iiz*;T~ap1yiA)a`dKMwu`^UhIUeltNQ1Yjo=q@bI@&3zH?rVUg=IxLy-ni zyxDu%-Fr{H6owTjZU2O5>nDb=q&Jz_TjeSq%!2m40x&U6w~GQ({quPL73IsJS;f`$ zsuhioqCBj(gJ>2hoo)Gou7(WP*pX)f=Y=!=k!&1K?EYY%jJ~X&DnK{^saPQK<1BJ z_A`_{%ZozcB(3w$z^To^6d|XuT@=X~wtW!+{4ID@N{AB~J6AL5vuY>JwvWCNFKsKh zd}@>q@_WV#QZ&UJ0#?X(pXR!oyXOEG3rqzHbCzGLONDb042i$})fM@XF)uSP(DHUc z^&{|$*xe{cs?Gp8=B%RY3L7#$ve$?TWh>MZdxF1zH1v}1z+$Ov#G7?%D)bBCyDe*% zSeKSpETC2V1){II>@UwJi>4uBN+iAx+82E~gb|Cr&8E^i&)A!uv-g?jzH99wU}8+# z$nh>yvb;TwZmS@7LrvuCu_d0-WxFNI&C7%sWuTL%YU!l|I1{|->=dlOeHOCtUO#zkS3ESO8LHV4hTdQL5EdV zuWD33fFPH}HPrW^s$Qn1Xgp&AT6<-He{{4%eIu3rN=iK|9mURdKXfB&Q?qGok%!cs ze53UP{Z!TO-Y@q2;;k2avA3`lm4OoN4@S*k=UA)7H;qZ`d8`XaYFCv?Ba+uGW@r5v z&&{nf(24WSBOhc7!qF^@0cz;XcUynNaj6w2349;s!K{KVqs5yS{ z7VubS`2OzT^5#1~6Tt^RTvt9-J|D2F>y~>2;jeF>g`hx5l%B3H=aLExQihuYngzlnBTYOTHJQMzl>kwqN5JYs)Ej zblA@ntkUS~xi+}y6|(81helS}Q~&VB37qyV|S3Y=><^1wh%msQM?fz z<58MX(=|PSUKCF#)dbhR%D&xgCD?$aR0qen+wpp6 zst}vX18!Be96TD??j1HsHTUx(a&@F?=gT`Q$oJFFyrh^;zgz!(NlAHGn0cJy@us=w zNhC#l5G;H}+>49Nsh12=ZPO2r*2OBQe5kpb&1?*PIBFitK8}FUfb~S-#hKfF0o#&d z#3aPkB$9scYku&kA6{0xHnBV#&Wei5J>5T-XX-gUXEPo+9b7WL=*XESc(3BshL`aj zXp}QIp*40}oWJt*l043e8_5;H5PI5c)U&IEw5dF(4zjX0y_lk9 zAp@!mK>WUqHo)-jop=DoK>&no>kAD=^qIE7qis&_*4~ z6q^EF$D@R~3_xseCG>Ikb6Gfofb$g|75PPyyZN&tiRxqovo_k zO|HA|sgy#B<32gyU9x^&)H$1jvw@qp+1b(eGAb)O%O!&pyX@^nQd^9BQ4{(F8<}|A zhF&)xusQhtoXOOhic=8#Xtt5&slLia3c*a?dIeczyTbC#>FTfiLST57nc3@Y#v_Eg#VUv zT8cKH#f3=1PNj!Oroz_MAR*pow%Y0*6YCYmUy^7`^r|j23Q~^*TW#cU7CHf0eAD_0 zEWEVddxFgQ7=!nEBQ|ibaScslvhuUk^*%b#QUNrEB{3PG@uTxNwW}Bs4$nS9wc(~O zG7Iq>aMsYkcr!9#A;HNsJrwTDYkK8ikdj{M;N$sN6BqJ<8~z>T20{J8Z2rRUuH7~3 z=tgS`AgxbBOMg87UT4Lwge`*Y=01Dvk>)^{Iu+n6fuVX4%}>?3czOGR$0 zpp*wp>bsFFSV`V;r_m+TZns$ZprIi`OUMhe^cLE$2O+pP3nP!YB$ry}2THx2QJs3< za1;>d-AggCarrQ>&Z!d@;mW+!q6eXhb&`GbzUDSxpl8AJ#Cm#tuc)_xh(2NV=5XMs zrf_ozRYO$NkC=pKFX5OH8v1>0i9Z$ec`~Mf+_jQ68spn(CJwclDhEEkH2Qw;${J$clv__nUjn5jA0wCLEnu1j;v!0vB>Ri6m9`;R{JMS%^)4FC zU0Z44+u$I$w=Bj|iu4DT5h~sS`C*zbmX?@-crY}E+hy>}2~C0Nn(EKk@5^qO4@l@! z6O0lr%tzGC`D^)8xU3FnMZVm0kX1sBWhaQyzVoXFWwr%Ny?=2M{5s#5i7fTu3gEkG zc{(Pr$v=;`Y#&`y*J}#M9ux>0?xu!`$9cUKm#Bdd_&S#LPTS?ZPV6zN6>W6JTS~-LfjL{mB=b(KMk3 z2HjBSlJeyUVqDd=Mt!=hpYsvby2GL&3~zm;0{^nZJq+4vb?5HH4wufvr}IX42sHeK zm@x?HN$8TsTavXs)tLDFJtY9b)y~Tl@7z4^I8oUQq4JckH@~CVQ;FoK(+e0XAM>1O z(ei}h?)JQp>)d=6ng-BZF1Z5hsAKW@mXq+hU?r8I(*%`tnIIOXw7V6ZK(T9RFJJe@ zZS!aC+p)Gf2Ujc=a6hx4!A1Th%YH!Lb^xpI!Eu` zmJO{9rw){B1Ql18d%F%da+Tbu1()?o(zT7StYqK6_w`e+fjXq5L^y(0 z09QA6H4oFj59c2wR~{~>jUoDzDdKz}5#onYPJRwa`SUO)Pd4)?(ENBaFVLJr6Kvz= zhTtXqbx09C1z~~iZt;g^9_2nCZ{};-b4dQJbv8HsWHXPVg^@(*!@xycp#R?a|L!+` zY5w))JWV`Gls(=}shH0#r*;~>_+-P5Qc978+QUd>J%`fyn{*TsiG-dWMiJXNgwBaT zJ=wgYFt+1ACW)XwtNx)Q9tA2LPoB&DkL16P)ERWQlY4%Y`-5aM9mZ{eKPUgI!~J3Z zkMd5A_p&v?V-o-6TUa8BndiX?ooviev(DKw=*bBVOW|=zps9=Yl|-R5@yJe*BPzN}a0mUsLn{4LfjB_oxpv(mwq# zSY*%E{iB)sNvWfzg-B!R!|+x(Q|b@>{-~cFvdDHA{F2sFGA5QGiIWy#3?P2JIpPKg6ncI^)dvqe`_|N=8 '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH="\\\"\\\"" - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index db3a6ac..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH= - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/settings.gradle.kts b/settings.gradle.kts deleted file mode 100644 index ae9dc1b..0000000 --- a/settings.gradle.kts +++ /dev/null @@ -1,6 +0,0 @@ -rootProject.name = "ddns" - -pluginManagement.repositories { - maven("https://git.infendro.com/api/packages/Infendro/maven") - mavenCentral() -} diff --git a/src/commonMain/kotlin/com/infendro/ddns/Http.kt b/src/commonMain/kotlin/com/infendro/ddns/Http.kt deleted file mode 100644 index 503ffca..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/Http.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.infendro.ddns - -import io.ktor.client.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.serialization.kotlinx.json.* - -val http = HttpClient { - install(ContentNegotiation) { - json() - } -} diff --git a/src/commonMain/kotlin/com/infendro/ddns/Main.kt b/src/commonMain/kotlin/com/infendro/ddns/Main.kt deleted file mode 100644 index d7d2d3e..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/Main.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.infendro.ddns - -import com.infendro.ddns.command.ddns - -fun main(args: Array) = ddns.run(*args) diff --git a/src/commonMain/kotlin/com/infendro/ddns/command/Parameters.kt b/src/commonMain/kotlin/com/infendro/ddns/command/Parameters.kt deleted file mode 100644 index c059004..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/command/Parameters.kt +++ /dev/null @@ -1,12 +0,0 @@ -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.command.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 deleted file mode 100644 index f13da0e..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/command/ddns.kt +++ /dev/null @@ -1,9 +0,0 @@ -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/parameter/IpService.kt b/src/commonMain/kotlin/com/infendro/ddns/command/parameter/IpService.kt deleted file mode 100644 index 1e7756b..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/command/parameter/IpService.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.infendro.ddns.command.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/command/parameter/Path.kt b/src/commonMain/kotlin/com/infendro/ddns/command/parameter/Path.kt deleted file mode 100644 index 990299e..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/command/parameter/Path.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.infendro.ddns.command.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) diff --git a/src/commonMain/kotlin/com/infendro/ddns/command/porkbun.kt b/src/commonMain/kotlin/com/infendro/ddns/command/porkbun.kt deleted file mode 100644 index 1f39bb4..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/command/porkbun.kt +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index d2c34cd..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/command/update.kt +++ /dev/null @@ -1,35 +0,0 @@ -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.command.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 deleted file mode 100644 index 2f5b683..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/config/Config.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.infendro.ddns.config - -import com.infendro.ddns.dns.DnsService -import com.infendro.ddns.ip.IpService -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -interface Config { - @Serializable - sealed class Entry { - abstract val domain: String - abstract val subdomains: List - abstract val ipService: IpService? - abstract val dnsService: DnsService - - @Serializable - @SerialName("porkbun") - class Porkbun( - val key: String, - val secretKey: String, - override val domain: String, - override val subdomains: List, - @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 deleted file mode 100644 index 648aa0a..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/config/IpServiceSerializer.kt +++ /dev/null @@ -1,25 +0,0 @@ -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/dns/DnsService.kt b/src/commonMain/kotlin/com/infendro/ddns/dns/DnsService.kt deleted file mode 100644 index bab5e8a..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/dns/DnsService.kt +++ /dev/null @@ -1,30 +0,0 @@ -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 updateRecord(record: DnsRecord, value: String) - - fun updateRecords(records: List, value: String) { - for (record in records) { - if (record.value == value) continue - updateRecord(record, value) - } - } - - 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 deleted file mode 100644 index adbd74e..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/dns/model/DnsRecord.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.infendro.ddns.dns.model - -data class DnsRecord( - val id: String, - val type: Type, - val domain: Domain, - val value: String, - val ttl: Int, - val priority: Int?, -) { - data class Domain( - val subdomain: String?, - val domain: String, - ) { - val full: String - get() = when { - subdomain != null -> "$subdomain.$domain" - else -> domain - } - } - - enum class Type { - NS, A, AAAA, CNAME, MX, TXT; - - companion object { - val address: List - get() = listOf(A, AAAA) - } - } -} diff --git a/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/Porkbun.kt b/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/Porkbun.kt deleted file mode 100644 index bffa772..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/Porkbun.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.infendro.ddns.dns.porkbun - -import com.infendro.ddns.dns.DnsService -import com.infendro.ddns.dns.model.DnsRecord -import com.infendro.ddns.dns.porkbun.dto.request.GetRecordsRequest -import com.infendro.ddns.dns.porkbun.dto.request.UpdateRecordRequest -import com.infendro.ddns.dns.porkbun.dto.response.GetRecordsResponse -import com.infendro.ddns.http -import io.ktor.client.call.* -import io.ktor.client.request.* -import io.ktor.http.* -import kotlinx.coroutines.runBlocking - -class Porkbun( - val key: String, - val secretkey: String, -) : DnsService { - 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, - secretapikey = secretkey, - ) - contentType(ContentType.Application.Json) - setBody(request) - } - val body = response.body() - - body.records - .map { - val labels = it.name.split('.') - val subdomain = labels.dropLast(2).joinToString(".") - val domain = labels.takeLast(2).joinToString(".") - - DnsRecord( - id = it.id, - type = DnsRecord.Type.valueOf(it.type), - domain = DnsRecord.Domain( - subdomain = subdomain, - domain = domain, - ), - value = it.content, - ttl = it.ttl.toInt(), - priority = it.prio?.toInt(), - ) - } - .filter { it.type in DnsRecord.Type.address && (it.domain.subdomain ?: "") in subdomains } - } - - 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, - secretapikey = secretkey, - type = record.type.toString(), - name = record.domain.subdomain ?: "", - content = value, - ttl = record.ttl.toString(), - ) - contentType(ContentType.Application.Json) - setBody(request) - } - } -} diff --git a/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/dto/request/GetRecordsRequest.kt b/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/dto/request/GetRecordsRequest.kt deleted file mode 100644 index 7c69011..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/dto/request/GetRecordsRequest.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.infendro.ddns.dns.porkbun.dto.request - -import kotlinx.serialization.Serializable - -@Serializable -data class GetRecordsRequest( - val apikey: String, - val secretapikey: String, -) diff --git a/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/dto/request/UpdateRecordRequest.kt b/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/dto/request/UpdateRecordRequest.kt deleted file mode 100644 index 2a9d001..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/dto/request/UpdateRecordRequest.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.infendro.ddns.dns.porkbun.dto.request - -import kotlinx.serialization.Serializable - -@Serializable -data class UpdateRecordRequest( - val apikey: String, - val secretapikey: String, - val type: String, - val name: String, - val content: String, - val ttl: String, -) diff --git a/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/dto/response/GetRecordsResponse.kt b/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/dto/response/GetRecordsResponse.kt deleted file mode 100644 index a356f33..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/dns/porkbun/dto/response/GetRecordsResponse.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.infendro.ddns.dns.porkbun.dto.response - -import kotlinx.serialization.Serializable - -@Serializable -data class GetRecordsResponse( - val status: String, - val cloudflare: String, - val records: Array, -) { - @Serializable - data class Record( - val id: String, - val type: String, - val name: String, - val content: String, - val ttl: String, - val prio: String?, - val notes: String?, - ) -} diff --git a/src/commonMain/kotlin/com/infendro/ddns/ip/IpService.kt b/src/commonMain/kotlin/com/infendro/ddns/ip/IpService.kt deleted file mode 100644 index 7098ac1..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/ip/IpService.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.infendro.ddns.ip - -interface IpService { - val ipv4: String - val ipv6: String - - val ip: Pair - get() = Pair(ipv4, ipv6) -} diff --git a/src/commonMain/kotlin/com/infendro/ddns/ip/ipify/Ipify.kt b/src/commonMain/kotlin/com/infendro/ddns/ip/ipify/Ipify.kt deleted file mode 100644 index 7619db1..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/ip/ipify/Ipify.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.infendro.ddns.ip.ipify - -import com.infendro.ddns.http -import com.infendro.ddns.ip.IpService -import com.infendro.ddns.ip.ipify.dto.response.GetIpResponse -import io.ktor.client.call.* -import io.ktor.client.request.* -import kotlinx.coroutines.runBlocking - -object Ipify : IpService { - override val ipv4: String - get() = runBlocking { - val response = http.get("https://api4.ipify.org?format=json") - val body = response.body() - body.ip - } - - override val ipv6: String - get() = runBlocking { - val response = http.get("https://api6.ipify.org?format=json") - val body = response.body() - body.ip - } -} diff --git a/src/commonMain/kotlin/com/infendro/ddns/ip/ipify/dto/response/GetIpResponse.kt b/src/commonMain/kotlin/com/infendro/ddns/ip/ipify/dto/response/GetIpResponse.kt deleted file mode 100644 index 2118f34..0000000 --- a/src/commonMain/kotlin/com/infendro/ddns/ip/ipify/dto/response/GetIpResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.infendro.ddns.ip.ipify.dto.response - -import kotlinx.serialization.Serializable - -@Serializable -data class GetIpResponse( - val ip: String, -) diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..471f8b6 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,27 @@ +use serde::Deserialize; +use std::{error::Error, fs}; + +#[derive(Deserialize)] +pub struct Config { + #[serde(flatten)] + pub auth: Auth, + pub entries: Vec, +} + +#[derive(Deserialize)] +pub struct Auth { + pub key: String, + pub secret_key: String, +} + +#[derive(Deserialize)] +pub struct Entry { + pub domain: String, + pub subdomains: Vec, +} + +pub fn load(path: &str) -> Result> { + let raw = fs::read_to_string(&path)?; + let config = serde_json::from_str(&raw)?; + Ok(config) +} diff --git a/src/dns.rs b/src/dns.rs new file mode 100644 index 0000000..be5f7f5 --- /dev/null +++ b/src/dns.rs @@ -0,0 +1,93 @@ +use std::error::Error; + +use reqwest::Client; +use serde::Deserialize; +use serde_json::json; + +use crate::config::Auth; + +pub struct Record { + pub id: String, + pub record_type: String, + pub domain: String, + pub subdomain: String, + pub value: String, + pub ttl: String, +} + +const API: &str = "https://api.porkbun.com/api/json/v3"; + +#[derive(Deserialize)] +struct RecordsResponse { + records: Vec, +} + +#[derive(Deserialize)] +struct RecordResponse { + id: String, + #[serde(rename = "type")] + record_type: String, + name: String, + content: String, + ttl: String, +} + +impl RecordResponse { + fn into_model(self, domain: &str) -> Record { + let subdomain = self.name + .strip_suffix(domain).unwrap() + .strip_suffix(".").unwrap_or(""); + + Record { + id: self.id, + record_type: self.record_type, + domain: domain.to_string(), + subdomain: subdomain.to_string(), + value: self.content, + ttl: self.ttl, + } + } +} + +pub async fn get_records( + client: &Client, + auth: &Auth, + domain: &str, +) -> Result, Box> { + let body = json!({ + "apikey": auth.key, + "secretapikey": auth.secret_key, + }); + let request = client.post(format!("{API}/dns/retrieve/{domain}")) + .json(&body) + .send().await?; + let response = request.json::().await?; + + let records = response.records + .into_iter() + .map(|it| it.into_model(domain)) + .collect(); + + Ok(records) +} + +pub async fn update_record( + client: &Client, + auth: &Auth, + record: &Record, + value: &str, +) -> Result<(), Box> { + let body = json!({ + "apikey": auth.key, + "secretapikey": auth.secret_key, + "type": record.record_type, + "name": record.subdomain, + "content": value, + "ttl": record.ttl, + }); + client.post(format!("{API}/dns/edit/{domain}/{id}", domain = record.domain, id = record.id)) + .json(&body) + .send().await?; + + Ok(()) +} diff --git a/src/ip.rs b/src/ip.rs new file mode 100644 index 0000000..d0ab587 --- /dev/null +++ b/src/ip.rs @@ -0,0 +1,18 @@ +use reqwest::Client; +use std::error::Error; + +pub async fn get_ipv4(client: &Client) -> Result> { + get_ip(client, "https://api4.ipify.org").await +} + +pub async fn get_ipv6(client: &Client) -> Result> { + get_ip(client, "https://api6.ipify.org").await +} + +async fn get_ip(client: &Client, url: &str) -> Result> { + let request = client.get(url) + .send().await? + .error_for_status()?; + let ip = request.text().await?; + Ok(ip) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..51125ec --- /dev/null +++ b/src/main.rs @@ -0,0 +1,52 @@ +use std::error::Error; + +use reqwest::Client; + +use crate::dns::Record; + +mod config; +mod dns; +mod ip; + +const CONFIG: &str = "/etc/ddns.json"; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new(); + + let config = config::load(CONFIG)?; + + let ipv4 = ip::get_ipv4(&client).await.ok(); + let ipv6 = ip::get_ipv6(&client).await.ok(); + + for entry in &config.entries { + let records = dns::get_records(&client, &config.auth, &entry.domain).await?; + + if let Some(ip) = &ipv4 { + let a_records = find_records(&records, "A", &entry.subdomains, ip); + for record in a_records { + dns::update_record(&client, &config.auth, &record, ip).await?; + } + } + + if let Some(ip) = &ipv6 { + let aaaa_records = find_records(&records, "AAAA", &entry.subdomains, ip); + for record in aaaa_records { + dns::update_record(&client, &config.auth, &record, ip).await?; + } + } + } + + Ok(()) +} + +fn find_records<'a>( + records: &'a [Record], + record_type: &str, + subdomains: &[String], + value: &str +) -> Vec<&'a Record> { + records.iter() + .filter(|it| it.record_type == record_type && subdomains.contains(&it.subdomain) && it.value != value) + .collect() +}