initial commit
This commit is contained in:
13
src/commonMain/kotlin/com/infendro/ddns/Http.kt
Normal file
13
src/commonMain/kotlin/com/infendro/ddns/Http.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.infendro.ddns
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
|
||||
object Http {
|
||||
val client = HttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/commonMain/kotlin/com/infendro/ddns/Main.kt
Normal file
110
src/commonMain/kotlin/com/infendro/ddns/Main.kt
Normal file
@@ -0,0 +1,110 @@
|
||||
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.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.DnsServices.PORKBUN
|
||||
import com.infendro.ddns.dns.model.DnsRecord
|
||||
import com.infendro.ddns.dns.porkbun.Porkbun
|
||||
import com.infendro.ddns.ip.IpService
|
||||
|
||||
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<String>,
|
||||
) {
|
||||
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<String>,
|
||||
) {
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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.ddns.ip.IpService
|
||||
import com.infendro.ddns.ip.IpServices
|
||||
|
||||
object IpServiceParser : Parser<IpService> {
|
||||
override fun parse(
|
||||
text: String?,
|
||||
): IpService {
|
||||
return IpServices.get(text!!)
|
||||
}
|
||||
}
|
||||
|
||||
class IpServiceOrDefaultArgument(
|
||||
override val name: String,
|
||||
) : Argument.SingleOrElse<IpService>() {
|
||||
override val other: IpService
|
||||
get() = IpServices.DEFAULT
|
||||
|
||||
override val parser: Parser<IpService>
|
||||
get() = IpServiceParser
|
||||
}
|
||||
|
||||
fun ipService(
|
||||
name: String,
|
||||
) = IpServiceOrDefaultArgument(name)
|
||||
|
||||
fun Command.Builder.ipService(
|
||||
name: String,
|
||||
) = IpServiceOrDefaultArgument(name)
|
||||
.also { argument(it) }
|
||||
128
src/commonMain/kotlin/com/infendro/ddns/config/Config.kt
Normal file
128
src/commonMain/kotlin/com/infendro/ddns/config/Config.kt
Normal file
@@ -0,0 +1,128 @@
|
||||
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
|
||||
|
||||
class Config(
|
||||
val services: List<Service>,
|
||||
) {
|
||||
class Service(
|
||||
val name: String,
|
||||
val arguments: List<Argument>,
|
||||
) {
|
||||
class Argument(
|
||||
val name: String,
|
||||
val value: String,
|
||||
)
|
||||
|
||||
operator fun <T> Single<T>.getValue(
|
||||
thisRef: Any?,
|
||||
property: KProperty<*>,
|
||||
): T {
|
||||
return parser.parse(
|
||||
arguments
|
||||
.find { it.name == name }!!
|
||||
.value
|
||||
)
|
||||
}
|
||||
|
||||
operator fun <T> SingleOrElse<T>.getValue(
|
||||
thisRef: Any?,
|
||||
property: KProperty<*>,
|
||||
): T {
|
||||
return arguments
|
||||
.find { it.name == name }
|
||||
?.let { parser.parse(it.value) }
|
||||
?: other
|
||||
}
|
||||
|
||||
operator fun <T> SingleOrNull<T>.getValue(
|
||||
thisRef: Any?,
|
||||
property: KProperty<*>,
|
||||
): T? {
|
||||
return arguments
|
||||
.find { it.name == name }
|
||||
?.let { parser.parse(it.value) }
|
||||
}
|
||||
|
||||
operator fun <T> Multiple<T>.getValue(
|
||||
thisRef: Any?,
|
||||
property: KProperty<*>,
|
||||
): List<T> {
|
||||
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..<comment)
|
||||
.trim()
|
||||
)
|
||||
} else {
|
||||
builder.appendLine(
|
||||
line.trim()
|
||||
)
|
||||
}
|
||||
}
|
||||
return builder.toString().trim()
|
||||
}
|
||||
|
||||
private fun parse(
|
||||
text: String,
|
||||
): Config {
|
||||
val services = text
|
||||
.split(EMPTY_LINES)
|
||||
.map { parseService(it) }
|
||||
|
||||
return Config(services)
|
||||
}
|
||||
|
||||
private fun parseService(
|
||||
text: String,
|
||||
): Service {
|
||||
val lines = text.lines()
|
||||
val name = lines[0]
|
||||
val arguments = lines
|
||||
.drop(1)
|
||||
.map { parseArgument(it) }
|
||||
|
||||
return Service(name, arguments)
|
||||
}
|
||||
|
||||
private fun parseArgument(
|
||||
text: String,
|
||||
): Service.Argument {
|
||||
val match = ARGUMENT.find(text)!!
|
||||
val name = match.groups["name"]!!.value
|
||||
val value = match.groups["value"]!!.value
|
||||
|
||||
return Service.Argument(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/commonMain/kotlin/com/infendro/ddns/config/Regex.kt
Normal file
6
src/commonMain/kotlin/com/infendro/ddns/config/Regex.kt
Normal file
@@ -0,0 +1,6 @@
|
||||
package com.infendro.ddns.config
|
||||
|
||||
object Regex {
|
||||
val EMPTY_LINES = Regex("""\n{2,}""")
|
||||
val ARGUMENT = Regex("""(?<name>[a-z]+(?:-[a-z]+)*)(?:=(?<value>.*))?""")
|
||||
}
|
||||
25
src/commonMain/kotlin/com/infendro/ddns/dns/DnsService.kt
Normal file
25
src/commonMain/kotlin/com/infendro/ddns/dns/DnsService.kt
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.infendro.ddns.dns
|
||||
|
||||
import com.infendro.ddns.dns.model.DnsRecord
|
||||
|
||||
abstract class DnsService {
|
||||
abstract fun getRecords(
|
||||
domain: String,
|
||||
subdomains: List<String>,
|
||||
): List<DnsRecord>
|
||||
|
||||
abstract fun updateRecord(
|
||||
record: DnsRecord,
|
||||
value: String,
|
||||
)
|
||||
|
||||
fun updateRecords(
|
||||
records: List<DnsRecord>,
|
||||
value: String,
|
||||
) {
|
||||
for (record in records) {
|
||||
if (record.value == value) continue
|
||||
updateRecord(record, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.infendro.ddns.dns
|
||||
|
||||
object DnsServices {
|
||||
const val PORKBUN = "porkbun"
|
||||
}
|
||||
5
src/commonMain/kotlin/com/infendro/ddns/dns/Regex.kt
Normal file
5
src/commonMain/kotlin/com/infendro/ddns/dns/Regex.kt
Normal file
@@ -0,0 +1,5 @@
|
||||
package com.infendro.ddns.dns
|
||||
|
||||
object Regex {
|
||||
val DOMAIN = Regex("""(?:(?<subdomain>(?:\w+\.)*\w+)\.)?(?<domain>\w+\.\w+)""")
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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() = if (subdomain != null) {
|
||||
"$subdomain.$domain"
|
||||
} else {
|
||||
domain
|
||||
}
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
NS, A, AAAA, CNAME, MX, TXT;
|
||||
|
||||
companion object {
|
||||
val address: List<Type>
|
||||
get() = listOf(A, AAAA)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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 kotlinx.coroutines.runBlocking
|
||||
|
||||
class Porkbun(
|
||||
val key: String,
|
||||
val secretkey: String,
|
||||
) : DnsService() {
|
||||
override fun getRecords(
|
||||
domain: String,
|
||||
subdomains: List<String>,
|
||||
): List<DnsRecord> = runBlocking {
|
||||
val response = client.post("https://api.porkbun.com/api/json/v3/dns/retrieve/$domain") {
|
||||
val request = getRecordsRequest()
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
val body = response.body<GetRecordsResponse>()
|
||||
|
||||
body.records
|
||||
.map { model(it) }
|
||||
.filter { it.type in DnsRecord.Type.address && (it.domain.subdomain ?: "") in subdomains }
|
||||
}
|
||||
|
||||
override fun updateRecord(
|
||||
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)
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.infendro.ddns.dns.porkbun.dto.request
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetRecordsRequest(
|
||||
val apikey: String,
|
||||
val secretapikey: String,
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
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<Record>,
|
||||
) {
|
||||
@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?,
|
||||
)
|
||||
}
|
||||
9
src/commonMain/kotlin/com/infendro/ddns/ip/IpService.kt
Normal file
9
src/commonMain/kotlin/com/infendro/ddns/ip/IpService.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package com.infendro.ddns.ip
|
||||
|
||||
abstract class IpService {
|
||||
abstract val ipv4: String
|
||||
abstract val ipv6: String
|
||||
|
||||
val ip: List<String>
|
||||
get() = listOf(ipv4, ipv6)
|
||||
}
|
||||
17
src/commonMain/kotlin/com/infendro/ddns/ip/IpServices.kt
Normal file
17
src/commonMain/kotlin/com/infendro/ddns/ip/IpServices.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/commonMain/kotlin/com/infendro/ddns/ip/ipify/Ipify.kt
Normal file
24
src/commonMain/kotlin/com/infendro/ddns/ip/ipify/Ipify.kt
Normal file
@@ -0,0 +1,24 @@
|
||||
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.body
|
||||
import io.ktor.client.request.get
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
object Ipify : IpService() {
|
||||
override val ipv4: String
|
||||
get() = runBlocking {
|
||||
val response = Http.client.get("https://api.ipify.org?format=json")
|
||||
val body = response.body<GetIpResponse>()
|
||||
body.ip
|
||||
}
|
||||
|
||||
override val ipv6: String
|
||||
get() = runBlocking {
|
||||
val response = Http.client.get("https://api64.ipify.org?format=json")
|
||||
val body = response.body<GetIpResponse>()
|
||||
body.ip
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.infendro.ddns.ip.ipify.dto.response
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetIpResponse(
|
||||
val ip: String,
|
||||
)
|
||||
Reference in New Issue
Block a user