Skip to content

Commit

Permalink
Merge pull request #75 from lightsparkdev/feat/demosigner
Browse files Browse the repository at this point in the history
Add a demo remote signing server
  • Loading branch information
jklein24 authored Sep 18, 2023
2 parents 6d76295 + d638b9a commit 598c1b2
Show file tree
Hide file tree
Showing 14 changed files with 326 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ data class WebhookEvent(
val data: JsonObject? = null,
)

const val SIGNATURE_HEADER = "lightspark-signature"

@OptIn(ExperimentalStdlibApi::class)
@Throws(LightsparkException::class)
fun verifyAndParseWebhook(
Expand Down
36 changes: 36 additions & 0 deletions remotesignerdemo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/
36 changes: 36 additions & 0 deletions remotesignerdemo/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
plugins {
kotlin("jvm")
id("io.ktor.plugin") version "2.3.3"
alias(libs.plugins.kotlinSerialization)
}

group = "com.lightspark"
version = "0.0.1"

task("prepareKotlinIdeaImport") {}
task("prepareKotlinBuildScriptModel") {}

application {
mainClass.set("com.lightspark.ApplicationKt")

val isDevelopment: Boolean = project.ext.has("development")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
}

dependencies {
implementation("io.ktor:ktor-server-core-jvm")
implementation("io.ktor:ktor-server-default-headers-jvm")
implementation("io.ktor:ktor-server-call-logging-jvm")
implementation("io.ktor:ktor-server-content-negotiation-jvm")
implementation("io.ktor:ktor-client-content-negotiation-jvm")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
implementation("io.ktor:ktor-server-auth-jvm")
implementation("io.ktor:ktor-server-compression-jvm")
implementation("io.ktor:ktor-server-netty-jvm")
implementation(project(":lightspark-sdk"))
implementation(project(":core"))
implementation(project(":crypto"))
implementation("ch.qos.logback:logback-classic:1.4.11")
testImplementation("io.ktor:ktor-server-tests-jvm")
testImplementation(libs.kotlin.test.junit)
}
1 change: 1 addition & 0 deletions remotesignerdemo/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kotlin.code.style=official
21 changes: 21 additions & 0 deletions remotesignerdemo/src/main/kotlin/com/lightspark/Application.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.lightspark

import com.lightspark.plugins.configureHTTP
import com.lightspark.plugins.configureMonitoring
import com.lightspark.plugins.configureRouting
import io.ktor.server.application.Application
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty

fun main() {
val port = System.getenv("PORT")?.toInt() ?: 8080
embeddedServer(Netty, port = port, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}

fun Application.module() {
val config = RemoteSigningConfig.fromEnv()
configureHTTP()
configureMonitoring()
configureRouting(config)
}
11 changes: 11 additions & 0 deletions remotesignerdemo/src/main/kotlin/com/lightspark/Logging.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.lightspark

import io.ktor.server.application.ApplicationCall

fun ApplicationCall.debugLog(message: String) {
application.environment.log.debug(message)
}

fun ApplicationCall.errorLog(message: String, exception: Throwable? = null) {
application.environment.log.error(message, exception)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@file:OptIn(ExperimentalStdlibApi::class)

package com.lightspark

data class RemoteSigningConfig(
val apiClientID: String,
val apiClientSecret: String,
val webhookSecret: String,
val masterSeedHex: String,
val clientBaseURL: String?,
) {
val masterSeed: ByteArray
get() = masterSeedHex.hexToByteArray()

companion object {
fun fromEnv(): RemoteSigningConfig {
return RemoteSigningConfig(
apiClientID = System.getenv("LIGHTSPARK_API_TOKEN_CLIENT_ID")
?: error("LIGHTSPARK_API_TOKEN_CLIENT_ID not set"),
apiClientSecret = System.getenv("LIGHTSPARK_API_TOKEN_CLIENT_SECRET")
?: error("LIGHTSPARK_API_TOKEN_CLIENT_SECRET not set"),
webhookSecret = System.getenv("LIGHTSPARK_WEBHOOK_SECRET")
?: error("LIGHTSPARK_WEBHOOK_SECRET not set"),
masterSeedHex = System.getenv("LIGHTSPARK_MASTER_SEED_HEX")
?: error("LIGHTSPARK_MASTER_SEED_HEX not set"),
clientBaseURL = System.getenv("LIGHTSPARK_EXAMPLE_BASE_URL"),
)
}
}
}
47 changes: 47 additions & 0 deletions remotesignerdemo/src/main/kotlin/com/lightspark/WebhookHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.lightspark

import com.lightspark.sdk.LightsparkCoroutinesClient
import com.lightspark.sdk.model.WebhookEventType
import com.lightspark.sdk.remotesigning.handleRemoteSigningEvent
import com.lightspark.sdk.uma.PubKeyResponse
import com.lightspark.sdk.webhooks.SIGNATURE_HEADER
import com.lightspark.sdk.webhooks.verifyAndParseWebhook
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.request.receiveText
import io.ktor.server.response.respond

suspend fun handleWebhookRequest(
call: ApplicationCall,
config: RemoteSigningConfig,
client: LightsparkCoroutinesClient
): String {
val signature = call.request.headers[SIGNATURE_HEADER] ?: run {
call.respond(HttpStatusCode.BadRequest, "Missing signature header.")
return "Missing signature header."
}
val webhookEvent = try {
val bodyBytes = call.receiveText().toByteArray()
verifyAndParseWebhook(bodyBytes, signature, config.webhookSecret)
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, "Invalid webhook request.")
return "Invalid webhook request or bad signature."
}

val response = when (webhookEvent.eventType) {
WebhookEventType.REMOTE_SIGNING -> try {
handleRemoteSigningEvent(client, webhookEvent, config.masterSeed)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, "Error handling remote signing event.")
return "Error handling remote signing event."
}
else -> {
call.respond(HttpStatusCode.OK)
return "Unhandled webhook event type ${webhookEvent.eventType}."
}
}

call.respond(HttpStatusCode.OK)

return response
}
35 changes: 35 additions & 0 deletions remotesignerdemo/src/main/kotlin/com/lightspark/plugins/HTTP.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.lightspark.plugins

import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.compression.Compression
import io.ktor.server.plugins.compression.deflate
import io.ktor.server.plugins.compression.gzip
import io.ktor.server.plugins.compression.minimumSize
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.defaultheaders.DefaultHeaders
import kotlinx.serialization.json.Json

fun Application.configureHTTP() {
install(DefaultHeaders) {
header("X-Engine", "Ktor") // will send this header with each response
}
install(ContentNegotiation) {
json(
Json {
prettyPrint = true
isLenient = true
},
)
}
install(Compression) {
gzip {
priority = 1.0
}
deflate {
priority = 10.0
minimumSize(1024) // condition
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.lightspark.plugins

import io.ktor.server.application.*
import io.ktor.server.plugins.callloging.*
import io.ktor.server.request.*
import org.slf4j.event.*

fun Application.configureMonitoring() {
install(CallLogging) {
level = Level.INFO
filter { call -> call.request.path().startsWith("/") }
}
}
35 changes: 35 additions & 0 deletions remotesignerdemo/src/main/kotlin/com/lightspark/plugins/Routing.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.lightspark.plugins

import com.lightspark.RemoteSigningConfig
import com.lightspark.debugLog
import com.lightspark.handleWebhookRequest
import com.lightspark.sdk.ClientConfig
import com.lightspark.sdk.LightsparkCoroutinesClient
import com.lightspark.sdk.auth.AccountApiTokenAuthProvider
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.response.respond
import io.ktor.server.routing.get
import io.ktor.server.routing.routing

fun Application.configureRouting(
config: RemoteSigningConfig,
client: LightsparkCoroutinesClient = LightsparkCoroutinesClient(
ClientConfig(
serverUrl = config.clientBaseURL ?: "api.lightspark.com",
authProvider = AccountApiTokenAuthProvider(config.apiClientID, config.apiClientSecret),
),
),
) {
routing {
get("/ping") {
call.respond(HttpStatusCode.NoContent)
}

get("/ln/webhooks") {
val response = handleWebhookRequest(call, config, client)
call.debugLog("Handled webhook event: $response")
}
}
}
12 changes: 12 additions & 0 deletions remotesignerdemo/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="trace">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>
</configuration>
46 changes: 46 additions & 0 deletions remotesignerdemo/src/test/kotlin/com/lightspark/ApplicationTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.lightspark

import com.lightspark.plugins.configureHTTP
import com.lightspark.plugins.configureRouting
import com.lightspark.sdk.crypto.Secp256k1
import com.lightspark.sdk.uma.InMemoryPublicKeyCache
import com.lightspark.sdk.uma.KtorUmaRequester
import com.lightspark.sdk.uma.KycStatus
import com.lightspark.sdk.uma.LnurlpResponse
import com.lightspark.sdk.uma.PayReqResponse
import com.lightspark.sdk.uma.UMA_VERSION_STRING
import com.lightspark.sdk.uma.UmaProtocolHelper
import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.testing.testApplication
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

@OptIn(ExperimentalStdlibApi::class)
class ApplicationTest {
val env = RemoteSigningConfig.fromEnv()

@Test
fun testPingRequest() = testApplication {
val client = createClient {
install(ContentNegotiation) {
json()
}
}
application {
configureHTTP()
configureRouting(env)
}
client.get("/ping").apply {
assertEquals(HttpStatusCode.NoContent, status)
}
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ include(":javatest")
include(":oauth")
include(":crypto")
include(":umaserverdemo")
include(":remotesignerdemo")

0 comments on commit 598c1b2

Please sign in to comment.