diff --git a/src/main/kotlin/dev/usbharu/hideout/config/ActivityPubConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/ActivityPubConfig.kt index 5e6c3016..e9786a1f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/ActivityPubConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/ActivityPubConfig.kt @@ -7,6 +7,8 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.springframework.beans.factory.annotation.Qualifier import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import java.time.format.DateTimeFormatter +import java.util.* @Configuration class ActivityPubConfig { @@ -20,4 +22,8 @@ class ActivityPubConfig { .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) return objectMapper } + + @Bean + @Qualifier("http") + fun dateTimeFormatter(): DateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestService.kt new file mode 100644 index 00000000..4db4561b --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestService.kt @@ -0,0 +1,23 @@ +package dev.usbharu.hideout.service.ap + +import dev.usbharu.hideout.domain.model.ap.Object +import dev.usbharu.hideout.domain.model.hideout.entity.User + +interface APRequestService { + suspend fun apGet(url: String, signer: User? = null, responseClass: Class): R + suspend fun apPost( + url: String, + body: T? = null, + signer: User? = null, + responseClass: Class + ): R +} + +suspend inline fun APRequestService.apGet(url: String, signer: User? = null): R = + apGet(url, signer, R::class.java) + +suspend inline fun APRequestService.apPost( + url: String, + body: T? = null, + signer: User? = null +): R = apPost(url, body, signer, R::class.java) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt new file mode 100644 index 00000000..605184e1 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt @@ -0,0 +1,121 @@ +package dev.usbharu.hideout.service.ap + +import com.fasterxml.jackson.databind.ObjectMapper +import dev.usbharu.hideout.domain.model.ap.Object +import dev.usbharu.hideout.domain.model.hideout.entity.User +import dev.usbharu.hideout.service.signature.HttpSignatureSigner +import dev.usbharu.hideout.service.signature.Key +import dev.usbharu.hideout.util.Base64Util +import dev.usbharu.hideout.util.HttpUtil.Activity +import dev.usbharu.hideout.util.RsaUtil +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Service +import java.net.URL +import java.security.MessageDigest +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +class APRequestServiceImpl( + private val httpClient: HttpClient, + @Qualifier("activitypub") private val objectMapper: ObjectMapper, + private val httpSignatureSigner: HttpSignatureSigner, + @Qualifier("http") private val dateTimeFormatter: DateTimeFormatter, +) : APRequestService { + + override suspend fun apGet(url: String, signer: User?, responseClass: Class): R { + + val date = dateTimeFormatter.format(LocalDateTime.now(ZoneId.of("GMT"))) + val u = URL(url) + if (signer?.privateKey == null) { + val bodyAsText = httpClient.get(url) { + header("Accept", ContentType.Application.Activity) + header("Date", date) + }.bodyAsText() + return objectMapper.readValue(bodyAsText, responseClass) + } + + val headers = headers { + append("Accept", ContentType.Application.Activity) + append("Date", date) + append("Host", u.host) + } + + val sign = httpSignatureSigner.sign( + url, HttpMethod.Get, headers, "", Key( + keyId = "${signer.url}#pubkey", + privateKey = RsaUtil.decodeRsaPrivateKey(signer.privateKey), + publicKey = RsaUtil.decodeRsaPublicKey(signer.publicKey) + ), listOf("(request-target)", "date", "host", "accept") + ) + + val bodyAsText = httpClient.get(url) { + headers { + headers { + appendAll(sign.headers) + remove("Host") + } + } + }.bodyAsText() + return objectMapper.readValue(bodyAsText, responseClass) + } + + override suspend fun apPost( + url: String, + body: T?, + signer: User?, + responseClass: Class + ): R { + + val requestBody = objectMapper.writeValueAsString(body) + + val sha256 = MessageDigest.getInstance("SHA-256") + + val digest = Base64Util.encode(sha256.digest(requestBody.toByteArray())) + + val date = dateTimeFormatter.format(LocalDateTime.now(ZoneId.of("GMT"))) + val u = URL(url) + if (signer?.privateKey == null) { + val bodyAsText = httpClient.post(url) { + header("Accept", ContentType.Application.Activity) + header("ContentType", ContentType.Application.Activity) + header("Date", date) + header("Digest", digest) + setBody(requestBody) + }.bodyAsText() + return objectMapper.readValue(bodyAsText, responseClass) + } + + val headers = headers { + append("Accept", ContentType.Application.Activity) + append("ContentType", ContentType.Application.Activity) + append("Date", date) + append("Host", u.host) + append("Digest", digest) + } + + val sign = httpSignatureSigner.sign( + url, HttpMethod.Get, headers, "", Key( + keyId = "${signer.url}#pubkey", + privateKey = RsaUtil.decodeRsaPrivateKey(signer.privateKey), + publicKey = RsaUtil.decodeRsaPublicKey(signer.publicKey) + ), listOf("(request-target)", "date", "host", "digest") + ) + + val bodyAsText = httpClient.post(url) { + headers { + headers { + appendAll(sign.headers) + remove("Host") + } + } + setBody(requestBody) + }.bodyAsText() + return objectMapper.readValue(bodyAsText, responseClass) + } +}