feat: ジョブキューでHTTP Signatureの検証を行うように

This commit is contained in:
usbharu 2023-11-22 00:07:38 +09:00
parent 65b37cbb5e
commit 986d16f442
5 changed files with 115 additions and 6 deletions

View File

@ -1,16 +1,28 @@
package dev.usbharu.hideout.activitypub.interfaces.api.inbox
import dev.usbharu.hideout.activitypub.service.common.APService
import dev.usbharu.httpsignature.common.HttpHeaders
import dev.usbharu.httpsignature.common.HttpMethod
import dev.usbharu.httpsignature.common.HttpRequest
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
import java.net.URL
@RestController
class InboxControllerImpl(private val apService: APService) : InboxController {
@Suppress("TooGenericExceptionCaught")
override suspend fun inbox(@RequestBody string: String): ResponseEntity<Unit> {
override suspend fun inbox(
@RequestBody string: String
): ResponseEntity<Unit> {
val request = (requireNotNull(RequestContextHolder.getRequestAttributes()) as ServletRequestAttributes).request
val parseActivity = try {
apService.parseActivity(string)
} catch (e: Exception) {
@ -19,7 +31,29 @@ class InboxControllerImpl(private val apService: APService) : InboxController {
}
LOGGER.info("INBOX Processing Activity Type: {}", parseActivity)
try {
apService.processActivity(string, parseActivity)
val url = request.requestURL.toString()
val headersList = request.headerNames?.toList().orEmpty()
val headers =
headersList.associateWith { header -> request.getHeaders(header)?.toList().orEmpty() }
val method = when (val method = request.method.lowercase()) {
"get" -> HttpMethod.GET
"post" -> HttpMethod.POST
else -> {
throw IllegalArgumentException("Unsupported method: $method")
}
}
println(headers)
apService.processActivity(
string, parseActivity, HttpRequest(
URL(url + request.queryString.orEmpty()),
HttpHeaders(headers),
method
), headers
)
} catch (e: Exception) {
LOGGER.warn("FAILED Process Activity $parseActivity", e)
return ResponseEntity(HttpStatus.ACCEPTED)

View File

@ -7,6 +7,7 @@ import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubResponse
import dev.usbharu.hideout.activitypub.interfaces.api.common.ActivityPubStringResponse
import dev.usbharu.hideout.core.external.job.InboxJob
import dev.usbharu.hideout.core.service.job.JobQueueParentService
import dev.usbharu.httpsignature.common.HttpRequest
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
@ -15,7 +16,12 @@ import org.springframework.stereotype.Service
interface APService {
fun parseActivity(json: String): ActivityType
suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse?
suspend fun processActivity(
json: String,
type: ActivityType,
httpRequest: HttpRequest,
map: Map<String, List<String>>
): ActivityPubResponse?
}
enum class ActivityType {
@ -215,11 +221,20 @@ class APServiceImpl(
}
@Suppress("CyclomaticComplexMethod", "NotImplementedDeclaration")
override suspend fun processActivity(json: String, type: ActivityType): ActivityPubResponse {
override suspend fun processActivity(
json: String,
type: ActivityType,
httpRequest: HttpRequest,
map: Map<String, List<String>>
): ActivityPubResponse {
logger.debug("process activity: {}", type)
jobQueueParentService.schedule(InboxJob) {
props[it.json] = json
props[it.type] = type.name
val writeValueAsString = objectMapper.writeValueAsString(httpRequest)
println(writeValueAsString)
props[it.httpRequest] = writeValueAsString
props[it.headers] = objectMapper.writeValueAsString(map)
}
return ActivityPubStringResponse(message = "")
}

View File

@ -12,8 +12,17 @@ import dev.usbharu.hideout.activitypub.service.activity.like.APLikeServiceImpl
import dev.usbharu.hideout.activitypub.service.activity.like.ApReactionJobService
import dev.usbharu.hideout.activitypub.service.activity.undo.APUndoServiceImpl
import dev.usbharu.hideout.activitypub.service.objects.note.ApNoteJobService
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
import dev.usbharu.hideout.core.external.job.*
import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.hideout.util.RsaUtil
import dev.usbharu.httpsignature.common.HttpHeaders
import dev.usbharu.httpsignature.common.HttpRequest
import dev.usbharu.httpsignature.common.PublicKey
import dev.usbharu.httpsignature.verify.DefaultSignatureHeaderParser
import dev.usbharu.httpsignature.verify.RsaSha256HttpSignatureVerifier
import kjob.core.dsl.JobContextWithProps
import kjob.core.job.JobProps
import org.slf4j.LoggerFactory
@ -31,7 +40,12 @@ class ApJobServiceImpl(
private val APLikeServiceImpl: APLikeServiceImpl,
private val APUndoServiceImpl: APUndoServiceImpl,
private val APReceiveDeleteServiceImpl: APReceiveDeleteServiceImpl,
@Qualifier("activitypub") private val objectMapper: ObjectMapper
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
private val httpSignatureVerifier: RsaSha256HttpSignatureVerifier,
private val signatureHeaderParser: DefaultSignatureHeaderParser,
private val apUserService: APUserService,
private val userQueryService: UserQueryService,
private val transaction: Transaction
) : ApJobService {
@Suppress("REDUNDANT_ELSE_IN_WHEN")
override suspend fun <T : HideoutJob> processActivity(job: JobContextWithProps<T>, hideoutJob: HideoutJob) {
@ -41,6 +55,29 @@ class ApJobServiceImpl(
// Springで作成されるプロキシの都合上パターンマッチングが壊れるので必須
when (hideoutJob) {
is InboxJob -> {
val httpRequestString = (job.props as JobProps<InboxJob>)[InboxJob.httpRequest]
println(httpRequestString)
val headerString = (job.props as JobProps<InboxJob>)[InboxJob.headers]
val readValue = objectMapper.readValue<Map<String, List<String>>>(headerString)
val httpRequest =
objectMapper.readValue<HttpRequest>(httpRequestString).copy(headers = HttpHeaders(readValue))
val signature = signatureHeaderParser.parse(httpRequest.headers)
val publicKey = transaction.transaction {
try {
userQueryService.findByKeyId(signature.keyId)
} catch (e: FailedToGetResourcesException) {
apUserService.fetchPersonWithEntity(signature.keyId).second
}.publicKey
}
httpSignatureVerifier.verify(
httpRequest,
PublicKey(RsaUtil.decodeRsaPublicKeyPem(publicKey), signature.keyId)
)
val typeString = (job.props as JobProps<InboxJob>)[InboxJob.type]
val json = (job.props as JobProps<InboxJob>)[InboxJob.json]
val type = ActivityType.valueOf(typeString)
@ -60,6 +97,7 @@ class ApJobServiceImpl(
}
}
}
is ReceiveFollowJob -> {
apReceiveFollowJobService.receiveFollowJob(
job.props as JobProps<ReceiveFollowJob>

View File

@ -0,0 +1,20 @@
package dev.usbharu.hideout.application.config
import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner
import dev.usbharu.httpsignature.verify.DefaultSignatureHeaderParser
import dev.usbharu.httpsignature.verify.RsaSha256HttpSignatureVerifier
import dev.usbharu.httpsignature.verify.SignatureHeaderParser
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class HttpSignatureConfig {
@Bean
fun defaultSignatureHeaderParser(): DefaultSignatureHeaderParser = DefaultSignatureHeaderParser()
@Bean
fun rsaSha256HttpSignatureVerifier(
signatureHeaderParser: SignatureHeaderParser,
signatureSigner: RsaSha256HttpSignatureSigner
): RsaSha256HttpSignatureVerifier = RsaSha256HttpSignatureVerifier(signatureHeaderParser, signatureSigner)
}

View File

@ -41,4 +41,6 @@ object DeliverRemoveReactionJob : HideoutJob("DeliverRemoveReactionJob") {
object InboxJob : HideoutJob("InboxJob") {
val json = string("json")
val type = string("type")
val httpRequest = string("http_request")
val headers = string("headers")
}