diff --git a/build.gradle.kts b/build.gradle.kts index fb59cb9..56b5cd7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,8 @@ plugins { kotlin("multiplatform") version Versions.kotlin kotlin("plugin.serialization") version Versions.kotlin id("app.cash.sqldelight") version Versions.sqlDelight + `java-library` + id("org.bytedeco.gradle-javacpp-platform").version("1.5.10") application } @@ -30,6 +32,7 @@ allprojects { maven("https://oss.sonatype.org/content/repositories/snapshots") mavenCentral() google() + maven("https://jitpack.io") } } @@ -108,10 +111,28 @@ kotlin { } sourceSets { + val jvmMain by getting { + // Include the native source directory if needed + resources.srcDirs("src/commonMain/kotlin/fr/acinq/lightning/vsock/native") + } commonMain { kotlin.srcDir(buildVersionsTask.map { it.destinationDir }) dependencies { - implementation("fr.acinq.lightning:lightning-kmp:${Versions.lightningKmp}") + implementation("com.github.raymond98.lightning-kmp:lightning-kmp:v1.6.2-FEECREDIT-8") + //implementation("com.github.raymond98.vsockj:vsockj-native:1.0.3") + //implementation("com.github.raymond98.vsockj:vsockj-core:1.0.3") + + implementation("org.bytedeco:javacpp:1.5.10") + implementation(kotlin("stdlib-jdk8")) + implementation("org.bytedeco:javacv-platform:1.5.10") + implementation("org.bytedeco:javacpp-presets:1.5.10") + + api("fr.acinq.bitcoin:bitcoin-kmp:${Versions.bitcoinKmpVersion}") + api("co.touchlab:kermit:${Versions.kermitLoggerVersion}") + api("org.jetbrains.kotlinx:kotlinx-datetime:${Versions.datetimeVersion}") + api(ktor("network")) + api(ktor("network-tls")) + // ktor serialization implementation(ktor("serialization-kotlinx-json")) // ktor server @@ -135,6 +156,7 @@ kotlin { jvmMain { dependencies { implementation("app.cash.sqldelight:sqlite-driver:${Versions.sqlDelight}") + implementation("fr.acinq.secp256k1:secp256k1-kmp-jni-jvm:${Versions.secpJniJvmVersion}") implementation(ktor("client-okhttp")) implementation("ch.qos.logback:logback-classic:1.2.3") } @@ -198,6 +220,9 @@ kotlin { application { mainClass = "fr.acinq.lightning.bin.MainKt" + + // Set java.library.path to include the directory where the shared library is generated + applicationDefaultJvmArgs = listOf("-Djava.library.path=${project.buildDir}/libs") } val cliScripts by tasks.register("cliScripts", CreateStartScripts::class) { @@ -211,6 +236,33 @@ tasks.startScripts { dependsOn(cliScripts) } +val compileNative by tasks.register("compileNative") { + group = "build" + description = "Compile the native C++ code into a shared library" + + val outputDir = file("${project.buildDir}/libs") + val nativeSourceDir = file("src/commonMain/kotlin/fr/acinq/lightning/vsock/native") + + // Locate the JNI headers - adjust these paths based on your actual JDK location + val jdkHome = System.getenv("JAVA_HOME") ?: "/usr/lib/jvm/default-java" + val jniIncludeDir = file("$jdkHome/include") + val jniPlatformIncludeDir = file("$jniIncludeDir/linux") // or "win32" for Windows, "darwin" for macOS + + inputs.dir(nativeSourceDir) + outputs.dir(outputDir) + + commandLine("g++", "-I$jniIncludeDir", "-I$jniPlatformIncludeDir", "-shared", "-o", outputDir.resolve("libjniVSockImpl.so"), nativeSourceDir.resolve("VSockImpl.cpp"), "-fPIC") +} + +// Ensure the native library is compiled before creating the distribution +tasks.withType { + dependsOn(compileNative) +} + +tasks.withType { + dependsOn(compileNative) +} + distributions { main { distributionBaseName = "phoenix" @@ -221,6 +273,7 @@ distributions { // forward std input when app is run via gradle (otherwise keyboard input will return EOF) tasks.withType { standardInput = System.`in` + dependsOn(compileNative) //This should not be the case for all platforms } sqldelight { diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 5d96b3b..7a499ef 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -1,9 +1,16 @@ object Versions { val kotlin = "1.9.23" val lightningKmp = "1.7.0-FEECREDIT-8" + val lightningKmpTag = "v1.6.2-FEECREDIT-8" val sqlDelight = "2.0.1" val okio = "3.8.0" val clikt = "4.2.2" val ktor = "2.3.8" fun ktor(module: String) = "io.ktor:ktor-$module:$ktor" + + //For local tests + val bitcoinKmpVersion = "0.19.0" + val kermitLoggerVersion = "2.0.2" + val datetimeVersion = "0.6.0" + val secpJniJvmVersion = "0.15.0" } \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt index ee0db36..b2bb8e1 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt @@ -337,7 +337,7 @@ class Api(private val nodeParams: NodeParams, private val peer: Peer, private va is Either.Left -> call.respondText(res.value.message.toString()) } } - post("/splicein") {//Manual splice-in + post("splicein") {//Manual splice-in val formParameters = call.receiveParameters() val amountSat = formParameters.getLong("amountSat").msat //the splice in command will send all the balance in wallet val feerate = FeeratePerKw(FeeratePerByte(formParameters.getLong("feerateSatByte").sat)) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt index aa54145..9706a80 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt @@ -11,6 +11,7 @@ import com.github.ajalt.clikt.output.MordantHelpFormatter import com.github.ajalt.clikt.parameters.groups.OptionGroup import com.github.ajalt.clikt.parameters.groups.provideDelegate import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.types.boolean import com.github.ajalt.clikt.parameters.types.choice import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.restrictTo @@ -40,7 +41,6 @@ import fr.acinq.lightning.bin.logs.stringTimestamp import fr.acinq.lightning.blockchain.electrum.ElectrumClient import fr.acinq.lightning.blockchain.electrum.ElectrumWatcher import fr.acinq.lightning.blockchain.mempool.MempoolSpaceClient -import fr.acinq.lightning.blockchain.mempool.MempoolSpaceWatcher import fr.acinq.lightning.crypto.LocalKeyManager import fr.acinq.lightning.db.ChannelsDb import fr.acinq.lightning.db.Databases @@ -54,6 +54,7 @@ import fr.acinq.lightning.utils.ServerAddress import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.sat import fr.acinq.lightning.utils.toByteVector +import fr.acinq.lightning.vsock.VsockServer import fr.acinq.phoenix.db.* import io.ktor.http.* import io.ktor.server.application.* @@ -66,7 +67,7 @@ import okio.buffer import okio.use import kotlin.system.exitProcess import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.minutes +//import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -100,9 +101,9 @@ class Phoenixd : CliktCommand() { else -> error("unsupported chain") } } - private val mempoolPollingInterval by option("--mempool-space-polling-interval-minutes", help = "Polling interval for mempool.space API", hidden = true) + /*private val mempoolPollingInterval by option("--mempool-space-polling-interval-minutes", help = "Polling interval for mempool.space API", hidden = true) .int().convert { it.minutes } - .default(10.minutes) + .default(10.minutes)*/ private val httpBindIp by option("--http-bind-ip", help = "Bind ip for the http api").default("127.0.0.1") private val httpBindPort by option("--http-bind-port", help = "Bind port for the http api").int().default(9740) private val httpPassword by option("--http-password", help = "Password for the http api") @@ -126,6 +127,14 @@ class Phoenixd : CliktCommand() { value } + //Electrum has a list of testnet and mainnet servers we can use to randomly find available ones https://github.com/spesmilo/electrum/blob/afa1a4d22a31d23d088c6670e1588eed32f7114d/lib/network.py#L57 + private val electrumServerIp by option("--electrum-server-ip", help = "An IP for the Electrum server").default("testnet.qtornado.com") + private val electrumServerPort by option("--electrum-server-port", help = "Port for the electrum server").int().default(51002) + + private val startVsock by option("--start-vsock-server", help = "Start the vsock server for API calls").boolean().default(true) + private val vsockCID by option("--vsock-server-cid", help = "CID for the Vsock server").int().default(4) + private val vsockPort by option("--vsock-server-port", help = "Port for the Vsock server").int().default(9001) + class LiquidityOptions : OptionGroup(name = "Liquidity Options") { val autoLiquidity by option("--auto-liquidity", help = "Amount automatically requested when inbound liquidity is needed").choice( "off" to 0.sat, @@ -133,7 +142,7 @@ class Phoenixd : CliktCommand() { "5m" to 5_000_000.sat, "10m" to 10_000_000.sat, ).default(2_000_000.sat, "2m") - val maxAbsoluteFee by option("--max-absolute-fee", hidden = true).deprecated("--max-absolute-fee is deprecated, use --max-mining-fee instead", error = true) + //val maxAbsoluteFee by option("--max-absolute-fee", hidden = true).deprecated("--max-absolute-fee is deprecated, use --max-mining-fee instead", error = true) val maxMiningFee by option("--max-mining-fee", help = "Max mining fee for on-chain operations, in satoshis") .int().convert { it.sat } .restrictTo(5_000.sat..200_000.sat) @@ -276,7 +285,7 @@ class Phoenixd : CliktCommand() { //val watcher = MempoolSpaceWatcher(mempoolSpace, scope, loggerFactory, pollingInterval = mempoolPollingInterval) val electrumClient = ElectrumClient(scope, nodeParams.loggerFactory) - val serverAddress = ServerAddress("electrum.acinq.co", 50002, TcpSocket.TLS.UNSAFE_CERTIFICATES) + val serverAddress = ServerAddress(electrumServerIp, electrumServerPort, TcpSocket.TLS.UNSAFE_CERTIFICATES) val socketBuilder = TcpSocket.Builder() runBlocking { @@ -382,6 +391,12 @@ class Phoenixd : CliktCommand() { peer.setAutoLiquidityParams(liquidityOptions.autoLiquidity) } + var vsockServer: VsockServer? = null + if(startVsock){ + vsockServer = VsockServer(vsockCID, vsockPort, httpBindPort, httpBindIp, loggerFactory) + vsockServer.start() + } + val server = embeddedServer(CIO, port = httpBindPort, host = httpBindIp, configure = { reuseAddress = true @@ -409,6 +424,7 @@ class Phoenixd : CliktCommand() { peerConnectionLoop.cancel() peer.disconnect() server.stop() + vsockServer?.stop() exitProcess(0) } server.environment.monitor.subscribe(ApplicationStopped) { consoleLog(brightYellow("http server stopped")) } diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Lsp.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Lsp.kt index 24ecf23..40fb579 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Lsp.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Lsp.kt @@ -42,9 +42,9 @@ data class LSP(val walletParams: WalletParams, val swapInXpub: String) { ) ) is Chain.Testnet -> LSP( - swapInXpub = "tpubDAmCFB21J9ExKBRPDcVxSvGs9jtcf8U1wWWbS1xTYmnUsuUHPCoFdCnEGxLE3THSWcQE48GHJnyz8XPbYUivBMbLSMBifFd3G9KmafkM9og", + swapInXpub = "tpubDCbLTyj9J59ygFSXyFxssuCFzLLD6ccfQmGdEmwb4miv33NAa7VyRdhDBJRUcsR987cMRm8ufCvdXBphT79QAYWczZJ8mqHeKtrXeE9PVD5", walletParams = WalletParams( - trampolineNode = NodeUri(PublicKey.fromHex("03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134"), "13.248.222.197", 9735), + trampolineNode = NodeUri(PublicKey.fromHex("02cb140e651b9bef52dfe8976ac08336414373d738d161fcf479f98a1ae4416c92"), "10.0.0.137", 9735), trampolineFees, invoiceDefaultRoutingFees, swapInParams diff --git a/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt b/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt index 24988b2..6dc6e26 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/bin/json/JsonSerializers.kt @@ -78,6 +78,13 @@ sealed class ApiType { val deeplyConfirmed: Long ) : ApiType() + @Serializable + data class VsockApiRequest( + val method: String, + val params: Map = emptyMap(), + val httpPassword: String + ) : ApiType() + @Serializable data class FinalWalletInfo(@SerialName("path") val path: String, @SerialName("xpub") val xpub: String) : ApiType() diff --git a/src/commonMain/kotlin/fr/acinq/lightning/vsock/BaseVSock.kt b/src/commonMain/kotlin/fr/acinq/lightning/vsock/BaseVSock.kt new file mode 100644 index 0000000..da725ad --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/vsock/BaseVSock.kt @@ -0,0 +1,72 @@ +package fr.acinq.lightning.vsock + +import fr.acinq.lightning.vsock.native.VSockImpl +import java.io.Closeable +import java.io.IOException +import java.net.SocketException + +abstract class BaseVSock : Closeable { + protected val closeLock: Any = Any() + + protected var isClosed: Boolean = false + protected var created: Boolean = false + protected var bound: Boolean = false + private var implementation: VSockImpl? = null + + @Throws(SocketException::class) + private fun createImplementation() { + implementation = VSockImpl() + implementation!!.create() + created = true + } + + @Throws(SocketException::class) + fun getImplementation(): VSockImpl? { + if (!created) createImplementation() + return implementation + } + + @Throws(SocketException::class) + fun setImplementation(): VSockImpl { + if (implementation == null) { + implementation = VSockImpl() + } + return implementation!! + } + + @get:Throws(IOException::class) + val localCid: Int + get() = getImplementation()!!.getLocalCid() + + @JvmOverloads + @Throws(IOException::class) + fun bind(address: VSockAddress?, backlog: Int = DEFAULT_BACKLOG) { + var backlog = backlog + if (isClosed) { + throw SocketException("Socket closed") + } + if (bound) { + throw SocketException("Socket already bound") + } + if (backlog <= 0) { + backlog = DEFAULT_BACKLOG + } + getImplementation()!!.bind(address) + getImplementation()!!.listen(backlog) + bound = true + } + + @Synchronized + @Throws(IOException::class) + override fun close() { + synchronized(closeLock) { + if (isClosed) return + if (created) getImplementation()!!.close() + isClosed = true + } + } + + companion object { + private const val DEFAULT_BACKLOG = 42 + } +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/vsock/ServerVSock.kt b/src/commonMain/kotlin/fr/acinq/lightning/vsock/ServerVSock.kt new file mode 100644 index 0000000..b6ec846 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/vsock/ServerVSock.kt @@ -0,0 +1,17 @@ +package fr.acinq.lightning.vsock + +import java.io.IOException +import java.net.SocketException + +class ServerVSock : BaseVSock() { + @Throws(IOException::class) + fun accept(): VSock { + if (isClosed) throw SocketException("Socket closed") + if (!bound) throw SocketException("Socket not bound") + val socket = VSock() + socket.setImplementation() + socket.getImplementation()?.let { getImplementation()!!.accept(it) } + socket.postAccept() + return socket + } +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/vsock/VSock.kt b/src/commonMain/kotlin/fr/acinq/lightning/vsock/VSock.kt new file mode 100644 index 0000000..b35e36f --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/vsock/VSock.kt @@ -0,0 +1,71 @@ +package fr.acinq.lightning.vsock + +import java.io.Closeable +import java.io.IOException +import java.net.SocketException + +class VSock : BaseVSock, Closeable { + private var connected = false + + @get:Throws(IOException::class) + @get:Synchronized + var outputStream: VSockOutputStream? = null + get() { + if (isClosed) { + throw SocketException("VSock is closed") + } + if (field == null) { + field = getImplementation()?.let { VSockOutputStream(it) } + } + return field + } + private set + + @get:Throws(IOException::class) + @get:Synchronized + var inputStream: VSockInputStream? = null + get() { + if (isClosed) { + throw SocketException("VSock is closed") + } + if (field == null) { + field = getImplementation()?.let { VSockInputStream(it) } + } + return field + } + private set + + constructor() + + constructor(address: VSockAddress?) { + try { + getImplementation()!!.connect(address) + } catch (e: Exception) { + try { + close() + } catch (ce: Exception) { + e.addSuppressed(ce) + } + throw IllegalStateException(e.message, e) + } + } + + @Throws(SocketException::class) + fun connect(address: VSockAddress?) { + if (isClosed) { + throw SocketException("Socket closed") + } + if (connected) { + throw SocketException("Socket already connected") + } + getImplementation()!!.connect(address) + connected = true + bound = true + } + + fun postAccept() { + created = true + bound = true + connected = true + } +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/vsock/VSockAddress.kt b/src/commonMain/kotlin/fr/acinq/lightning/vsock/VSockAddress.kt new file mode 100644 index 0000000..80829a1 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/vsock/VSockAddress.kt @@ -0,0 +1,35 @@ +package fr.acinq.lightning.vsock + +import java.net.SocketAddress +import java.util.Objects + +class VSockAddress(val cid: Int, val port: Int) : SocketAddress() { + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o == null || javaClass != o.javaClass) return false + val that = o as VSockAddress + return cid == that.cid && + port == that.port + } + + override fun hashCode(): Int { + return Objects.hash(cid, port) + } + + override fun toString(): String { + return "VSockAddress{" + + "cid=" + cid + + ", port=" + port + + '}' + } + + companion object { + const val VMADDR_CID_ANY: Int = -1 + const val VMADDR_CID_HYPERVISOR: Int = 0 + const val VMADDR_CID_RESERVED: Int = 1 + const val VMADDR_CID_HOST: Int = 2 + const val VMADDR_CID_PARENT: Int = 3 + + const val VMADDR_PORT_ANY: Int = -1 + } +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/vsock/VSockInputStream.kt b/src/commonMain/kotlin/fr/acinq/lightning/vsock/VSockInputStream.kt new file mode 100644 index 0000000..b07275b --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/vsock/VSockInputStream.kt @@ -0,0 +1,30 @@ +package fr.acinq.lightning.vsock + +import fr.acinq.lightning.vsock.native.VSockImpl +import java.io.IOException +import java.io.InputStream + +class VSockInputStream(private val vSock: VSockImpl) : InputStream() { + private lateinit var temp: ByteArray + + @Throws(IOException::class) + override fun read(b: ByteArray, off: Int, len: Int): Int { + return vSock.read(b, off, len) + } + + @Throws(IOException::class) + override fun read(): Int { + temp = ByteArray(1) + val n = read(temp, 0, 1) + if (n <= 0) { + return -1 + } + return temp[0].toInt() + } + + @Throws(IOException::class) + override fun close() { + vSock.close() + super.close() + } +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/vsock/VSockOutputStream.kt b/src/commonMain/kotlin/fr/acinq/lightning/vsock/VSockOutputStream.kt new file mode 100644 index 0000000..83c9690 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/vsock/VSockOutputStream.kt @@ -0,0 +1,26 @@ +package fr.acinq.lightning.vsock + +import fr.acinq.lightning.vsock.native.VSockImpl +import java.io.IOException +import java.io.OutputStream + +class VSockOutputStream internal constructor(private val vSock: VSockImpl) : OutputStream() { + private val temp = ByteArray(1) + + @Throws(IOException::class) + override fun write(b: Int) { + temp[0] = b.toByte() + this.write(temp, 0, 1) + } + + @Throws(IOException::class) + override fun write(b: ByteArray, off: Int, len: Int) { + vSock.write(b, off, len) + } + + @Throws(IOException::class) + override fun close() { + vSock.close() + super.close() + } +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/vsock/VsockMain.kt b/src/commonMain/kotlin/fr/acinq/lightning/vsock/VsockMain.kt new file mode 100644 index 0000000..893ecd9 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/vsock/VsockMain.kt @@ -0,0 +1,96 @@ +package fr.acinq.lightning.vsock + +import fr.acinq.lightning.bin.json.ApiType.* +import fr.acinq.lightning.logging.LoggerFactory +import fr.acinq.lightning.logging.debug +import fr.acinq.lightning.logging.error +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.util.Base64 + +class VsockServer(private val CID: Int, private val port: Int, httpBindPort: Int, host: String, loggerFactory: LoggerFactory) { + private var server: ServerVSock? = null + private val logger = loggerFactory.newLogger(this::class) + private val client = HttpClient() + private val apiBaseUrl: String = "${host}:${httpBindPort}" + private val bufferSize: Int = 4096 + + @OptIn(DelicateCoroutinesApi::class) + fun start() { + server = ServerVSock() + try { + server?.bind(VSockAddress(CID, port)) //For any CID use VSockAddress.VMADDR_CID_ANY + logger.debug { "Vsock Bound on Cid: ${server?.localCid}" } + + server?.accept()?.use { peerVSock -> + val buffer = ByteArray(bufferSize) + val bytesRead = peerVSock.inputStream?.read(buffer, 0, bufferSize) + if (bytesRead != null) { + if (bytesRead > 0) { + val receivedData = String(buffer, 0, bytesRead, StandardCharsets.UTF_8).trim() + logger.debug { "Received Data: $receivedData" } + + // Parse the received data into a http request + val apiRequest = try { + Json.decodeFromString(receivedData) + } catch (e: Exception) { + logger.error { "Failed to parse JSON: ${e.message}" } + peerVSock.outputStream?.write("Invalid JSON format".toByteArray(StandardCharsets.UTF_8)) + return + } + + // Handle the API call + GlobalScope.launch { + val response = handleApiCall(apiRequest) + peerVSock.outputStream?.write(response.toByteArray(StandardCharsets.UTF_8)) + } + } + } + } + } catch (ex: IOException) { + logger.error { "Error starting Vsock: ${ex.message}" } + } finally { + stop() + } + } + + private suspend fun handleApiCall(request: VsockApiRequest): String { + return try { + val url = "$apiBaseUrl/${request.method}" + val response: HttpResponse = client.get(url) { + headers { + append(HttpHeaders.Authorization, "Basic ${Base64.getEncoder().encodeToString("user:${request.httpPassword}".toByteArray())}") + } + url { + parameters.appendAll(Parameters.build { + request.params.forEach { (key, value) -> + append(key, value) + } + }) + } + } + + if (response.status == HttpStatusCode.OK) { + response.bodyAsText() + } else { + "API Error: ${response.status} - ${response.bodyAsText()}" + } + } catch (e: Exception) { + logger.error { "API call failed: ${e.message}" } + "API call failed: ${e.message}" + } + } + + fun stop() { + logger.debug { "Stopping Vsock Server" } + server?.close() + } +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/vsock/native/VSockImpl.cpp b/src/commonMain/kotlin/fr/acinq/lightning/vsock/native/VSockImpl.cpp new file mode 100644 index 0000000..600fc87 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/vsock/native/VSockImpl.cpp @@ -0,0 +1,195 @@ +#include +#include "VSockImpl.h" // Include the generated JNI header +#include +#include +#include +#include +#include +#include +#include + +#define JVM_IO_INTR (-2) +#define BUFFER_LEN 65536 +#define min(a, b) ((a) < (b) ? (a) : (b)) + +// Make sure to wrap all your JNI functions with extern "C" to avoid name mangling +extern "C" { + +// Native method implementations matching the JNI header + +JNIEXPORT void JNICALL +Java_fr_acinq_lightning_vsock_native_VSockImpl_socketCreate(JNIEnv *env, jobject thisObj) { +int fd = socket(AF_VSOCK, SOCK_STREAM, 0); +// Optionally store the socket descriptor in the Java object's field +} + +JNIEXPORT void JNICALL +Java_fr_acinq_lightning_vsock_native_VSockImpl_connect(JNIEnv *env, jobject thisObj, jobject addr) { +int fd = -1; // Assuming you have stored the fd somewhere accessible +if (fd == -1) { +env->ThrowNew(env->FindClass("java/net/SocketException"), "Socket is closed"); +return; +} + +// Get the cid and port from the VSockAddress object +jclass VSockAddressClass = env->GetObjectClass(addr); +jfieldID cidField = env->GetFieldID(VSockAddressClass, "cid", "I"); +jfieldID portField = env->GetFieldID(VSockAddressClass, "port", "I"); + +struct sockaddr_vm sock_addr; +std::memset(&sock_addr, 0, sizeof(struct sockaddr_vm)); +sock_addr.svm_family = AF_VSOCK; +sock_addr.svm_port = env->GetIntField(addr, portField); +sock_addr.svm_cid = env->GetIntField(addr, cidField); +int status = ::connect(fd, (struct sockaddr *)&sock_addr, sizeof(struct sockaddr_vm)); + +if (status != 0) { +env->ThrowNew(env->FindClass("java/net/ConnectException"), +("Connect failed with error no: " + std::to_string(errno)).c_str()); +} +} + +JNIEXPORT void JNICALL +Java_fr_acinq_lightning_vsock_native_VSockImpl_close(JNIEnv *env, jobject thisObj) { +int fd = -1; // Assuming you have stored the fd somewhere accessible +if (fd == -1) { +return; +} + +int status = ::close(fd); +fd = -1; + +if (status != 0) { +env->ThrowNew(env->FindClass("java/net/SocketException"), +("Close failed with error no: " + std::to_string(errno)).c_str()); +} +} + +JNIEXPORT void JNICALL +Java_fr_acinq_lightning_vsock_native_VSockImpl_write(JNIEnv *env, jobject thisObj, jbyteArray b, jint offset, jint len) { +int fd = -1; // Assuming you have stored the fd somewhere accessible +if (fd == -1) { +env->ThrowNew(env->FindClass("java/net/SocketException"), "Socket is closed"); +return; +} + +char BUF[BUFFER_LEN]; +while (len > 0) { +int chunkLen = min(BUFFER_LEN, len); +env->GetByteArrayRegion(b, offset, chunkLen, (jbyte *)BUF); + +int n = (int)::send(fd, BUF, chunkLen, 0); +if (n <= 0) { +env->ThrowNew(env->FindClass("java/net/SocketException"), "Write failed"); +return; +} + +len -= chunkLen; +offset += chunkLen; +} +} + +JNIEXPORT jint JNICALL + Java_fr_acinq_lightning_vsock_native_VSockImpl_read(JNIEnv *env, jobject thisObj, jbyteArray b, jint offset, jint len) { +int fd = -1; // Assuming you have stored the fd somewhere accessible +if (fd == -1) { +env->ThrowNew(env->FindClass("java/net/SocketException"), "Socket is closed"); +return -1; +} + +char *bufP = (char *)malloc((size_t)len); +int nread = (int)::recv(fd, bufP, len, 0); + +if (nread < 0) { +env->ThrowNew(env->FindClass("java/net/SocketException"), +("Read failed with error no: " + std::to_string(errno)).c_str()); +free(bufP); +return -1; +} + +env->SetByteArrayRegion(b, offset, nread, (jbyte *)bufP); +free(bufP); +return nread; +} + +JNIEXPORT void JNICALL +Java_fr_acinq_lightning_vsock_native_VSockImpl_bind(JNIEnv *env, jobject thisObj, jobject addr) { +int fd = -1; // Assuming you have stored the fd somewhere accessible +if (fd == -1) { +env->ThrowNew(env->FindClass("java/net/SocketException"), "Socket is closed"); +return; +} + +// Get the cid and port from the VSockAddress object +jclass VSockAddressClass = env->GetObjectClass(addr); +jfieldID cidField = env->GetFieldID(VSockAddressClass, "cid", "I"); +jfieldID portField = env->GetFieldID(VSockAddressClass, "port", "I"); + +struct sockaddr_vm sock_addr; +std::memset(&sock_addr, 0, sizeof(struct sockaddr_vm)); +sock_addr.svm_family = AF_VSOCK; +sock_addr.svm_port = env->GetIntField(addr, portField); +sock_addr.svm_cid = env->GetIntField(addr, cidField); + +int status = ::bind(fd, (struct sockaddr *)&sock_addr, sizeof(struct sockaddr_vm)); + +if (status != 0) { +env->ThrowNew(env->FindClass("java/net/BindException"), +("Bind failed with error no: " + std::to_string(errno)).c_str()); +} +} + +JNIEXPORT void JNICALL +Java_fr_acinq_lightning_vsock_native_VSockImpl_listen(JNIEnv *env, jobject thisObj, jint backlog) { +int fd = -1; // Assuming you have stored the fd somewhere accessible +if (fd == -1) { +env->ThrowNew(env->FindClass("java/net/SocketException"), "Socket is closed"); +return; +} + +int status = ::listen(fd, backlog); + +if (status != 0) { +env->ThrowNew(env->FindClass("java/net/SocketException"), +("Listen failed with error no: " + std::to_string(errno)).c_str()); +} +} + +JNIEXPORT void JNICALL +Java_fr_acinq_lightning_vsock_native_VSockImpl_accept(JNIEnv *env, jobject thisObj, jobject connectionVSock) { +int fd = -1; // Assuming you have stored the fd somewhere accessible +if (fd == -1) { +env->ThrowNew(env->FindClass("java/net/SocketException"), "Socket is closed"); +return; +} + +struct sockaddr_vm peer_addr; +socklen_t peer_addr_size = sizeof(struct sockaddr_vm); +int peer_fd = ::accept(fd, (struct sockaddr *)&peer_addr, &peer_addr_size); + +if (peer_fd == -1) { +env->ThrowNew(env->FindClass("java/net/SocketException"), +("Accept failed with error no: " + std::to_string(errno)).c_str()); +return; +} + +// Set the peer_fd in the Java connectionVSock object +jclass VSockImplClass = env->GetObjectClass(connectionVSock); +jfieldID fdField = env->GetFieldID(VSockImplClass, "fd", "I"); +env->SetIntField(connectionVSock, fdField, peer_fd); +} + +JNIEXPORT jint JNICALL + Java_fr_acinq_lightning_vsock_native_VSockImpl_getLocalCid(JNIEnv *env, jobject thisObj) { +int fd = -1; // Assuming you have stored the fd somewhere accessible +if (fd == -1) { +env->ThrowNew(env->FindClass("java/net/SocketException"), "Socket is closed"); +return -1; +} + +unsigned int cid; +ioctl(fd, IOCTL_VM_SOCKETS_GET_LOCAL_CID, &cid); +return (jint)cid; +} + +} // extern "C" diff --git a/src/commonMain/kotlin/fr/acinq/lightning/vsock/native/VSockImpl.h b/src/commonMain/kotlin/fr/acinq/lightning/vsock/native/VSockImpl.h new file mode 100644 index 0000000..627206e --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/vsock/native/VSockImpl.h @@ -0,0 +1,86 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class fr_acinq_lightning_vsock_native_VSockImpl */ + +#ifndef _Included_fr_acinq_lightning_vsock_native_VSockImpl +#define _Included_fr_acinq_lightning_vsock_native_VSockImpl +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Class: fr_acinq_lightning_vsock_native_VSockImpl + * Method: socketCreate + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_socketCreate +(JNIEnv *, jobject); + +/* + * Class: fr_acinq_lightning_vsock_native_VSockImpl + * Method: connect + * Signature: (Lfr/acinq/lightning/vsock/VSockAddress;)V + */ +JNIEXPORT void JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_connect +(JNIEnv *, jobject, jobject); + +/* + * Class: fr_acinq_lightning_vsock_native_VSockImpl + * Method: close + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_close +(JNIEnv *, jobject); + +/* + * Class: fr_acinq_lightning_vsock_native_VSockImpl + * Method: write + * Signature: ([BII)V + */ +JNIEXPORT void JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_write +(JNIEnv *, jobject, jbyteArray, jint, jint); + +/* + * Class: fr_acinq_lightning_vsock_native_VSockImpl + * Method: read + * Signature: ([BII)I + */ +JNIEXPORT jint JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_read + (JNIEnv *, jobject, jbyteArray, jint, jint); + +/* + * Class: fr_acinq_lightning_vsock_native_VSockImpl + * Method: bind + * Signature: (Lfr/acinq/lightning/vsock/VSockAddress;)V + */ +JNIEXPORT void JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_bind +(JNIEnv *, jobject, jobject); + +/* + * Class: fr_acinq_lightning_vsock_native_VSockImpl + * Method: listen + * Signature: (I)V + */ +JNIEXPORT void JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_listen +(JNIEnv *, jobject, jint); + +/* + * Class: fr_acinq_lightning_vsock_native_VSockImpl + * Method: accept + * Signature: (Lfr/acinq/lightning/vsock/native/VSockImpl;)V + */ +JNIEXPORT void JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_accept +(JNIEnv *, jobject, jobject); + +/* + * Class: fr_acinq_lightning_vsock_native_VSockImpl + * Method: getLocalCid + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_fr_acinq_lightning_vsock_native_VSockImpl_getLocalCid + (JNIEnv *, jobject); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/commonMain/kotlin/fr/acinq/lightning/vsock/native/VSockImpl.kt b/src/commonMain/kotlin/fr/acinq/lightning/vsock/native/VSockImpl.kt new file mode 100644 index 0000000..fa60369 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/vsock/native/VSockImpl.kt @@ -0,0 +1,59 @@ +package fr.acinq.lightning.vsock.native + +import fr.acinq.lightning.vsock.VSockAddress +import org.bytedeco.javacpp.Loader +import org.bytedeco.javacpp.Pointer +import org.bytedeco.javacpp.annotation.Namespace +import org.bytedeco.javacpp.annotation.Platform +import java.net.SocketException + + +@Platform(include = [ + "", + "", + "", + "", + "" +]) +@Namespace("vsock") +class VSockImpl() : Pointer() { + + init { + Loader.load() //load the native library + } + + var fd: Int = -1 + + @Throws(SocketException::class) + fun create() { + socketCreate() + } + + private external fun allocate() + + private external fun socketCreate() + + @Throws(Exception::class) + external fun connect(address: VSockAddress?) + + @Throws(Exception::class) + external override fun close() + + @Throws(Exception::class) + external fun write(b: ByteArray, offset: Int, len: Int) + + @Throws(Exception::class) + external fun read(b: ByteArray, offset: Int, len: Int): Int + + @Throws(Exception::class) + external fun bind(addr: fr.acinq.lightning.vsock.VSockAddress?) + + @Throws(Exception::class) + external fun listen(backlog: Int) + + @Throws(Exception::class) + external fun accept(peerVSock: VSockImpl) + + @Throws(Exception::class) + external fun getLocalCid(): Int +}