feat: APRequestServiceに切り替え

This commit is contained in:
usbharu 2023-10-14 19:43:25 +09:00
parent f385e4fc8c
commit 0814258bfa
10 changed files with 60 additions and 161 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -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<Like>(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
)
}
}

View File

@ -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<Follow>(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)

View File

@ -11,6 +11,8 @@ interface APRequestService {
signer: User? = null,
responseClass: Class<R>
): R
suspend fun <T : Object> apPost(url: String, body: T? = null, signer: User? = null): String
}
suspend inline fun <reified R : Object> APRequestService.apGet(url: String, signer: User? = null): R =

View File

@ -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 <R : Object> apGet(url: String, signer: User?, responseClass: Class<R>): 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>
): R {
val bodyAsText = apPost(url, body, signer)
return objectMapper.readValue(bodyAsText, responseClass)
}
override suspend fun <T : Object> 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)
}
}

View File

@ -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)
}
}

View File

@ -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 <T : Object> runResolve(url: String, singer: User?, clazz: Class<T>): 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 {

View File

@ -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<UserQueryService> {
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)
}
}
}