diff --git a/src/main/kotlin/dev/usbharu/hideout/config/HttpClientConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/HttpClientConfig.kt index f373805c..3332c81d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/HttpClientConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/HttpClientConfig.kt @@ -1,7 +1,6 @@ package dev.usbharu.hideout.config import dev.usbharu.hideout.plugins.KtorKeyMap -import dev.usbharu.hideout.plugins.httpSignaturePlugin import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.service.core.Transaction import io.ktor.client.* @@ -16,9 +15,6 @@ import tech.barbero.http.message.signing.KeyMap class HttpClientConfig { @Bean fun httpClient(keyMap: KeyMap): HttpClient = HttpClient(CIO).config { - install(httpSignaturePlugin) { - this.keyMap = keyMap - } install(Logging) { logger = Logger.DEFAULT level = LogLevel.INFO diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/ActivityPub.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/ActivityPub.kt index 01ed970e..6b235333 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/ActivityPub.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/ActivityPub.kt @@ -1,16 +1,11 @@ package dev.usbharu.hideout.plugins -import com.fasterxml.jackson.databind.ObjectMapper import dev.usbharu.hideout.config.ApplicationConfig -import dev.usbharu.hideout.domain.model.ap.JsonLd import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.service.core.Transaction import dev.usbharu.hideout.service.user.UserAuthServiceImpl -import dev.usbharu.hideout.util.HttpUtil.Activity -import io.ktor.client.* import io.ktor.client.plugins.api.* import io.ktor.client.request.* -import io.ktor.client.statement.* import io.ktor.http.* import kotlinx.coroutines.runBlocking import tech.barbero.http.message.signing.HttpMessage @@ -27,31 +22,6 @@ import java.text.SimpleDateFormat import java.util.* import javax.crypto.SecretKey -suspend fun HttpClient.postAp( - urlString: String, - username: String, - jsonLd: JsonLd, - objectMapper: ObjectMapper -): HttpResponse { - jsonLd.context += "https://www.w3.org/ns/activitystreams" - return this.post(urlString) { - header("Accept", ContentType.Application.Activity) - header("Content-Type", ContentType.Application.Activity) - header("Signature", "keyId=\"$username\",algorithm=\"rsa-sha256\",headers=\"(request-target) digest date\"") - val text = objectMapper.writeValueAsString(jsonLd) - setBody(text) - } -} - -suspend fun HttpClient.getAp(urlString: String, username: String?): HttpResponse { - return this.get(urlString) { - header("Accept", ContentType.Application.Activity) - username?.let { - header("Signature", "keyId=\"$username\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date\"") - } - } -} - class HttpSignaturePluginConfig { lateinit var keyMap: KeyMap } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt index 440eba4e..071e6930 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt @@ -12,7 +12,6 @@ import dev.usbharu.hideout.domain.model.job.DeliverPostJob import dev.usbharu.hideout.exception.FailedToGetResourcesException import dev.usbharu.hideout.exception.ap.FailedToGetActivityPubResourceException import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException -import dev.usbharu.hideout.plugins.postAp import dev.usbharu.hideout.query.FollowerQueryService import dev.usbharu.hideout.query.MediaQueryService import dev.usbharu.hideout.query.PostQueryService @@ -20,6 +19,7 @@ import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.repository.PostRepository import dev.usbharu.hideout.service.ap.resource.APResourceResolveService import dev.usbharu.hideout.service.ap.resource.resolve +import dev.usbharu.hideout.service.core.Transaction import dev.usbharu.hideout.service.job.JobQueueParentService import dev.usbharu.hideout.service.post.PostCreateInterceptor import dev.usbharu.hideout.service.post.PostService @@ -69,7 +69,9 @@ class APNoteServiceImpl( @Qualifier("activitypub") private val objectMapper: ObjectMapper, private val applicationConfig: ApplicationConfig, private val postService: PostService, - private val apResourceResolveService: APResourceResolveService + private val apResourceResolveService: APResourceResolveService, + private val apRequestService: APRequestService, + private val transaction: Transaction ) : APNoteService, PostCreateInterceptor { @@ -121,17 +123,20 @@ class APNoteServiceImpl( ) val inbox = props[DeliverPostJob.inbox] logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox) - httpClient.postAp( - urlString = inbox, - username = "$actor#pubkey", - jsonLd = Create( - name = "Create Note", - `object` = note, - actor = note.attributedTo, - id = "${applicationConfig.url}/create/note/${postEntity.id}" - ), - objectMapper - ) + + transaction.transaction { + val signer = userQueryService.findByUrl(actor) + apRequestService.apPost( + inbox, Create( + name = "Create Note", + `object` = note, + actor = note.attributedTo, + id = "${applicationConfig.url}/create/note/${postEntity.id}" + ), signer + ) + } + + } override suspend fun fetchNote(url: String, targetActor: String?): Note { diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionService.kt index d4c1dfd1..1833b08d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReactionService.kt @@ -8,7 +8,6 @@ import dev.usbharu.hideout.domain.model.ap.Undo import dev.usbharu.hideout.domain.model.hideout.entity.Reaction import dev.usbharu.hideout.domain.model.job.DeliverReactionJob import dev.usbharu.hideout.domain.model.job.DeliverRemoveReactionJob -import dev.usbharu.hideout.plugins.postAp import dev.usbharu.hideout.query.FollowerQueryService import dev.usbharu.hideout.query.PostQueryService import dev.usbharu.hideout.query.UserQueryService @@ -34,8 +33,8 @@ class APReactionServiceImpl( private val followerQueryService: FollowerQueryService, private val postQueryService: PostQueryService, @Qualifier("activitypub") private val objectMapper: ObjectMapper, - private val applicationConfig: ApplicationConfig - + private val applicationConfig: ApplicationConfig, + private val apRequestService: APRequestService ) : APReactionService { override suspend fun reaction(like: Reaction) { val followers = followerQueryService.findFollowersById(like.userId) @@ -74,17 +73,17 @@ class APReactionServiceImpl( val postUrl = props[DeliverReactionJob.postUrl] val id = props[DeliverReactionJob.id] val content = props[DeliverReactionJob.reaction] - httpClient.postAp( - urlString = inbox, - username = "$actor#pubkey", - jsonLd = Like( + + val signer = userQueryService.findByUrl(actor) + + apRequestService.apPost( + inbox, Like( name = "Like", actor = actor, `object` = postUrl, id = "${applicationConfig.url}/like/note/$id", content = content - ), - objectMapper + ), signer ) } @@ -92,17 +91,17 @@ class APReactionServiceImpl( val inbox = props[DeliverRemoveReactionJob.inbox] val actor = props[DeliverRemoveReactionJob.actor] val like = objectMapper.readValue(props[DeliverRemoveReactionJob.like]) - httpClient.postAp( - urlString = inbox, - username = "$actor#pubkey", - jsonLd = Undo( + + val signer = userQueryService.findByUrl(actor) + + apRequestService.apPost( + inbox, Undo( name = "Undo Reaction", actor = actor, `object` = like, id = "${applicationConfig.url}/undo/note/${like.id}", published = Instant.now() - ), - objectMapper + ), signer ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowService.kt index 13b95b7d..28f277da 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APReceiveFollowService.kt @@ -7,7 +7,6 @@ import dev.usbharu.hideout.domain.model.ActivityPubStringResponse import dev.usbharu.hideout.domain.model.ap.Accept import dev.usbharu.hideout.domain.model.ap.Follow import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob -import dev.usbharu.hideout.plugins.postAp import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.service.core.Transaction import dev.usbharu.hideout.service.job.JobQueueParentService @@ -31,7 +30,8 @@ class APReceiveFollowServiceImpl( private val httpClient: HttpClient, private val userQueryService: UserQueryService, private val transaction: Transaction, - @Qualifier("activitypub") private val objectMapper: ObjectMapper + @Qualifier("activitypub") private val objectMapper: ObjectMapper, + private val apRequestService: APRequestService ) : APReceiveFollowService { override suspend fun receiveFollow(follow: Follow): ActivityPubResponse { // TODO: Verify HTTP Signature @@ -50,15 +50,17 @@ class APReceiveFollowServiceImpl( val targetActor = props[ReceiveFollowJob.targetActor] val person = apUserService.fetchPerson(actor, targetActor) val follow = objectMapper.readValue(props[ReceiveFollowJob.follow]) - httpClient.postAp( - urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found"), - username = "$targetActor#pubkey", - jsonLd = Accept( + + val signer = userQueryService.findByUrl(actor) + + val urlString = person.inbox ?: throw IllegalArgumentException("inbox is not found") + + apRequestService.apPost( + urlString, Accept( name = "Follow", `object` = follow, actor = targetActor - ), - objectMapper + ), signer ) val targetEntity = userQueryService.findByUrl(targetActor) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestService.kt index 4db4561b..be5c5796 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestService.kt @@ -11,6 +11,8 @@ interface APRequestService { signer: User? = null, responseClass: Class ): R + + suspend fun apPost(url: String, body: T? = null, signer: User? = null): String } suspend inline fun APRequestService.apGet(url: String, signer: User? = null): R = diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt index 605184e1..171066d2 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APRequestServiceImpl.kt @@ -16,8 +16,8 @@ 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.ZonedDateTime import java.time.format.DateTimeFormatter @Service @@ -30,7 +30,7 @@ class APRequestServiceImpl( override suspend fun apGet(url: String, signer: User?, responseClass: Class): R { - val date = dateTimeFormatter.format(LocalDateTime.now(ZoneId.of("GMT"))) + val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT"))) val u = URL(url) if (signer?.privateKey == null) { val bodyAsText = httpClient.get(url) { @@ -71,6 +71,11 @@ class APRequestServiceImpl( signer: User?, responseClass: Class ): R { + val bodyAsText = apPost(url, body, signer) + return objectMapper.readValue(bodyAsText, responseClass) + } + + override suspend fun apPost(url: String, body: T?, signer: User?): String { val requestBody = objectMapper.writeValueAsString(body) @@ -78,17 +83,16 @@ class APRequestServiceImpl( val digest = Base64Util.encode(sha256.digest(requestBody.toByteArray())) - val date = dateTimeFormatter.format(LocalDateTime.now(ZoneId.of("GMT"))) + val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT"))) val u = URL(url) if (signer?.privateKey == null) { - val bodyAsText = httpClient.post(url) { + return 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 { @@ -107,7 +111,7 @@ class APRequestServiceImpl( ), listOf("(request-target)", "date", "host", "digest") ) - val bodyAsText = httpClient.post(url) { + return httpClient.post(url) { headers { headers { appendAll(sign.headers) @@ -116,6 +120,5 @@ class APRequestServiceImpl( } setBody(requestBody) }.bodyAsText() - return objectMapper.readValue(bodyAsText, responseClass) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowService.kt index 97d50c5d..91fe53b9 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APSendFollowService.kt @@ -3,8 +3,6 @@ package dev.usbharu.hideout.service.ap import com.fasterxml.jackson.databind.ObjectMapper import dev.usbharu.hideout.domain.model.ap.Follow import dev.usbharu.hideout.domain.model.hideout.dto.SendFollowDto -import dev.usbharu.hideout.plugins.postAp -import io.ktor.client.* import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Service @@ -14,8 +12,8 @@ interface APSendFollowService { @Service class APSendFollowServiceImpl( - private val httpClient: HttpClient, @Qualifier("activitypub") private val objectMapper: ObjectMapper, + private val apRequestService: APRequestService, ) : APSendFollowService { override suspend fun sendFollow(sendFollowDto: SendFollowDto) { val follow = Follow( @@ -23,11 +21,7 @@ class APSendFollowServiceImpl( `object` = sendFollowDto.followTargetUserId.url, actor = sendFollowDto.userId.url ) - httpClient.postAp( - urlString = sendFollowDto.followTargetUserId.inbox, - username = sendFollowDto.userId.url, - jsonLd = follow, - objectMapper - ) + + apRequestService.apPost(sendFollowDto.followTargetUserId.inbox, follow, sendFollowDto.userId) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImpl.kt index 12c6b4d5..90d648f9 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/resource/APResourceResolveServiceImpl.kt @@ -4,17 +4,13 @@ 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.repository.UserRepository -import dev.usbharu.hideout.util.HttpUtil.Activity -import io.ktor.client.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* +import dev.usbharu.hideout.service.ap.APRequestService import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Service @Service class APResourceResolveServiceImpl( - private val httpClient: HttpClient, + private val apRequestService: APRequestService, private val userRepository: UserRepository, private val cacheManager: CacheManager, @Qualifier("activitypub") private val objectMapper: ObjectMapper @@ -48,10 +44,7 @@ class APResourceResolveServiceImpl( } private suspend fun runResolve(url: String, singer: User?, clazz: Class): Object { - val bodyAsText = httpClient.get(url) { - header("Accept", ContentType.Application.Activity) - }.bodyAsText() - return objectMapper.readValue(bodyAsText, clazz) + return apRequestService.apGet(url, singer, clazz) } private fun genCacheKey(url: String, singerId: Long?): String { diff --git a/src/test/kotlin/dev/usbharu/hideout/plugins/ActivityPubKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/plugins/ActivityPubKtTest.kt deleted file mode 100644 index 52ac0b6b..00000000 --- a/src/test/kotlin/dev/usbharu/hideout/plugins/ActivityPubKtTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -package dev.usbharu.hideout.plugins - -import dev.usbharu.hideout.domain.model.ap.JsonLd -import dev.usbharu.hideout.domain.model.hideout.entity.User -import dev.usbharu.hideout.query.UserQueryService -import dev.usbharu.hideout.service.user.toPem -import io.ktor.client.* -import io.ktor.client.engine.mock.* -import io.ktor.client.plugins.logging.* -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Test -import org.mockito.kotlin.any -import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.mock -import utils.JsonObjectMapper.objectMapper -import utils.TestApplicationConfig.testApplicationConfig -import utils.TestTransaction -import java.security.KeyPairGenerator -import java.time.Instant - -class ActivityPubKtTest { - @Test - fun HttpSignTest() { - val userQueryService = mock { - onBlocking { findByNameAndDomain(any(), any()) } doAnswer { - val keyPairGenerator = KeyPairGenerator.getInstance("RSA") - keyPairGenerator.initialize(1024) - val generateKeyPair = keyPairGenerator.generateKeyPair() - User.of( - id = 1, - name = "test", - domain = "localhost", - screenName = "test", - description = "", - password = "", - inbox = "https://example.com/inbox", - outbox = "https://example.com/outbox", - url = "https://example.com", - publicKey = "", - privateKey = generateKeyPair.private.toPem(), - createdAt = Instant.now() - ) - } - } - runBlocking { - val ktorKeyMap = KtorKeyMap(userQueryService, TestTransaction, testApplicationConfig) - - val httpClient = HttpClient( - MockEngine { httpRequestData -> - respondOk() - } - ) { - install(httpSignaturePlugin) { - keyMap = ktorKeyMap - } - install(Logging) { - logger = Logger.DEFAULT - level = LogLevel.ALL - } - } - - httpClient.postAp("https://localhost", "test", JsonLd(emptyList()), objectMapper) - } - } -}