mirror of https://github.com/usbharu/Hideout.git
Merge pull request #87 from usbharu/feature/http-client
Feature/http client
This commit is contained in:
commit
e3b7e0ed23
|
@ -143,7 +143,6 @@ dependencies {
|
|||
|
||||
implementation("org.drewcarlson:kjob-core:0.6.0")
|
||||
implementation("org.drewcarlson:kjob-mongo:0.6.0")
|
||||
testImplementation("org.slf4j:slf4j-simple:2.0.7")
|
||||
|
||||
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.1")
|
||||
}
|
||||
|
@ -152,10 +151,19 @@ detekt {
|
|||
parallel = true
|
||||
config = files("detekt.yml")
|
||||
buildUponDefaultConfig = true
|
||||
basePath = "${rootDir.absolutePath}/src/"
|
||||
basePath = "${rootDir.absolutePath}/src/main/kotlin"
|
||||
autoCorrect = true
|
||||
}
|
||||
|
||||
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>() {
|
||||
exclude("**/generated/**")
|
||||
doFirst {
|
||||
|
||||
}
|
||||
setSource("src/main/kotlin")
|
||||
exclude("build/")
|
||||
}
|
||||
|
||||
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
|
||||
exclude("**/org/koin/ksp/generated/**", "**/generated/**")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -12,19 +12,19 @@ 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.getAp
|
||||
import dev.usbharu.hideout.plugins.postAp
|
||||
import dev.usbharu.hideout.query.FollowerQueryService
|
||||
import dev.usbharu.hideout.query.MediaQueryService
|
||||
import dev.usbharu.hideout.query.PostQueryService
|
||||
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
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.statement.*
|
||||
import kjob.core.job.JobProps
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
|
@ -68,7 +68,10 @@ class APNoteServiceImpl(
|
|||
private val mediaQueryService: MediaQueryService,
|
||||
@Qualifier("activitypub") private val objectMapper: ObjectMapper,
|
||||
private val applicationConfig: ApplicationConfig,
|
||||
private val postService: PostService
|
||||
private val postService: PostService,
|
||||
private val apResourceResolveService: APResourceResolveService,
|
||||
private val apRequestService: APRequestService,
|
||||
private val transaction: Transaction
|
||||
|
||||
) : APNoteService, PostCreateInterceptor {
|
||||
|
||||
|
@ -120,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 {
|
||||
|
@ -143,11 +149,8 @@ class APNoteServiceImpl(
|
|||
}
|
||||
|
||||
logger.info("AP GET url: {}", url)
|
||||
val response = try {
|
||||
httpClient.getAp(
|
||||
url,
|
||||
targetActor?.let { "$targetActor#pubkey" }
|
||||
)
|
||||
val note = try {
|
||||
apResourceResolveService.resolve<Note>(url, null as Long?)
|
||||
} catch (e: ClientRequestException) {
|
||||
logger.warn(
|
||||
"FAILED Failed to retrieve ActivityPub resource. HTTP Status Code: {} url: {}",
|
||||
|
@ -156,7 +159,6 @@ class APNoteServiceImpl(
|
|||
)
|
||||
throw FailedToGetActivityPubResourceException("Could not retrieve $url.", e)
|
||||
}
|
||||
val note = objectMapper.readValue<Note>(response.bodyAsText())
|
||||
val savedNote = saveIfMissing(note, targetActor, url)
|
||||
logger.debug("SUCCESS Fetch Note url: {}", url)
|
||||
return savedNote
|
||||
|
|
|
@ -8,12 +8,10 @@ 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
|
||||
import dev.usbharu.hideout.service.job.JobQueueParentService
|
||||
import io.ktor.client.*
|
||||
import kjob.core.job.JobProps
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.stereotype.Service
|
||||
|
@ -29,13 +27,12 @@ interface APReactionService {
|
|||
@Service
|
||||
class APReactionServiceImpl(
|
||||
private val jobQueueParentService: JobQueueParentService,
|
||||
private val httpClient: HttpClient,
|
||||
private val userQueryService: UserQueryService,
|
||||
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 +71,19 @@ 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,19 @@ 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,12 +7,10 @@ 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
|
||||
import dev.usbharu.hideout.service.user.UserService
|
||||
import io.ktor.client.*
|
||||
import io.ktor.http.*
|
||||
import kjob.core.job.JobProps
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
|
@ -28,10 +26,10 @@ class APReceiveFollowServiceImpl(
|
|||
private val jobQueueParentService: JobQueueParentService,
|
||||
private val apUserService: APUserService,
|
||||
private val userService: UserService,
|
||||
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 +48,19 @@ 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)
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
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 <R : Object> apGet(url: String, signer: User? = null, responseClass: Class<R>): R
|
||||
suspend fun <T : Object, R : Object> apPost(
|
||||
url: String,
|
||||
body: T? = null,
|
||||
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 =
|
||||
apGet(url, signer, R::class.java)
|
||||
|
||||
suspend inline fun <T : Object, reified R : Object> APRequestService.apPost(
|
||||
url: String,
|
||||
body: T? = null,
|
||||
signer: User? = null
|
||||
): R = apPost(url, body, signer, R::class.java)
|
|
@ -0,0 +1,139 @@
|
|||
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.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
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 <R : Object> apGet(url: String, signer: User?, responseClass: Class<R>): R {
|
||||
val date = dateTimeFormatter.format(ZonedDateTime.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 = url,
|
||||
method = HttpMethod.Get,
|
||||
headers = headers,
|
||||
requestBody = "",
|
||||
keyPair = Key(
|
||||
keyId = "${signer.url}#pubkey",
|
||||
privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey),
|
||||
publicKey = RsaUtil.decodeRsaPublicKeyPem(signer.publicKey)
|
||||
),
|
||||
signHeaders = listOf("(request-target)", "date", "host", "accept")
|
||||
)
|
||||
|
||||
val bodyAsText = httpClient.get(url) {
|
||||
headers {
|
||||
headers {
|
||||
appendAll(sign.headers)
|
||||
remove("Host")
|
||||
}
|
||||
}
|
||||
contentType(ContentType.Application.Activity)
|
||||
}.bodyAsText()
|
||||
return objectMapper.readValue(bodyAsText, responseClass)
|
||||
}
|
||||
|
||||
override suspend fun <T : Object, R : Object> apPost(
|
||||
url: String,
|
||||
body: T?,
|
||||
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 {
|
||||
if (body != null) {
|
||||
val mutableListOf = mutableListOf<String>()
|
||||
mutableListOf.add("https://www.w3.org/ns/activitystreams")
|
||||
mutableListOf.addAll(body.context)
|
||||
body.context = mutableListOf
|
||||
}
|
||||
|
||||
val requestBody = objectMapper.writeValueAsString(body)
|
||||
|
||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
val digest = Base64Util.encode(sha256.digest(requestBody.toByteArray()))
|
||||
|
||||
val date = dateTimeFormatter.format(ZonedDateTime.now(ZoneId.of("GMT")))
|
||||
val u = URL(url)
|
||||
if (signer?.privateKey == null) {
|
||||
return httpClient.post(url) {
|
||||
header("Accept", ContentType.Application.Activity)
|
||||
header("Date", date)
|
||||
header("Digest", "sha-256=$digest")
|
||||
setBody(requestBody)
|
||||
contentType(ContentType.Application.Activity)
|
||||
}.bodyAsText()
|
||||
}
|
||||
|
||||
val headers = headers {
|
||||
append("Accept", ContentType.Application.Activity)
|
||||
append("Date", date)
|
||||
append("Host", u.host)
|
||||
append("Digest", "sha-256=$digest")
|
||||
}
|
||||
|
||||
val sign = httpSignatureSigner.sign(
|
||||
url = url,
|
||||
method = HttpMethod.Post,
|
||||
headers = headers,
|
||||
requestBody = "",
|
||||
keyPair = Key(
|
||||
keyId = "${signer.url}#pubkey",
|
||||
privateKey = RsaUtil.decodeRsaPrivateKeyPem(signer.privateKey),
|
||||
publicKey = RsaUtil.decodeRsaPublicKeyPem(signer.publicKey)
|
||||
),
|
||||
signHeaders = listOf("(request-target)", "date", "host", "digest")
|
||||
)
|
||||
|
||||
return httpClient.post(url) {
|
||||
headers {
|
||||
headers {
|
||||
appendAll(sign.headers)
|
||||
}
|
||||
}
|
||||
setBody(requestBody)
|
||||
contentType(ContentType.Application.Activity)
|
||||
}.bodyAsText()
|
||||
}
|
||||
}
|
|
@ -1,11 +1,7 @@
|
|||
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
|
||||
|
||||
interface APSendFollowService {
|
||||
|
@ -14,8 +10,7 @@ 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 +18,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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package dev.usbharu.hideout.service.ap
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import dev.usbharu.hideout.config.ApplicationConfig
|
||||
import dev.usbharu.hideout.domain.model.ap.Image
|
||||
import dev.usbharu.hideout.domain.model.ap.Key
|
||||
|
@ -10,16 +8,11 @@ import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto
|
|||
import dev.usbharu.hideout.domain.model.hideout.entity.User
|
||||
import dev.usbharu.hideout.exception.FailedToGetResourcesException
|
||||
import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException
|
||||
import dev.usbharu.hideout.plugins.getAp
|
||||
import dev.usbharu.hideout.query.UserQueryService
|
||||
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.user.UserService
|
||||
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 org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
interface APUserService {
|
||||
|
@ -40,11 +33,10 @@ interface APUserService {
|
|||
@Service
|
||||
class APUserServiceImpl(
|
||||
private val userService: UserService,
|
||||
private val httpClient: HttpClient,
|
||||
private val userQueryService: UserQueryService,
|
||||
private val transaction: Transaction,
|
||||
private val applicationConfig: ApplicationConfig,
|
||||
@Qualifier("activitypub") private val objectMapper: ObjectMapper
|
||||
private val apResourceResolveService: APResourceResolveService
|
||||
) :
|
||||
APUserService {
|
||||
|
||||
|
@ -111,14 +103,7 @@ class APUserServiceImpl(
|
|||
endpoints = mapOf("sharedInbox" to "${applicationConfig.url}/inbox")
|
||||
) to userEntity
|
||||
} catch (ignore: FailedToGetResourcesException) {
|
||||
val httpResponse = if (targetActor != null) {
|
||||
httpClient.getAp(url, "$targetActor#pubkey")
|
||||
} else {
|
||||
httpClient.get(url) {
|
||||
accept(ContentType.Application.Activity)
|
||||
}
|
||||
}
|
||||
val person = objectMapper.readValue<Person>(httpResponse.bodyAsText())
|
||||
val person = apResourceResolveService.resolve<Person>(url, null as Long?)
|
||||
|
||||
person to userService.createRemoteUser(
|
||||
RemoteUserCreateDto(
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package dev.usbharu.hideout.service.ap.resource
|
||||
|
||||
import dev.usbharu.hideout.domain.model.ap.Object
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.User
|
||||
|
||||
interface APResourceResolveService {
|
||||
suspend fun <T : Object> resolve(url: String, clazz: Class<T>, singer: User?): T
|
||||
suspend fun <T : Object> resolve(url: String, clazz: Class<T>, singerId: Long?): T
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : Object> APResourceResolveService.resolve(url: String, singer: User?): T =
|
||||
resolve(url, T::class.java, singer)
|
||||
|
||||
suspend inline fun <reified T : Object> APResourceResolveService.resolve(url: String, singerId: Long?): T =
|
||||
resolve(url, T::class.java, singerId)
|
|
@ -0,0 +1,49 @@
|
|||
package dev.usbharu.hideout.service.ap.resource
|
||||
|
||||
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.service.ap.APRequestService
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class APResourceResolveServiceImpl(
|
||||
private val apRequestService: APRequestService,
|
||||
private val userRepository: UserRepository,
|
||||
private val cacheManager: CacheManager
|
||||
) :
|
||||
APResourceResolveService {
|
||||
|
||||
override suspend fun <T : Object> resolve(url: String, clazz: Class<T>, singerId: Long?): T =
|
||||
internalResolve(url, singerId, clazz)
|
||||
|
||||
override suspend fun <T : Object> resolve(url: String, clazz: Class<T>, singer: User?): T =
|
||||
internalResolve(url, singer, clazz)
|
||||
|
||||
private suspend fun <T : Object> internalResolve(url: String, singerId: Long?, clazz: Class<T>): T {
|
||||
val key = genCacheKey(url, singerId)
|
||||
|
||||
cacheManager.putCache(key) {
|
||||
runResolve(url, singerId?.let { userRepository.findById(it) }, clazz)
|
||||
}
|
||||
return cacheManager.getOrWait(key) as T
|
||||
}
|
||||
|
||||
private suspend fun <T : Object> internalResolve(url: String, singer: User?, clazz: Class<T>): T {
|
||||
val key = genCacheKey(url, singer?.id)
|
||||
cacheManager.putCache(key) {
|
||||
runResolve(url, singer, clazz)
|
||||
}
|
||||
return cacheManager.getOrWait(key) as T
|
||||
}
|
||||
|
||||
private suspend fun <T : Object> runResolve(url: String, singer: User?, clazz: Class<T>): Object =
|
||||
apRequestService.apGet(url, singer, clazz)
|
||||
|
||||
private fun genCacheKey(url: String, singerId: Long?): String {
|
||||
if (singerId != null) {
|
||||
return "$url-$singerId"
|
||||
}
|
||||
return url
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package dev.usbharu.hideout.service.ap.resource
|
||||
|
||||
import dev.usbharu.hideout.domain.model.ap.Object
|
||||
|
||||
interface CacheManager {
|
||||
|
||||
suspend fun putCache(key: String, block: suspend () -> Object)
|
||||
suspend fun getOrWait(key: String): Object
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package dev.usbharu.hideout.service.ap.resource
|
||||
|
||||
import dev.usbharu.hideout.domain.model.ap.Object
|
||||
import dev.usbharu.hideout.util.LruCache
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Instant
|
||||
|
||||
@Service
|
||||
class InMemoryCacheManager : CacheManager {
|
||||
private val cacheKey = LruCache<String, Long>(15)
|
||||
private val valueStore = mutableMapOf<String, Object>()
|
||||
private val keyMutex = Mutex()
|
||||
|
||||
override suspend fun putCache(key: String, block: suspend () -> Object) {
|
||||
val needRunBlock: Boolean
|
||||
keyMutex.withLock {
|
||||
cacheKey.filter { Instant.ofEpochMilli(it.value).plusSeconds(300) <= Instant.now() }
|
||||
|
||||
val cached = cacheKey.get(key)
|
||||
if (cached == null) {
|
||||
needRunBlock = true
|
||||
cacheKey[key] = Instant.now().toEpochMilli()
|
||||
|
||||
valueStore.remove(key)
|
||||
} else {
|
||||
needRunBlock = false
|
||||
}
|
||||
}
|
||||
if (needRunBlock) {
|
||||
val processed = block()
|
||||
|
||||
if (cacheKey.containsKey(key)) {
|
||||
valueStore[key] = processed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getOrWait(key: String): Object {
|
||||
while (valueStore.contains(key).not()) {
|
||||
if (cacheKey.containsKey(key).not()) {
|
||||
throw IllegalStateException("Invalid cache key.")
|
||||
}
|
||||
delay(1)
|
||||
}
|
||||
return valueStore.getValue(key)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package dev.usbharu.hideout.service.signature
|
||||
|
||||
import io.ktor.http.*
|
||||
|
||||
interface HttpSignatureSigner {
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun sign(
|
||||
url: String,
|
||||
method: HttpMethod,
|
||||
headers: Headers,
|
||||
requestBody: String,
|
||||
keyPair: Key,
|
||||
signHeaders: List<String>
|
||||
): SignedRequest
|
||||
|
||||
suspend fun signRaw(signString: String, keyPair: Key, signHeaders: List<String>): Sign
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package dev.usbharu.hideout.service.signature
|
||||
|
||||
import dev.usbharu.hideout.util.Base64Util
|
||||
import io.ktor.http.*
|
||||
import io.ktor.util.*
|
||||
import org.springframework.stereotype.Component
|
||||
import java.net.URL
|
||||
import java.security.Signature
|
||||
|
||||
@Component
|
||||
class HttpSignatureSignerImpl : HttpSignatureSigner {
|
||||
override suspend fun sign(
|
||||
url: String,
|
||||
method: HttpMethod,
|
||||
headers: Headers,
|
||||
requestBody: String,
|
||||
keyPair: Key,
|
||||
signHeaders: List<String>
|
||||
): SignedRequest {
|
||||
val sign = signRaw(
|
||||
signString = buildSignString(
|
||||
url = URL(url),
|
||||
method = method,
|
||||
headers = headers,
|
||||
signHeaders = signHeaders
|
||||
),
|
||||
keyPair = keyPair,
|
||||
signHeaders = signHeaders
|
||||
)
|
||||
val signedHeaders = headers {
|
||||
appendAll(headers)
|
||||
set("Signature", sign.signatureHeader)
|
||||
}
|
||||
return SignedRequest(
|
||||
url = url,
|
||||
method = method,
|
||||
headers = signedHeaders,
|
||||
requestBody = requestBody,
|
||||
sign = sign
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun signRaw(signString: String, keyPair: Key, signHeaders: List<String>): Sign {
|
||||
val signer = Signature.getInstance("SHA256withRSA")
|
||||
signer.initSign(keyPair.privateKey)
|
||||
signer.update(signString.toByteArray())
|
||||
val sign = signer.sign()
|
||||
val signature = Base64Util.encode(sign)
|
||||
return Sign(
|
||||
signature,
|
||||
"""keyId="${keyPair.keyId}",algorithm="rsa-sha256",headers="${
|
||||
signHeaders.joinToString(
|
||||
" "
|
||||
)
|
||||
}",signature="$signature""""
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildSignString(
|
||||
url: URL,
|
||||
method: HttpMethod,
|
||||
headers: Headers,
|
||||
signHeaders: List<String>
|
||||
): String {
|
||||
headers.toMap().map { it.key.lowercase() to it.value }.toMap()
|
||||
val result = signHeaders.joinToString("\n") {
|
||||
if (it.startsWith("(")) {
|
||||
specialHeader(it, url, method)
|
||||
} else {
|
||||
generalHeader(it, headers.get(it)!!)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun specialHeader(fieldName: String, url: URL, method: HttpMethod): String {
|
||||
if (fieldName != "(request-target)") {
|
||||
throw IllegalArgumentException(fieldName + "is unsupported type")
|
||||
}
|
||||
return "(request-target): ${method.value.lowercase()} ${url.path}"
|
||||
}
|
||||
|
||||
private fun generalHeader(fieldName: String, value: String): String = "$fieldName: $value"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package dev.usbharu.hideout.service.signature
|
||||
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
|
||||
data class Key(
|
||||
val keyId: String,
|
||||
val privateKey: PrivateKey,
|
||||
val publicKey: PublicKey
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
package dev.usbharu.hideout.service.signature
|
||||
|
||||
data class Sign(
|
||||
val signature: String,
|
||||
val signatureHeader: String
|
||||
)
|
|
@ -0,0 +1,23 @@
|
|||
package dev.usbharu.hideout.service.signature
|
||||
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
|
||||
data class SignedRequest(
|
||||
val url: String,
|
||||
val method: HttpMethod,
|
||||
val headers: Headers,
|
||||
val requestBody: String,
|
||||
val sign: Sign
|
||||
) {
|
||||
fun toRequestBuilder(): HttpRequestBuilder {
|
||||
val httpRequestBuilder = HttpRequestBuilder()
|
||||
httpRequestBuilder.url(this.url)
|
||||
httpRequestBuilder.method = this.method
|
||||
httpRequestBuilder.headers {
|
||||
this.appendAll(headers)
|
||||
}
|
||||
httpRequestBuilder.setBody(requestBody)
|
||||
return httpRequestBuilder
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package dev.usbharu.hideout.util
|
||||
|
||||
import java.io.Serial
|
||||
|
||||
class LruCache<K, V>(private val maxSize: Int) : LinkedHashMap<K, V>(15, 0.75f, true) {
|
||||
|
||||
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>?): Boolean = size > maxSize
|
||||
|
||||
companion object {
|
||||
@Serial
|
||||
private const val serialVersionUID: Long = -6446947260925053191L
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ import java.security.spec.PKCS8EncodedKeySpec
|
|||
import java.security.spec.X509EncodedKeySpec
|
||||
|
||||
object RsaUtil {
|
||||
private val replaceHeaderAndFooterRegex = Regex("-----.*?-----")
|
||||
|
||||
fun decodeRsaPublicKey(byteArray: ByteArray): RSAPublicKey {
|
||||
val x509EncodedKeySpec = X509EncodedKeySpec(byteArray)
|
||||
return KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) as RSAPublicKey
|
||||
|
@ -14,10 +16,22 @@ object RsaUtil {
|
|||
|
||||
fun decodeRsaPublicKey(encoded: String): RSAPublicKey = decodeRsaPublicKey(Base64Util.decode(encoded))
|
||||
|
||||
fun decodeRsaPublicKeyPem(pem: String): RSAPublicKey {
|
||||
val replace = pem.replace(replaceHeaderAndFooterRegex, "")
|
||||
.replace("\n", "")
|
||||
return decodeRsaPublicKey(replace)
|
||||
}
|
||||
|
||||
fun decodeRsaPrivateKey(byteArray: ByteArray): RSAPrivateKey {
|
||||
val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(byteArray)
|
||||
return KeyFactory.getInstance("RSA").generatePrivate(pkcS8EncodedKeySpec) as RSAPrivateKey
|
||||
}
|
||||
|
||||
fun decodeRsaPrivateKey(encoded: String): RSAPrivateKey = decodeRsaPrivateKey(Base64Util.decode(encoded))
|
||||
|
||||
fun decodeRsaPrivateKeyPem(pem: String): RSAPrivateKey {
|
||||
val replace = pem.replace(replaceHeaderAndFooterRegex, "")
|
||||
.replace("\n", "")
|
||||
return decodeRsaPrivateKey(replace)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -96,7 +96,10 @@ class APNoteServiceImplTest {
|
|||
objectMapper = objectMapper,
|
||||
applicationConfig = testApplicationConfig,
|
||||
postService = mock(),
|
||||
mediaQueryService = mediaQueryService
|
||||
mediaQueryService = mediaQueryService,
|
||||
apResourceResolveService = mock(),
|
||||
apRequestService = mock(),
|
||||
transaction = mock()
|
||||
)
|
||||
val postEntity = Post.of(
|
||||
1L,
|
||||
|
@ -136,7 +139,10 @@ class APNoteServiceImplTest {
|
|||
objectMapper = objectMapper,
|
||||
applicationConfig = testApplicationConfig,
|
||||
postService = mock(),
|
||||
mediaQueryService = mediaQueryService
|
||||
mediaQueryService = mediaQueryService,
|
||||
apResourceResolveService = mock(),
|
||||
transaction = mock(),
|
||||
apRequestService = mock()
|
||||
)
|
||||
activityPubNoteService.createNoteJob(
|
||||
JobProps(
|
||||
|
|
|
@ -3,17 +3,17 @@
|
|||
|
||||
package dev.usbharu.hideout.service.ap
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import dev.usbharu.hideout.config.Config
|
||||
import dev.usbharu.hideout.config.ConfigData
|
||||
import dev.usbharu.hideout.domain.model.ap.*
|
||||
import dev.usbharu.hideout.domain.model.ap.Follow
|
||||
import dev.usbharu.hideout.domain.model.ap.Image
|
||||
import dev.usbharu.hideout.domain.model.ap.Key
|
||||
import dev.usbharu.hideout.domain.model.ap.Person
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.User
|
||||
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
|
||||
import dev.usbharu.hideout.query.UserQueryService
|
||||
import dev.usbharu.hideout.service.job.JobQueueParentService
|
||||
import dev.usbharu.hideout.service.user.UserService
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.mock.*
|
||||
import kjob.core.dsl.ScheduleContext
|
||||
import kjob.core.job.JobProps
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
@ -40,9 +40,9 @@ class APReceiveFollowServiceImplTest {
|
|||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
mock(),
|
||||
TestTransaction,
|
||||
objectMapper
|
||||
objectMapper,
|
||||
mock()
|
||||
)
|
||||
activityPubFollowService.receiveFollow(
|
||||
Follow(
|
||||
|
@ -145,35 +145,10 @@ class APReceiveFollowServiceImplTest {
|
|||
mock(),
|
||||
apUserService,
|
||||
userService,
|
||||
HttpClient(
|
||||
MockEngine { httpRequestData ->
|
||||
assertEquals(person.inbox, httpRequestData.url.toString())
|
||||
val accept = Accept(
|
||||
type = emptyList(),
|
||||
name = "Follow",
|
||||
`object` = Follow(
|
||||
type = emptyList(),
|
||||
name = "Follow",
|
||||
`object` = "https://example.com",
|
||||
actor = "https://follower.example.com"
|
||||
),
|
||||
actor = "https://example.com"
|
||||
)
|
||||
accept.context += "https://www.w3.org/ns/activitystreams"
|
||||
val content = httpRequestData.body.toByteArray().decodeToString()
|
||||
println(content)
|
||||
assertEquals(
|
||||
accept,
|
||||
Config.configData.objectMapper.readValue<Accept>(
|
||||
content
|
||||
)
|
||||
)
|
||||
respondOk()
|
||||
}
|
||||
),
|
||||
userQueryService,
|
||||
TestTransaction,
|
||||
objectMapper
|
||||
objectMapper,
|
||||
mock()
|
||||
)
|
||||
activityPubFollowService.receiveFollowJob(
|
||||
JobProps(
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
package dev.usbharu.hideout.service.ap.resource
|
||||
|
||||
import dev.usbharu.hideout.domain.model.ap.Object
|
||||
import dev.usbharu.hideout.domain.model.hideout.entity.User
|
||||
import dev.usbharu.hideout.repository.UserRepository
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.mock.*
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.mockito.junit.jupiter.MockitoExtension
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.whenever
|
||||
import java.time.Instant
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ExtendWith(MockitoExtension::class)
|
||||
@Disabled
|
||||
class APResourceResolveServiceImplTest {
|
||||
|
||||
@Test
|
||||
fun `単純な一回のリクエスト`() = runTest {
|
||||
|
||||
var count = 0
|
||||
|
||||
val httpClient = HttpClient(MockEngine { request ->
|
||||
count++
|
||||
respondOk("{}")
|
||||
})
|
||||
|
||||
val userRepository = mock<UserRepository>()
|
||||
|
||||
whenever(userRepository.findById(any())).doReturn(
|
||||
User.of(
|
||||
2L,
|
||||
"follower",
|
||||
"follower.example.com",
|
||||
"followerUser",
|
||||
"test follower user",
|
||||
"https://follower.example.com/inbox",
|
||||
"https://follower.example.com/outbox",
|
||||
"https://follower.example.com",
|
||||
"https://follower.example.com",
|
||||
publicKey = "",
|
||||
createdAt = Instant.now()
|
||||
)
|
||||
)
|
||||
|
||||
val apResourceResolveService =
|
||||
APResourceResolveServiceImpl(mock(), userRepository, InMemoryCacheManager())
|
||||
|
||||
apResourceResolveService.resolve<Object>("https", 0)
|
||||
|
||||
assertEquals(1, count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun 複数回の同じリクエストが重複して発行されない() = runTest {
|
||||
var count = 0
|
||||
|
||||
val httpClient = HttpClient(MockEngine { request ->
|
||||
count++
|
||||
respondOk("{}")
|
||||
})
|
||||
|
||||
val userRepository = mock<UserRepository>()
|
||||
|
||||
whenever(userRepository.findById(any())).doReturn(
|
||||
User.of(
|
||||
2L,
|
||||
"follower",
|
||||
"follower.example.com",
|
||||
"followerUser",
|
||||
"test follower user",
|
||||
"https://follower.example.com/inbox",
|
||||
"https://follower.example.com/outbox",
|
||||
"https://follower.example.com",
|
||||
"https://follower.example.com",
|
||||
publicKey = "",
|
||||
createdAt = Instant.now()
|
||||
)
|
||||
)
|
||||
|
||||
val apResourceResolveService =
|
||||
APResourceResolveServiceImpl(mock(), userRepository, InMemoryCacheManager())
|
||||
|
||||
apResourceResolveService.resolve<Object>("https", 0)
|
||||
apResourceResolveService.resolve<Object>("https", 0)
|
||||
apResourceResolveService.resolve<Object>("https", 0)
|
||||
apResourceResolveService.resolve<Object>("https", 0)
|
||||
|
||||
assertEquals(1, count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun 複数回の同じリクエストが同時に発行されても重複して発行されない() = runTest {
|
||||
var count = 0
|
||||
|
||||
val httpClient = HttpClient(MockEngine { request ->
|
||||
count++
|
||||
respondOk("{}")
|
||||
})
|
||||
|
||||
val userRepository = mock<UserRepository>()
|
||||
|
||||
whenever(userRepository.findById(any())).doReturn(
|
||||
User.of(
|
||||
2L,
|
||||
"follower",
|
||||
"follower.example.com",
|
||||
"followerUser",
|
||||
"test follower user",
|
||||
"https://follower.example.com/inbox",
|
||||
"https://follower.example.com/outbox",
|
||||
"https://follower.example.com",
|
||||
"https://follower.example.com",
|
||||
publicKey = "",
|
||||
createdAt = Instant.now()
|
||||
)
|
||||
)
|
||||
|
||||
val apResourceResolveService =
|
||||
APResourceResolveServiceImpl(mock(), userRepository, InMemoryCacheManager())
|
||||
|
||||
repeat(10) {
|
||||
awaitAll(
|
||||
async { apResourceResolveService.resolve<Object>("https", 0) },
|
||||
async { apResourceResolveService.resolve<Object>("https", 0) },
|
||||
async { apResourceResolveService.resolve<Object>("https", 0) },
|
||||
async { apResourceResolveService.resolve<Object>("https", 0) },
|
||||
async { apResourceResolveService.resolve<Object>("https", 0) },
|
||||
async { apResourceResolveService.resolve<Object>("https", 0) },
|
||||
async { apResourceResolveService.resolve<Object>("https", 0) },
|
||||
async { apResourceResolveService.resolve<Object>("https", 0) },
|
||||
async { apResourceResolveService.resolve<Object>("https", 0) },
|
||||
async { apResourceResolveService.resolve<Object>("https", 0) },
|
||||
async { apResourceResolveService.resolve<Object>("https", 0) },
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(1, count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun 関係のないリクエストは発行する() = runTest {
|
||||
var count = 0
|
||||
|
||||
val httpClient = HttpClient(MockEngine { request ->
|
||||
count++
|
||||
respondOk("{}")
|
||||
})
|
||||
|
||||
val userRepository = mock<UserRepository>()
|
||||
|
||||
whenever(userRepository.findById(any())).doReturn(
|
||||
User.of(
|
||||
2L,
|
||||
"follower",
|
||||
"follower.example.com",
|
||||
"followerUser",
|
||||
"test follower user",
|
||||
"https://follower.example.com/inbox",
|
||||
"https://follower.example.com/outbox",
|
||||
"https://follower.example.com",
|
||||
"https://follower.example.com",
|
||||
publicKey = "",
|
||||
createdAt = Instant.now()
|
||||
)
|
||||
)
|
||||
|
||||
val apResourceResolveService =
|
||||
APResourceResolveServiceImpl(mock(), userRepository, InMemoryCacheManager())
|
||||
|
||||
apResourceResolveService.resolve<Object>("abcd", 0)
|
||||
apResourceResolveService.resolve<Object>("1234", 0)
|
||||
apResourceResolveService.resolve<Object>("aaaa", 0)
|
||||
|
||||
assertEquals(3, count)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,340 @@
|
|||
package dev.usbharu.hideout.service.signature
|
||||
|
||||
import dev.usbharu.hideout.util.Base64Util
|
||||
import dev.usbharu.hideout.util.RsaUtil
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import tech.barbero.http.message.signing.HttpMessage
|
||||
import tech.barbero.http.message.signing.HttpRequest
|
||||
import tech.barbero.http.message.signing.KeyMap
|
||||
import tech.barbero.http.message.signing.SignatureHeaderVerifier
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
import java.security.MessageDigest
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import javax.crypto.SecretKey
|
||||
import kotlin.test.assertFalse
|
||||
|
||||
class HttpSignatureSignerImplTest {
|
||||
@Test
|
||||
fun `HTTP Signatureの署名を作成できる`() = runTest {
|
||||
|
||||
val publicKey = RsaUtil.decodeRsaPublicKey(
|
||||
"""MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv6tEMdAw9xk3Pt5YMxJ2t+1QZeb9p+PKpS1lVbkL5oWj6aL2Q3nRVQQabcILOb5YNUpWQVQWRjW4jkrBDuiAgvlmu126OPs4E1cVVWEqylJ5VOkOIeXpldOu/SvHM/sHPNHXYlovaHDIqT+3zp2xUmXQx2kum0b/o8Vp+wh45iIoflb62/0dQ5YZyZEp283XKne+u813BzCOa1IAsywbUvX9kUv1SaUDn3oxnjdjWgSqsJcJVU1lyiN0OrpnEg5TMVjDqN3vimoR4uqNn5Zm8rrif/o8w+/FlnWticbty5MQun0gFaCfLsR8ODm1/0DwT6WI/bRpy6zye1n4iQn/nwIDAQAB"""
|
||||
)
|
||||
val privateKey = RsaUtil.decodeRsaPrivateKey(
|
||||
"""MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/q0Qx0DD3GTc+3lgzEna37VBl5v2n48qlLWVVuQvmhaPpovZDedFVBBptwgs5vlg1SlZBVBZGNbiOSsEO6ICC+Wa7Xbo4+zgTVxVVYSrKUnlU6Q4h5emV0679K8cz+wc80ddiWi9ocMipP7fOnbFSZdDHaS6bRv+jxWn7CHjmIih+Vvrb/R1DlhnJkSnbzdcqd767zXcHMI5rUgCzLBtS9f2RS/VJpQOfejGeN2NaBKqwlwlVTWXKI3Q6umcSDlMxWMOo3e+KahHi6o2flmbyuuJ/+jzD78WWda2Jxu3LkxC6fSAVoJ8uxHw4ObX/QPBPpYj9tGnLrPJ7WfiJCf+fAgMBAAECggEAIkL4LrtbdWAxivBt7bs4M4qdW4nd/9vtRneF7LvmT6/F7CawRMGK1Nql6sbMAOdwlx4Rqx3f2W8S7YSZXBPdnQv9/DI17qehj3t6mceDwaTagX4jg5W4moq7dhAUTMtrsMiF6tPaM54tkGuObMWtg+AlYPABX8piOiE436HVErXrOaWsrQ6ReoHodTyibfO8aByzLkIb2k3nt1j8HotjjFe6ZqFVkXiGVWOUwdLpsqE+8BV6g1IF480SyKF4HnUfr/AxDnpKtTFspGCKu/w7BA6yOaaONeal0/EUA8vlfLsKdaRY2TRmCFCQzUwluBTr6ssjQyilJzgJ6VbDFpVSSQKBgQDgpt5kB7TDXN5ucD0alN0umI/rLD5TTg0rbpLo2wzfh2IAPYSiCgNOVr1Mi6JRxqSLa4KeEOCYATLu9wrFU8y+i/ffrDAMo/b2z3TORV3p3m1fPx6CnqBZMvxrHl2CCbij+6O1qmq+8AW8+lQuilq3u6dRBkYpt+mRHWsqvMeNqwKBgQDaair8CIEcoCtxlw8lDRJNn7bC9DRiaJLxPYuOHop7lolUy1amd2srREgoEB7FRwC5bki+BsSUffFyix2kUsf4I2dLHYmbf4Aci2GpqdRW4AnO2tWnvHGsAnkmsRQ2ZuoF7+8Phd1pnXY9DHImAxmpUgqhKDqbP4Hi1W2w5s0Z3QKBgQCTlUxYTq+0AFioGNgrlExSBivWBXTUaVxBghzFGNK2Lkx1d/SgNw/A8T7fAIScUHFcnj5q9Q93DKKXVnge9lR1gaJPsODIDRd7QQKtV+jAcT1M6zxx9x/EObiV7pbjjNtd7zy3ZcNGuIwsgA+5m27JcWAT3JlPYuDwUnFK3EYEjQKBgCHCm1ZNsjdMgqqSIOMnPBcHguZrfNVhOKVVUAbtrZYg1KVosMIWX1hWu5iFtVvk97Wx2EiXHzecp/9+hVxq90HhpwuzSxvf/1tqJ/RjrdCn3Jw+sxu0QxXFZBiY8njeO3ojdh4+INU8Y5RYIiTCAetsJPx4DWcFz/vR5ZyccEN5AoGAHgP5ZeUvn/NR5GvX7NIVbYReO6+YeilNE8mGa57Ew4GJotrS5P4nevDyZWZCs63f4ZQ/I/lJnrGRtQDfQC7wUGhMf7VjZfagFHcSO44uCVKsSO7ToTyuObTpdEC9dUeVaJt96ZP5eX4vWZ6MNgYstlmXKVLg9LHsLJlXKNHufg0="""
|
||||
)
|
||||
|
||||
val httpSignatureSignerImpl = HttpSignatureSignerImpl()
|
||||
|
||||
val format = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US)
|
||||
format.timeZone = TimeZone.getTimeZone("GMT")
|
||||
|
||||
//language=JSON
|
||||
val requestBody = """{
|
||||
"hoge": "fuga"
|
||||
}"""
|
||||
|
||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
val encode = Base64Util.encode(sha256.digest(requestBody.toByteArray()))
|
||||
|
||||
val url = "https://example.com/"
|
||||
httpSignatureSignerImpl.sign(
|
||||
url,
|
||||
HttpMethod.Post,
|
||||
Headers.build {
|
||||
append("Date", "Fri, 13 Oct 2023 07:14:50 GMT")
|
||||
append("Host", URL(url).host)
|
||||
append("Digest", "SHA-256=$encode")
|
||||
},
|
||||
requestBody,
|
||||
Key("https://example.com", privateKey, publicKey),
|
||||
listOf("(request-target)", "date", "host", "digest")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HTTP Signatureの署名が検証に成功する`() = runTest {
|
||||
val publicKey = RsaUtil.decodeRsaPublicKey(
|
||||
"""MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJVqbb17nCo8aBZYF+vDgnFANaFDNuvHKMT39qQGnetItYZ8DBtRZzvYE6njn1vH7gixPhGnjt6qLWJJzeoSSv1FgQp9yUq719QFC9BQ87RughpkrP1Nq0ZHuTLMH0U13g2oziRp04FZXElq6b3aHLK+Y78mX20l9HCqIh4GdBRjgiAjcZr/XOZl1cKa7ai3z4yO4euOb8LiJavMHz7/ISefUGtikrhnIqNwwQ1prxT1bZduTotjSi8bitdzsvGh5ftTiFxJC+Pe1yJn3ALW/L3SBm72x60S14osQv1gMaDLaA6YNXCYm34xKndF+UxWTUwLUpNM/GRDoNa8Yq7HBwIDAQAB"""
|
||||
)
|
||||
val privateKey = RsaUtil.decodeRsaPrivateKey(
|
||||
"""MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC4lWptvXucKjxoFlgX68OCcUA1oUM268coxPf2pAad60i1hnwMG1FnO9gTqeOfW8fuCLE+EaeO3qotYknN6hJK/UWBCn3JSrvX1AUL0FDztG6CGmSs/U2rRke5MswfRTXeDajOJGnTgVlcSWrpvdocsr5jvyZfbSX0cKoiHgZ0FGOCICNxmv9c5mXVwprtqLfPjI7h645vwuIlq8wfPv8hJ59Qa2KSuGcio3DBDWmvFPVtl25Oi2NKLxuK13Oy8aHl+1OIXEkL497XImfcAtb8vdIGbvbHrRLXiixC/WAxoMtoDpg1cJibfjEqd0X5TFZNTAtSk0z8ZEOg1rxirscHAgMBAAECggEAU5VRQs09Rpt3jBimHnrjptM6pK5X/ewpXKRItpZS6rqqy4xQ6riKFYmrUEgrazOH5ploDTe4XMEmZXOvAP/f9bYXfZXvHLHrOpHnERDtP1XyfpaOBSmUvJyQCORgOz6/ZERiLqqdgyl8+gXC1IJkXH9yKD/cE/UcbUKBP/7BpFj7lPMyNCApiS1Z2RinvOSsx2TCBfVLpEE1dTLdHg3g3vfkmnn+KQ/SU4z3ksXJa0ODZY9lsUGWUrGmnhd/tviSuNUJG3wx7h1er4LBjuA4OZD8qJA+sXcEY2Kn7XQHAOBWUfAOR7nzAl3mPYycIZs4sDrq2awwX12ML9qR/40swQKBgQDtBhIML+Xt32fLw4/wtSDmDJo4szyu0c3Gangl4eMjOc1WEXl/bL8uryNS9b+1he8b+VgEBFH2nhl3u1eman0/xpk9hqj9hd/IDazMqUr7mKq+b9WXWd24LFZNew+35RUELW01FdEDSr+KZsCIjFilAeWfpJORoj3oZFU5C/5mQQKBgQDHXI7NqHy2ATqDiQI3aG72B8n3TbR9B8G01Anfn3ZKcXIFWnDHoB9y/ITYzGrjrbbEOD2BsAacOy7bOWHlX1RIcD10ZWJIBdjqc+zfpahb36mXbcEQkb7col5s992KGVZHu8OBwfGJMVHYprIxOmygj1CAF9pEZyMy3alHChOrRwKBgQCYeyxHHNVNh0huBLxn/Q5SEM9yJJSoXp6Dw+DRdhU6hyf687j26c3ASblu2Fvhem1N0MX3p5PXFPSLW0FS9PTof2n789JpbqN9Ppbo/wwW+ar2YlnFSXHi1tsac020XzJ7AoJcAVH6TS8V6W55KdipJqRDZIvux7IN++X7kiSyQQKBgQCweIIAEhCym0vMe0729P6j0ik5PBN0SZVyF+/VfzYal2kyy+fhDSBJjLWbovdLKs4Jyy7GyaZQTSMg8x5xB3130cLUcZoZ3vMwNgWLwvvQt59LZ9/qZtjoPOIQ2yfDwsHZJZ/eEGtZ4cptWMGLSgg16CZ9/J88xX8m24eoVocqqQKBgCEj/FK26bBLnPtRlQ+5mTQ/CjcjD5/KoaHLawULvXq03qIiZfDZg+sm7JUmlaC48sERGLJnjNYk/1pjw5N8txyAk2UHxqi+dayRkTCRSfBm0PUWyVWiperHNEuByHnyh+qX00sE3SCz2qDSDLb1x7kV+2BhEL+XfgD7evqrvrNq"""
|
||||
)
|
||||
|
||||
val httpSignatureSignerImpl = HttpSignatureSignerImpl()
|
||||
|
||||
val format = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US)
|
||||
format.timeZone = TimeZone.getTimeZone("GMT")
|
||||
|
||||
//language=JSON
|
||||
val requestBody = """{
|
||||
"hoge": "fuga"
|
||||
}"""
|
||||
|
||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
val encode = Base64Util.encode(sha256.digest(requestBody.toByteArray()))
|
||||
|
||||
val url = "https://example.com/"
|
||||
val headers = Headers.build {
|
||||
append("Date", "Fri, 13 Oct 2023 07:14:50 GMT")
|
||||
append("Host", URL(url).host)
|
||||
append("Digest", "SHA-256=$encode")
|
||||
}
|
||||
val sign = httpSignatureSignerImpl.sign(
|
||||
url,
|
||||
HttpMethod.Post,
|
||||
headers,
|
||||
requestBody,
|
||||
Key("https://example.com", privateKey, publicKey),
|
||||
listOf("(request-target)", "date", "host", "digest")
|
||||
)
|
||||
|
||||
val keyMap = object : KeyMap {
|
||||
override fun getPublicKey(keyId: String?): PublicKey {
|
||||
return publicKey
|
||||
}
|
||||
|
||||
override fun getPrivateKey(keyId: String?): PrivateKey {
|
||||
return privateKey
|
||||
}
|
||||
|
||||
override fun getSecretKey(keyId: String?): SecretKey {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
val verifier = SignatureHeaderVerifier.builder().keyMap(keyMap).build()
|
||||
|
||||
val headers1 = headers {
|
||||
appendAll(headers)
|
||||
append("Signature", sign.sign.signatureHeader)
|
||||
}
|
||||
|
||||
val httpMessage = object : HttpMessage, HttpRequest {
|
||||
override fun headerValues(name: String?): MutableList<String> {
|
||||
return name?.let { headers1.getAll(it) }.orEmpty().toMutableList()
|
||||
}
|
||||
|
||||
override fun addHeader(name: String?, value: String?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun method(): String {
|
||||
return "POST"
|
||||
}
|
||||
|
||||
override fun uri(): URI {
|
||||
return URI(url)
|
||||
}
|
||||
}
|
||||
val verify = verifier.verify(httpMessage)
|
||||
assertTrue(verify)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HTTP Signatureの署名が検証に成功する2`() = runTest {
|
||||
val publicKey = RsaUtil.decodeRsaPublicKeyPem(
|
||||
"""-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3YdxpopDvAIp+Ciplvx
|
||||
SfY8tV3GquYIfxSfTPAqiusgf8zXxYz0ilxY+nHjzIpdOA8rDHcDVhBXI/5lP1Vl
|
||||
sgeY5cgJRuG9g9ZWaQV/8oKYoillgTkNuyNB0OGa84BAeKo+VMG1NNtlVCn2DrvA
|
||||
8FLXAc2e4wPcOozKV5JYHZ0RDcSIS1bPb5ArxhhF8zAjn9+s/plsDz+mgHD0Ce5z
|
||||
UUv1uHQF8nj53WL4cCcrl5TSvqaK6Krcmb7i1YVSlk52p0AYg79pXpPQLhe3TnvJ
|
||||
Gy+KPvKPq1cho5jM1vJktK6eGlnUPEgD0bCSXl7FrtE7mPMCsaQCRj+up4t+NBWu
|
||||
gwIDAQAB
|
||||
-----END PUBLIC KEY-----"""
|
||||
)
|
||||
val privateKey = RsaUtil.decodeRsaPrivateKeyPem(
|
||||
"""-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrdh3GmikO8Ain
|
||||
4KKmW/FJ9jy1Xcaq5gh/FJ9M8CqK6yB/zNfFjPSKXFj6cePMil04DysMdwNWEFcj
|
||||
/mU/VWWyB5jlyAlG4b2D1lZpBX/ygpiiKWWBOQ27I0HQ4ZrzgEB4qj5UwbU022VU
|
||||
KfYOu8DwUtcBzZ7jA9w6jMpXklgdnRENxIhLVs9vkCvGGEXzMCOf36z+mWwPP6aA
|
||||
cPQJ7nNRS/W4dAXyePndYvhwJyuXlNK+poroqtyZvuLVhVKWTnanQBiDv2lek9Au
|
||||
F7dOe8kbL4o+8o+rVyGjmMzW8mS0rp4aWdQ8SAPRsJJeXsWu0TuY8wKxpAJGP66n
|
||||
i340Fa6DAgMBAAECggEAUsE0h9l5/aKumtAZ0K9JmwgErwiuzWcvLJ64cDruXZQ0
|
||||
YFpuvgNVN75wl5gGeX9ClL8FaQO8EXrbhBzRoyrFZZKzIhxVFef4PzxhAllMMrED
|
||||
mCjgu+jcjrjqmDV7QxFgjJymbuP7YKKPmnqSLvRBn/xrl4w1pp4DWiL/uhqA+vE8
|
||||
ZOgfzJ6LzU3CUFjCEi73gfZzTyykzpw+H3Lf8WPYCRQteng7zGxFDpPM3uDt0AKV
|
||||
nTReopN6HKVOqobBuJLbD2kORfFzfzfLKrkAELivO/yOdosbG5GIf8nxZ0h86QIo
|
||||
knav6boRgF9LqZTzC+QWBjGXEng58gEYEuAaovup8QKBgQDeR9onVIj67FZ/J1k4
|
||||
VBTfxRZ4r2oFHyhh3O2Y1xmVM0ejlvtnQL989d6HCieT6wd9CcfTOnTidgXCW+1a
|
||||
wW3Q6eqtaPanRsU8aCcG2Pa19hbEkdsAvu/8eS8SWegnyqk0lKZjRP6KXDto99dd
|
||||
CWs8KMcTXTqpFfNr83AeuR1ViwKBgQDFeLms7hvnLVF0oS6LIh73WVd1YfhcCsxo
|
||||
MfjLmsivCfvyo/RAWmWjHTvh9ofYm3a/1gU4ACm33tI++uWz1juHxJFy+ryjjz7z
|
||||
MHimmohaWkeax9wyUn66hG52JYUHQFoi85cL/YLMMX3WZXa5LQyyXPgirF4L9+c9
|
||||
MTZNrKDZ6QKBgEhDX77NksLQtsYbyruvSiH9dvLBRFxp5rz6EBxSQbTpuO6MFSta
|
||||
N2auoCuSt481J3gVB+u542oEKJcpP57zp3n1sh+yMg3ryg97ZMSrIHnDiV9ac7Jo
|
||||
YKjZ1N3IcNsO3beEZBt9wKrGlWHowRE0ELK8Jww6kOmLg1mjCN5UHB9FAoGAVewl
|
||||
vl0MvxY07y6C9f8uwimZqHWsf0AjmOLFgrIiyCbr/bPhP28V8ldyCuweR929WdNi
|
||||
Ce/oNx05FjZNZGa/GGAreYAoPHLDzUU1+igbVFUb+vkjkrHaeoXNGpNQwsr5bWPY
|
||||
QVtZYkfWnUcg1YoIkENrpIqjkUmY0ENtgXavtqECgYEA2F+FJPPpm39gD2mnbnAH
|
||||
goM9c+h9hh/o3kW3CUNgPKeYT4ptd3AG0k9C9De+eWb3GGqH1/KUGvUbyXm7f1Wi
|
||||
y+SBT1Uk6/85ZZ3nCz2Yj8eGokhcfKhXd8K3HV2wgoUWMJT1Qvedrqc2R5S9wdY8
|
||||
wADggCG8df/amNR+dyQOOuQ=
|
||||
-----END PRIVATE KEY-----"""
|
||||
)
|
||||
|
||||
val httpSignatureSignerImpl = HttpSignatureSignerImpl()
|
||||
|
||||
val format = DateTimeFormatter.RFC_1123_DATE_TIME
|
||||
|
||||
//language=JSON
|
||||
val requestBody = """{
|
||||
"hoge": "fuga"
|
||||
}"""
|
||||
|
||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
val encode = Base64Util.encode(sha256.digest(requestBody.toByteArray()))
|
||||
|
||||
val url = "https://test-hideout.usbharu.dev/users/97ws8y3rj6/inbox"
|
||||
val headers = Headers.build {
|
||||
append("Date", format.format(ZonedDateTime.now(ZoneId.of("GMT"))))
|
||||
append("Host", URL(url).host)
|
||||
append("Digest", "sha-256=$encode")
|
||||
}
|
||||
val sign = httpSignatureSignerImpl.sign(
|
||||
url,
|
||||
HttpMethod.Post,
|
||||
headers,
|
||||
requestBody,
|
||||
Key("https://test-hideout.usbharu.dev/users/c#pubkey", privateKey, publicKey),
|
||||
listOf("(request-target)", "date", "host", "digest")
|
||||
)
|
||||
|
||||
val keyMap = object : KeyMap {
|
||||
override fun getPublicKey(keyId: String?): PublicKey {
|
||||
return publicKey
|
||||
}
|
||||
|
||||
override fun getPrivateKey(keyId: String?): PrivateKey {
|
||||
return privateKey
|
||||
}
|
||||
|
||||
override fun getSecretKey(keyId: String?): SecretKey {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
val verifier = SignatureHeaderVerifier.builder().keyMap(keyMap).build()
|
||||
|
||||
val headers1 = headers {
|
||||
appendAll(headers)
|
||||
append("Signature", sign.sign.signatureHeader)
|
||||
}
|
||||
|
||||
val httpMessage = object : HttpMessage, HttpRequest {
|
||||
override fun headerValues(name: String?): MutableList<String> {
|
||||
return name?.let { headers1.getAll(it) }.orEmpty().toMutableList()
|
||||
}
|
||||
|
||||
override fun addHeader(name: String?, value: String?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun method(): String {
|
||||
return "POST"
|
||||
}
|
||||
|
||||
override fun uri(): URI {
|
||||
return URI(url)
|
||||
}
|
||||
}
|
||||
val verify = verifier.verify(httpMessage)
|
||||
assertTrue(verify)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HTTP Signatureで署名した後、改ざんされた場合検証に失敗する`() = runTest {
|
||||
val publicKey = RsaUtil.decodeRsaPublicKey(
|
||||
"""MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJVqbb17nCo8aBZYF+vDgnFANaFDNuvHKMT39qQGnetItYZ8DBtRZzvYE6njn1vH7gixPhGnjt6qLWJJzeoSSv1FgQp9yUq719QFC9BQ87RughpkrP1Nq0ZHuTLMH0U13g2oziRp04FZXElq6b3aHLK+Y78mX20l9HCqIh4GdBRjgiAjcZr/XOZl1cKa7ai3z4yO4euOb8LiJavMHz7/ISefUGtikrhnIqNwwQ1prxT1bZduTotjSi8bitdzsvGh5ftTiFxJC+Pe1yJn3ALW/L3SBm72x60S14osQv1gMaDLaA6YNXCYm34xKndF+UxWTUwLUpNM/GRDoNa8Yq7HBwIDAQAB"""
|
||||
)
|
||||
val privateKey = RsaUtil.decodeRsaPrivateKey(
|
||||
"""MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC4lWptvXucKjxoFlgX68OCcUA1oUM268coxPf2pAad60i1hnwMG1FnO9gTqeOfW8fuCLE+EaeO3qotYknN6hJK/UWBCn3JSrvX1AUL0FDztG6CGmSs/U2rRke5MswfRTXeDajOJGnTgVlcSWrpvdocsr5jvyZfbSX0cKoiHgZ0FGOCICNxmv9c5mXVwprtqLfPjI7h645vwuIlq8wfPv8hJ59Qa2KSuGcio3DBDWmvFPVtl25Oi2NKLxuK13Oy8aHl+1OIXEkL497XImfcAtb8vdIGbvbHrRLXiixC/WAxoMtoDpg1cJibfjEqd0X5TFZNTAtSk0z8ZEOg1rxirscHAgMBAAECggEAU5VRQs09Rpt3jBimHnrjptM6pK5X/ewpXKRItpZS6rqqy4xQ6riKFYmrUEgrazOH5ploDTe4XMEmZXOvAP/f9bYXfZXvHLHrOpHnERDtP1XyfpaOBSmUvJyQCORgOz6/ZERiLqqdgyl8+gXC1IJkXH9yKD/cE/UcbUKBP/7BpFj7lPMyNCApiS1Z2RinvOSsx2TCBfVLpEE1dTLdHg3g3vfkmnn+KQ/SU4z3ksXJa0ODZY9lsUGWUrGmnhd/tviSuNUJG3wx7h1er4LBjuA4OZD8qJA+sXcEY2Kn7XQHAOBWUfAOR7nzAl3mPYycIZs4sDrq2awwX12ML9qR/40swQKBgQDtBhIML+Xt32fLw4/wtSDmDJo4szyu0c3Gangl4eMjOc1WEXl/bL8uryNS9b+1he8b+VgEBFH2nhl3u1eman0/xpk9hqj9hd/IDazMqUr7mKq+b9WXWd24LFZNew+35RUELW01FdEDSr+KZsCIjFilAeWfpJORoj3oZFU5C/5mQQKBgQDHXI7NqHy2ATqDiQI3aG72B8n3TbR9B8G01Anfn3ZKcXIFWnDHoB9y/ITYzGrjrbbEOD2BsAacOy7bOWHlX1RIcD10ZWJIBdjqc+zfpahb36mXbcEQkb7col5s992KGVZHu8OBwfGJMVHYprIxOmygj1CAF9pEZyMy3alHChOrRwKBgQCYeyxHHNVNh0huBLxn/Q5SEM9yJJSoXp6Dw+DRdhU6hyf687j26c3ASblu2Fvhem1N0MX3p5PXFPSLW0FS9PTof2n789JpbqN9Ppbo/wwW+ar2YlnFSXHi1tsac020XzJ7AoJcAVH6TS8V6W55KdipJqRDZIvux7IN++X7kiSyQQKBgQCweIIAEhCym0vMe0729P6j0ik5PBN0SZVyF+/VfzYal2kyy+fhDSBJjLWbovdLKs4Jyy7GyaZQTSMg8x5xB3130cLUcZoZ3vMwNgWLwvvQt59LZ9/qZtjoPOIQ2yfDwsHZJZ/eEGtZ4cptWMGLSgg16CZ9/J88xX8m24eoVocqqQKBgCEj/FK26bBLnPtRlQ+5mTQ/CjcjD5/KoaHLawULvXq03qIiZfDZg+sm7JUmlaC48sERGLJnjNYk/1pjw5N8txyAk2UHxqi+dayRkTCRSfBm0PUWyVWiperHNEuByHnyh+qX00sE3SCz2qDSDLb1x7kV+2BhEL+XfgD7evqrvrNq"""
|
||||
)
|
||||
|
||||
val httpSignatureSignerImpl = HttpSignatureSignerImpl()
|
||||
|
||||
val format = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US)
|
||||
format.timeZone = TimeZone.getTimeZone("GMT")
|
||||
|
||||
//language=JSON
|
||||
val requestBody = """{
|
||||
"hoge": "fuga"
|
||||
}"""
|
||||
|
||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
val encode = Base64Util.encode(sha256.digest(requestBody.toByteArray()))
|
||||
|
||||
val url = "https://example.com/"
|
||||
val headers = Headers.build {
|
||||
append("Date", "Fri, 13 Oct 2023 07:14:50 GMT")
|
||||
append("Host", URL(url).host)
|
||||
append("Digest", "SHA-256=$encode")
|
||||
}
|
||||
val sign = httpSignatureSignerImpl.sign(
|
||||
url,
|
||||
HttpMethod.Post,
|
||||
headers,
|
||||
requestBody,
|
||||
Key("https://example.com", privateKey, publicKey),
|
||||
listOf("(request-target)", "date", "host", "digest")
|
||||
)
|
||||
|
||||
val keyMap = object : KeyMap {
|
||||
override fun getPublicKey(keyId: String?): PublicKey {
|
||||
return publicKey
|
||||
}
|
||||
|
||||
override fun getPrivateKey(keyId: String?): PrivateKey {
|
||||
return privateKey
|
||||
}
|
||||
|
||||
override fun getSecretKey(keyId: String?): SecretKey {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
val verifier = SignatureHeaderVerifier.builder().keyMap(keyMap).build()
|
||||
|
||||
val headers1 = headers {
|
||||
appendAll(headers)
|
||||
append("Signature", sign.sign.signatureHeader)
|
||||
set("Digest", "aaaaaaaaaaaaaaaaafsadasfgafaaaaaaaaaaa")
|
||||
}
|
||||
|
||||
val httpMessage = object : HttpMessage, HttpRequest {
|
||||
override fun headerValues(name: String?): MutableList<String> {
|
||||
return name?.let { headers1.getAll(it) }.orEmpty().toMutableList()
|
||||
}
|
||||
|
||||
override fun addHeader(name: String?, value: String?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun method(): String {
|
||||
return "POST"
|
||||
}
|
||||
|
||||
override fun uri(): URI {
|
||||
return URI(url)
|
||||
}
|
||||
}
|
||||
val verify = verifier.verify(httpMessage)
|
||||
assertFalse(verify)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue