mirror of https://github.com/usbharu/Hideout.git
commit
f43507383e
hideout/hideout-activitypub
build.gradle.kts
src
main/kotlin/dev/usbharu/hideout/activitypub
application/webfinger
config
interfaces/wellknown
test/kotlin
dev/usbharu/hideout/activitypub/application/webfinger
util
wellknown
|
@ -52,6 +52,7 @@ dependencies {
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.h2db)
|
testImplementation(libs.h2db)
|
||||||
testImplementation(libs.flyway.core)
|
testImplementation(libs.flyway.core)
|
||||||
|
testImplementation(libs.mockito.kotlin)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.application.webfinger
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.activitypub.interfaces.wellknown.Link
|
||||||
|
import dev.usbharu.hideout.activitypub.interfaces.wellknown.XRD
|
||||||
|
import dev.usbharu.hideout.core.application.shared.AbstractApplicationService
|
||||||
|
import dev.usbharu.hideout.core.application.shared.Transaction
|
||||||
|
import dev.usbharu.hideout.core.config.ApplicationConfig
|
||||||
|
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
||||||
|
import dev.usbharu.hideout.core.domain.model.support.domain.apHost
|
||||||
|
import dev.usbharu.hideout.core.domain.model.support.principal.Principal
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class WebFingerApplicationService(
|
||||||
|
transaction: Transaction,
|
||||||
|
private val applicationConfig: ApplicationConfig,
|
||||||
|
private val actorRepository: ActorRepository,
|
||||||
|
) : AbstractApplicationService<String, XRD>(transaction, logger) {
|
||||||
|
|
||||||
|
override suspend fun internalExecute(resource: String, principal: Principal): XRD {
|
||||||
|
if (resource.startsWith("acct:").not()) {
|
||||||
|
throw IllegalArgumentException("Parameter (resource) is invalid.")
|
||||||
|
}
|
||||||
|
val acct = resource.substringAfter("acct:")
|
||||||
|
|
||||||
|
val host = acct.substringAfter('@', "")
|
||||||
|
if (applicationConfig.url.apHost != host) {
|
||||||
|
throw IllegalArgumentException("Parameter (resource) is invalid.")
|
||||||
|
}
|
||||||
|
val username = acct.substringBefore('@', "")
|
||||||
|
if (username.isEmpty()) {
|
||||||
|
throw IllegalArgumentException("Actor is not found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val actor = actorRepository.findByNameAndDomain(username, applicationConfig.url.apHost)
|
||||||
|
?: throw IllegalArgumentException("Actor $username not found.")
|
||||||
|
|
||||||
|
return XRD(
|
||||||
|
listOf(Link("self", null, "application/activity+json", actor.url.toString())),
|
||||||
|
URI.create("acct:${actor.name.name}@${actor.domain.domain}")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = LoggerFactory.getLogger(WebFingerApplicationService::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package dev.usbharu.hideout.activitypub.config
|
package dev.usbharu.hideout.activitypub.config
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.application.hostmeta.Link
|
import dev.usbharu.hideout.activitypub.interfaces.wellknown.Link
|
||||||
import dev.usbharu.hideout.core.config.ApplicationConfig
|
import dev.usbharu.hideout.core.config.ApplicationConfig
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
@ -12,7 +12,8 @@ class WebFingerHostMetaLinkConfiguration(private val applicationConfig: Applicat
|
||||||
return Link(
|
return Link(
|
||||||
rel = "lrdd",
|
rel = "lrdd",
|
||||||
type = "application/jrd+json",
|
type = "application/jrd+json",
|
||||||
template = applicationConfig.url.resolve(".well-known/webfinger").toString() + "?resource={uri}"
|
template = applicationConfig.url.resolve(".well-known/webfinger").toString() + "?resource={uri}",
|
||||||
|
href = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package dev.usbharu.hideout.activitypub.interfaces.wellknown
|
package dev.usbharu.hideout.activitypub.interfaces.wellknown
|
||||||
|
|
||||||
import dev.usbharu.hideout.activitypub.application.hostmeta.Link
|
|
||||||
import dev.usbharu.hideout.activitypub.application.hostmeta.WebHostMetadata
|
|
||||||
import org.springframework.core.annotation.Order
|
import org.springframework.core.annotation.Order
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
|
@ -14,19 +12,19 @@ import org.springframework.web.bind.annotation.RestController
|
||||||
class HostmetaController(private val linkList: List<Link> = emptyList()) {
|
class HostmetaController(private val linkList: List<Link> = emptyList()) {
|
||||||
@Order(1)
|
@Order(1)
|
||||||
@GetMapping("/host-meta")
|
@GetMapping("/host-meta")
|
||||||
fun hostmeta(): ResponseEntity<WebHostMetadata> {
|
fun hostmeta(): ResponseEntity<XRD> {
|
||||||
return ResponseEntity.ok().contentType(MediaType("application", "xrd+xml"))
|
return ResponseEntity.ok().contentType(MediaType("application", "xrd+xml"))
|
||||||
.body(WebHostMetadata(linkList))
|
.body(XRD(linkList))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Order(2)
|
@Order(2)
|
||||||
@GetMapping("/host-meta", produces = ["application/json"])
|
@GetMapping("/host-meta", produces = ["application/json"])
|
||||||
fun hostmetaJson(): WebHostMetadata {
|
fun hostmetaJson(): XRD {
|
||||||
return WebHostMetadata(linkList)
|
return XRD(linkList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/host-meta.json", produces = ["application/json"])
|
@GetMapping("/host-meta.json", produces = ["application/json"])
|
||||||
fun hostmetaJson2(): WebHostMetadata {
|
fun hostmetaJson2(): XRD {
|
||||||
return WebHostMetadata(linkList)
|
return XRD(linkList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.interfaces.wellknown
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.activitypub.application.webfinger.WebFingerApplicationService
|
||||||
|
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/.well-known")
|
||||||
|
class WebFingerController(
|
||||||
|
private val webFingerApplicationService: WebFingerApplicationService,
|
||||||
|
) {
|
||||||
|
@GetMapping("/webfinger", produces = ["application/json"])
|
||||||
|
suspend fun webfinger(@RequestParam(name = "resource", required = true) resource: String): XRD =
|
||||||
|
webFingerApplicationService.execute(resource, Anonymous)
|
||||||
|
}
|
|
@ -1,20 +1,31 @@
|
||||||
package dev.usbharu.hideout.activitypub.application.hostmeta
|
package dev.usbharu.hideout.activitypub.interfaces.wellknown
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
|
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
|
||||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
|
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
|
||||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement
|
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
@JacksonXmlRootElement(localName = "XRD", namespace = "http://docs.oasis-open.org/ns/xri/xrd-1.0")
|
@JacksonXmlRootElement(localName = "XRD", namespace = "http://docs.oasis-open.org/ns/xri/xrd-1.0")
|
||||||
class WebHostMetadata(
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
data class XRD(
|
||||||
@JacksonXmlProperty(localName = "Link", namespace = "http://docs.oasis-open.org/ns/xri/xrd-1.0")
|
@JacksonXmlProperty(localName = "Link", namespace = "http://docs.oasis-open.org/ns/xri/xrd-1.0")
|
||||||
@JacksonXmlElementWrapper(useWrapping = false)
|
@JacksonXmlElementWrapper(useWrapping = false)
|
||||||
@JsonProperty("links")
|
@JsonProperty("links")
|
||||||
val links: List<Link>,
|
val links: List<Link>,
|
||||||
|
@JacksonXmlProperty(localName = "subject")
|
||||||
|
@JsonProperty(value = "subject")
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
val subject: URI? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Link(
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
data class Link(
|
||||||
@JacksonXmlProperty(localName = "rel", isAttribute = true) val rel: String,
|
@JacksonXmlProperty(localName = "rel", isAttribute = true) val rel: String,
|
||||||
@JacksonXmlProperty(localName = "template", isAttribute = true) val template: String,
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
@JacksonXmlProperty(localName = "template", isAttribute = true) val template: String?,
|
||||||
@JacksonXmlProperty(localName = "type", isAttribute = true) val type: String,
|
@JacksonXmlProperty(localName = "type", isAttribute = true) val type: String,
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
@JacksonXmlProperty(localName = "href", isAttribute = true) val href: String?,
|
||||||
)
|
)
|
|
@ -0,0 +1,98 @@
|
||||||
|
package dev.usbharu.hideout.activitypub.application.webfinger
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.activitypub.external.activitystreams.TestActorFactory
|
||||||
|
import dev.usbharu.hideout.activitypub.interfaces.wellknown.Link
|
||||||
|
import dev.usbharu.hideout.activitypub.interfaces.wellknown.XRD
|
||||||
|
import dev.usbharu.hideout.core.config.ApplicationConfig
|
||||||
|
import dev.usbharu.hideout.core.domain.model.actor.ActorRepository
|
||||||
|
import dev.usbharu.hideout.core.domain.model.support.principal.Anonymous
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.mockito.InjectMocks
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Spy
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
|
import org.mockito.kotlin.eq
|
||||||
|
import org.mockito.kotlin.times
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
|
import org.mockito.kotlin.whenever
|
||||||
|
import util.TestTransaction
|
||||||
|
import java.net.URI
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension::class)
|
||||||
|
class WebFingerApplicationServiceTest {
|
||||||
|
@InjectMocks
|
||||||
|
lateinit var webFingerApplicationService: WebFingerApplicationService
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
lateinit var actorRepository: ActorRepository
|
||||||
|
|
||||||
|
@Spy
|
||||||
|
val transaction = TestTransaction
|
||||||
|
|
||||||
|
@Spy
|
||||||
|
val applicationConfig = ApplicationConfig(URI.create("https://example.com"))
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun acctから始まらないとだめ() = runTest {
|
||||||
|
assertThrows<IllegalArgumentException> {
|
||||||
|
webFingerApplicationService.execute("a", Anonymous)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun ドメインが自ドメインと一致しないとだめ() = runTest {
|
||||||
|
assertThrows<IllegalArgumentException> {
|
||||||
|
webFingerApplicationService.execute("acct:test@remote.example.com", Anonymous)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `acct@username@hostはだめ`() = runTest {
|
||||||
|
assertThrows<IllegalArgumentException> {
|
||||||
|
webFingerApplicationService.execute("acct:@username@example.com", Anonymous)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun actorが存在しないとだめ() = runTest {
|
||||||
|
assertThrows<IllegalArgumentException> {
|
||||||
|
webFingerApplicationService.execute("acct:test2@example.com", Anonymous)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(actorRepository, times(1)).findByNameAndDomain(eq("test2"), eq("example.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun actorが存在したら返す() = runTest {
|
||||||
|
whenever(
|
||||||
|
actorRepository.findByNameAndDomain(
|
||||||
|
eq("test"), eq("example.com")
|
||||||
|
)
|
||||||
|
).thenReturn(
|
||||||
|
TestActorFactory.create(
|
||||||
|
actorName = "test",
|
||||||
|
domain = "example.com",
|
||||||
|
uri = URI.create("https://example.com/users/test")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val execute = webFingerApplicationService.execute("acct:test@example.com", Anonymous)
|
||||||
|
|
||||||
|
val expected = XRD(
|
||||||
|
listOf(
|
||||||
|
Link(
|
||||||
|
rel = "self",
|
||||||
|
href = "https://example.com/users/test",
|
||||||
|
type = "application/activity+json",
|
||||||
|
template = null
|
||||||
|
)
|
||||||
|
), URI.create("acct:test@example.com")
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(expected, execute)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.core.application.shared.Transaction
|
||||||
|
|
||||||
|
object TestTransaction : Transaction {
|
||||||
|
override suspend fun <T> transaction(block: suspend () -> T): T = block()
|
||||||
|
override suspend fun <T> transaction(transactionLevel: Int, block: suspend () -> T): T = block()
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
package wellknown
|
package wellknown
|
||||||
|
|
||||||
import dev.usbharu.hideout.SpringApplication
|
import dev.usbharu.hideout.SpringApplication
|
||||||
|
import org.flywaydb.core.Flyway
|
||||||
|
import org.junit.jupiter.api.AfterAll
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
@ -62,4 +64,13 @@ class HostMetaControllerTest {
|
||||||
.andExpect { status { isOk() } }
|
.andExpect { status { isOk() } }
|
||||||
.andExpect { content { contentType(MediaType("application", "json")) } }
|
.andExpect { content { contentType(MediaType("application", "json")) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@AfterAll
|
||||||
|
fun dropDatabase(@Autowired flyway: Flyway) {
|
||||||
|
flyway.clean()
|
||||||
|
flyway.migrate()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package wellknown
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.SpringApplication
|
||||||
|
import org.flywaydb.core.Flyway
|
||||||
|
import org.junit.jupiter.api.AfterAll
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
|
||||||
|
import org.springframework.test.context.jdbc.Sql
|
||||||
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
|
import org.springframework.test.web.servlet.get
|
||||||
|
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import org.springframework.web.context.WebApplicationContext
|
||||||
|
|
||||||
|
@SpringBootTest(classes = [SpringApplication::class])
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
@Transactional
|
||||||
|
@Sql("/sql/actors.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
|
||||||
|
class WebFingerControllerTest {
|
||||||
|
@Autowired
|
||||||
|
private lateinit var context: WebApplicationContext
|
||||||
|
|
||||||
|
private lateinit var mockMvc: MockMvc
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
mockMvc = MockMvcBuilders.webAppContextSetup(context)
|
||||||
|
.apply<DefaultMockMvcBuilder>(springSecurity())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun webfinger() {
|
||||||
|
mockMvc
|
||||||
|
.get("/.well-known/webfinger?resource=acct:test@example.com") {
|
||||||
|
accept(MediaType.APPLICATION_JSON)
|
||||||
|
}
|
||||||
|
.asyncDispatch()
|
||||||
|
.andDo { print() }
|
||||||
|
.andExpect { status { isOk() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `webfinger resourceが無いと400`() {
|
||||||
|
mockMvc
|
||||||
|
.get("/.well-known/webfinger") {
|
||||||
|
accept(MediaType.APPLICATION_JSON)
|
||||||
|
}
|
||||||
|
.andExpect { status { isBadRequest() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@AfterAll
|
||||||
|
fun dropDatabase(@Autowired flyway: Flyway) {
|
||||||
|
flyway.clean()
|
||||||
|
flyway.migrate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue