diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7856eba..f685672 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,6 @@ name: "Build" on: - - "push" + - push jobs: build: name: "Build on JDK ${{ matrix.jdk }}" @@ -8,44 +8,45 @@ jobs: strategy: matrix: jdk: - - 17 + - 21 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: "Set up JDK ${{ matrix.jdk }}" - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: "${{ matrix.jdk }}" distribution: "zulu" - name: "Build with Gradle" run: ./gradlew check distTar - name: "Upload Artifact" - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: "Kotlin Application" path: "build/distributions/ddns.tar" retention-days: 1 build-and-push-image: + if: ${{ github.ref == 'refs/heads/main' }} runs-on: ubuntu-latest needs: build steps: - name: "Checkout" - uses: actions/checkout@v3 - - uses: actions/download-artifact@v3 + uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 with: name: "Kotlin Application" path: "build/distributions/" - name: "Untar files" run: mkdir -p build/install && tar -xvf build/distributions/ddns.tar -C $_ - name: "Set up Docker Buildx" - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: "Login to GHCR" - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: "Build and push" - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: . push: true diff --git a/Dockerfile b/Dockerfile index b1588aa..82b8ccf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Container with application -FROM bellsoft/liberica-openjre-alpine:17.0.5 +FROM bellsoft/liberica-openjre-alpine:21.0.3 RUN apk --no-cache add curl COPY /build/install/ddns /ddns ENTRYPOINT /ddns/bin/ddns diff --git a/build.gradle.kts b/build.gradle.kts index 2668472..ad0aeab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ plugins { application - kotlin("jvm").version("1.9.22") - kotlin("plugin.serialization").version("1.9.22") + kotlin("jvm").version("2.0.0-RC3") + kotlin("plugin.serialization").version("2.0.0-RC3") } application { @@ -14,18 +14,18 @@ repositories { } dependencies { - implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.7") + implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.11") - implementation("io.ktor:ktor-client-cio:2.3.7") - implementation("io.ktor:ktor-client-content-negotiation:2.3.7") + implementation("io.ktor:ktor-client-cio:2.3.11") + implementation("io.ktor:ktor-client-content-negotiation:2.3.11") - implementation("io.ktor:ktor-server-cio:2.3.7") - implementation("io.ktor:ktor-server-content-negotiation:2.3.7") - implementation("io.ktor:ktor-server-call-logging:2.3.7") + implementation("io.ktor:ktor-server-cio:2.3.11") + implementation("io.ktor:ktor-server-content-negotiation:2.3.11") + implementation("io.ktor:ktor-server-call-logging:2.3.11") - implementation("ch.qos.logback:logback-classic:1.4.14") + implementation("ch.qos.logback:logback-classic:1.5.6") - testImplementation("io.ktor:ktor-server-tests:2.3.7") + testImplementation("io.ktor:ktor-server-tests:2.3.11") testImplementation(kotlin("test-junit")) } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd49..e644113 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index db8c3ba..381baa9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..25da30d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +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 @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +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 diff --git a/src/main/kotlin/io/heapy/ddns/Client.kt b/src/main/kotlin/io/heapy/ddns/Client.kt index eebcfd3..673deb5 100644 --- a/src/main/kotlin/io/heapy/ddns/Client.kt +++ b/src/main/kotlin/io/heapy/ddns/Client.kt @@ -1,6 +1,7 @@ package io.heapy.ddns import io.heapy.ddns.dns_clients.CloudflareDnsClient +import io.heapy.ddns.dns_clients.CloudflareVerifyToken import io.heapy.ddns.dns_clients.DigitalOceanDnsClient import io.heapy.ddns.dns_clients.DnsClient import io.heapy.ddns.ip_provider.IpProvider @@ -78,22 +79,33 @@ open class ClientFactory( ) } - open val cloudflareDnsClient: DnsClient by lazy { + open val cloudflareDnsClient: CloudflareDnsClient by lazy { CloudflareDnsClient( httpClient = httpClient, configuration = cloudflareConfiguration, + verifyToken = cloudflareVerifyToken, ) } + open val cloudflareVerifyToken: CloudflareVerifyToken by lazy { + CloudflareVerifyToken( + httpClient = httpClient, + ) + } + + open val cloudflareToken by lazy { + config["CLOUDFLARE_TOKEN"] + ?: error("CLOUDFLARE_TOKEN is not set") + } + open val cloudflareConfiguration: CloudflareDnsClient.Configuration by lazy { CloudflareDnsClient.Configuration( zoneId = config["CLOUDFLARE_ZONE_ID"] ?: error("CLOUDFLARE_ZONE_ID is not set"), - token = config["CLOUDFLARE_TOKEN"] - ?: error("CLOUDFLARE_TOKEN is not set"), domainName = config["CLOUDFLARE_DOMAIN_NAME"] ?: error("CLOUDFLARE_DOMAIN_NAME is not set"), - ttl = configuration.checkPeriod.inWholeSeconds, + token = cloudflareToken, + ttl = checkPeriod.inWholeSeconds, ) } @@ -113,12 +125,16 @@ open class ClientFactory( ) } + open val checkPeriod by lazy { + config["CHECK_PERIOD"]?.let(Duration::parse) + ?: 5.minutes + } + open val configuration by lazy { Configuration( serverUrl = config["SERVER_URL"] ?: error("SERVER_URL is not set"), - checkPeriod = config["CHECK_PERIOD"]?.let(Duration::parse) - ?: 5.minutes, + checkPeriod = checkPeriod, requestTimeout = config["REQUEST_TIMEOUT"]?.let(Duration::parse) ?: 30.seconds, attemptsBeforeWarning = config["ATTEMPTS_BEFORE_WARNING"]?.toInt() ?: 5, diff --git a/src/main/kotlin/io/heapy/ddns/dns_clients/CloudflareDnsClient.kt b/src/main/kotlin/io/heapy/ddns/dns_clients/CloudflareDnsClient.kt index 18d20f1..749a285 100644 --- a/src/main/kotlin/io/heapy/ddns/dns_clients/CloudflareDnsClient.kt +++ b/src/main/kotlin/io/heapy/ddns/dns_clients/CloudflareDnsClient.kt @@ -12,10 +12,18 @@ import java.time.OffsetDateTime class CloudflareDnsClient( private val httpClient: HttpClient, private val configuration: Configuration, + private val verifyToken: CloudflareVerifyToken, ) : DnsClient { override suspend fun createOrUpdateRecord( ip: String, ): String? { + val verifyTokenResponse = verifyToken(configuration.token) + + if (!verifyTokenResponse.success) { + log.error("Token verification failed: ${verifyTokenResponse.errors}") + return null + } + val record = getRecord( name = configuration.domainName, zoneId = configuration.zoneId, @@ -112,7 +120,7 @@ class CloudflareDnsClient( return "$type by io.heapy.ddns-fullstack on ${OffsetDateTime.now()}" } - private suspend fun getRecord( + internal suspend fun getRecord( name: String, zoneId: String, token: String, @@ -187,7 +195,7 @@ class CloudflareDnsClient( val managedByApps: Boolean, @SerialName("managed_by_argo_tunnel") val managedByArgoTunnel: Boolean, - val source: String, + val source: String? = null, ) } diff --git a/src/main/kotlin/io/heapy/ddns/dns_clients/CloudflareVerifyToken.kt b/src/main/kotlin/io/heapy/ddns/dns_clients/CloudflareVerifyToken.kt new file mode 100644 index 0000000..ef491e8 --- /dev/null +++ b/src/main/kotlin/io/heapy/ddns/dns_clients/CloudflareVerifyToken.kt @@ -0,0 +1,49 @@ +package io.heapy.ddns.dns_clients + +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import kotlinx.serialization.Serializable + +class CloudflareVerifyToken( + private val httpClient: HttpClient, +) { + suspend operator fun invoke( + token: String, + ): VerifyTokenResponse { + return httpClient + .get("https://api.cloudflare.com/client/v4/user/tokens/verify") { + header("Content-Type", "application/json") + bearerAuth(token) + } + .body() + } + + @Serializable + data class VerifyTokenResponse( + val result: Result? = null, + val success: Boolean, + val errors: List, + val messages: List, + ) { + @Serializable + data class Result( + val id: String, + val status: String, + ) + + @Serializable + data class Error( + val code: Int, + val message: String, + ) + + @Serializable + data class Message( + val code: Int, + val message: String, + val type: String? = null, + ) + } + +} diff --git a/src/test/kotlin/io/heapy/ddns/dns_clients/CloudflareClientFactoryTest.kt b/src/test/kotlin/io/heapy/ddns/dns_clients/CloudflareClientFactoryTest.kt new file mode 100644 index 0000000..a6704b5 --- /dev/null +++ b/src/test/kotlin/io/heapy/ddns/dns_clients/CloudflareClientFactoryTest.kt @@ -0,0 +1,38 @@ +package io.heapy.ddns.dns_clients + +import io.heapy.ddns.ClientFactory +import kotlinx.coroutines.runBlocking +import org.junit.Test +import kotlin.test.assertEquals + +class CloudflareClientFactoryTest { + @Test + fun `verify test token`() = runBlocking { + val factory = ClientFactory(System.getenv()) + val token = factory.cloudflareToken + + val response = factory.cloudflareVerifyToken(token) + + assertEquals( + response.success, + true, + ) + } + + @Test + fun `get dns record`() = runBlocking { + val factory = ClientFactory(System.getenv()) + val config = factory.cloudflareConfiguration + + val record = factory.cloudflareDnsClient.getRecord( + name = config.domainName, + zoneId = config.zoneId, + token = config.token, + ) + + assertEquals( + record?.name, + config.domainName, + ) + } +}