Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@ jobs:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
if: "${{ matrix.java == '25' && env.SONAR_TOKEN != '' }}"
run: ./gradlew :sshlib:sonar -Dsonar.projectVersion=${{ github.sha }}
run: ./gradlew sonar -Dsonar.projectVersion=${{ github.sha }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,7 @@ local.properties

# Git worktrees
.worktrees/

# Gradle dependency verification caches and backups
gradle/verification-keyring.gpg
gradle/verification-keyring.keys.bak
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ You can use it by running the following commands:
### Library API

```kotlin
val client = SshClient("example.com", 22)
val client = SshClient("example.com", port = 22, hostKeyVerifier = myVerifier)
client.connect()
client.authenticatePassword("user", "pass")

Expand Down Expand Up @@ -146,7 +146,7 @@ class MyAgentProvider : AgentProvider {
}

// Enable agent forwarding
val client = SshClient("bastion.example.com")
val client = SshClient("bastion.example.com", hostKeyVerifier = myVerifier)
client.connect()
client.authenticatePassword("user", "pass")
client.enableAgentForwarding(MyAgentProvider())
Expand Down
29 changes: 29 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ plugins {
alias(libs.plugins.publish) apply false
alias(libs.plugins.kover)
alias(libs.plugins.cyclonedx)
alias(libs.plugins.sonarqube)
}

allprojects {
Expand All @@ -34,9 +35,37 @@ allprojects {
}

dependencies {
kover(project(":protocol"))
kover(project(":sshlib"))
}

sonar {
properties {
property("sonar.projectName", "ConnectBot SSH Library")
property("sonar.projectKey", "connectbot_cbssh")
property("sonar.organization", "connectbot")
property("sonar.host.url", "https://sonarcloud.io")
property("sonar.coverage.jacoco.xmlReportPaths", "build/reports/kover/report.xml")
property("sonar.exclusions", "**/build/generated/**")
property("sonar.issue.ignore.multicriteria", "cognitiveComplexityConnection,cognitiveComplexitySftp,cognitiveComplexityTransport")
property("sonar.issue.ignore.multicriteria.cognitiveComplexityConnection.ruleKey", "kotlin:S3776")
property(
"sonar.issue.ignore.multicriteria.cognitiveComplexityConnection.resourceKey",
"**/src/main/kotlin/org/connectbot/sshlib/client/SshConnection.kt",
)
property("sonar.issue.ignore.multicriteria.cognitiveComplexitySftp.ruleKey", "kotlin:S3776")
property(
"sonar.issue.ignore.multicriteria.cognitiveComplexitySftp.resourceKey",
"**/src/main/kotlin/org/connectbot/sshlib/client/sftp/SftpDispatcher.kt",
)
property("sonar.issue.ignore.multicriteria.cognitiveComplexityTransport.ruleKey", "kotlin:S3776")
property(
"sonar.issue.ignore.multicriteria.cognitiveComplexityTransport.resourceKey",
"**/src/main/kotlin/org/connectbot/sshlib/transport/KtorTcpTransport.kt",
)
}
}

spotless {
ratchetFrom = "origin/main"

Expand Down
2 changes: 1 addition & 1 deletion docs/SK_AUTH.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class SkAuthHandler(
override suspend fun onKeyboardInteractivePrompt(...) = null
}

val client = SshClient("server.example.com")
val client = SshClient("server.example.com", hostKeyVerifier = myVerifier)
client.connect()
val result = client.authenticate("user", SkAuthHandler(...))
```
Expand Down
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=2g
org.gradle.configuration-cache=false
#org.gradle.configuration-cache.parallel=true

Expand Down
5,001 changes: 5,001 additions & 0 deletions gradle/verification-keyring.keys

Large diffs are not rendered by default.

422 changes: 422 additions & 0 deletions gradle/verification-metadata.xml

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions protocol/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.publish)
alias(libs.plugins.dokka)
alias(libs.plugins.kover)
alias(libs.plugins.cyclonedx)
`java-library`
}
Expand Down Expand Up @@ -76,9 +77,24 @@ sourceSets {
dependencies {
api(libs.kaitai.runtime)
implementation(kotlin("stdlib"))
testImplementation(kotlin("test"))
testImplementation(libs.junit.jupiter.api)
testRuntimeOnly(libs.junit.jupiter.engine)
kaitaiCompiler(libs.kaitai.compiler)
}

tasks.test {
useJUnitPlatform()
}
Comment thread
kruton marked this conversation as resolved.

kover {
currentProject {
sources {
excludeJava.set(true)
}
}
}

java {
withSourcesJar()
toolchain {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* ConnectBot SSH Library
* Copyright 2025-2026 Kenny Root
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.connectbot.sshlib.protocol

import io.kaitai.struct.KaitaiStream
import io.kaitai.struct.KaitaiStruct
import org.junit.jupiter.api.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class KaitaiUtilsTest {

@Test
fun `toByteArray serializes generated Kaitai struct`() {
val message = ByteString().apply {
setLenData(3)
setData(byteArrayOf(1, 2, 3))
}

assertContentEquals(byteArrayOf(0, 0, 0, 3, 1, 2, 3), message.toByteArray())
}

@Test
fun `toByteArray grows buffer when initial capacity is too small`() {
val payloadSize = 20 * 1024
val message = FixedPayloadStruct(payloadSize)

val bytes = message.toByteArray()

assertEquals(payloadSize, bytes.size)
assertEquals(0, bytes.first())
assertEquals((payloadSize - 1).toByte(), bytes.last())
}

@Test
fun `toByteArray fails when message exceeds maximum serialization buffer`() {
val message = FixedPayloadStruct(1024 * 1024 + 1)

val exception = assertFailsWith<IllegalStateException> {
message.toByteArray()
}
assertEquals("Kaitai message exceeds 1048576 byte serialization limit", exception.message)
}

private class FixedPayloadStruct(
private val payloadSize: Int,
) : KaitaiStruct.ReadWrite(null) {
override fun _write_Seq() {
_io.writeBytes(ByteArray(payloadSize) { it.toByte() })
}

override fun _check() {
_dirty = false
}

override fun _fetchInstances() = Unit

override fun _read() = Unit
}
}
7 changes: 4 additions & 3 deletions sshlib/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ package org.connectbot.sshlib {

public interface AuthHandler {
method public default suspend java.lang.Object? onAuthMethodsAvailable(java.util.Set<java.lang.String> methods, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public default suspend java.lang.Object? onBanner(java.lang.String message, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend java.lang.Object? onKeyboardInteractivePrompt(java.lang.String name, java.lang.String instruction, java.util.List<org.connectbot.sshlib.KeyboardInteractiveCallback.Prompt> prompts, kotlin.coroutines.Continuation<? super java.util.List<java.lang.String>?>);
method public suspend java.lang.Object? onPasswordNeeded(kotlin.coroutines.Continuation<? super java.lang.String?>);
method public suspend java.lang.Object? onPublicKeysNeeded(kotlin.coroutines.Continuation<? super java.util.List<org.connectbot.sshlib.AuthPublicKey>>);
Expand Down Expand Up @@ -546,9 +547,9 @@ package org.connectbot.sshlib {
}

public static final class SshClient.Companion {
method public operator org.connectbot.sshlib.SshClient invoke(java.lang.String host, optional int port, optional java.lang.String clientVersion);
method public operator org.connectbot.sshlib.SshClient invoke(java.lang.String host, org.connectbot.sshlib.HostKeyVerifier hostKeyVerifier, optional int port, optional java.lang.String clientVersion);
method public operator org.connectbot.sshlib.SshClient invoke(org.connectbot.sshlib.SshClientConfig config);
method public operator org.connectbot.sshlib.SshClient invoke(org.connectbot.sshlib.transport.TransportFactory transportFactory, optional java.lang.String clientVersion);
method public operator org.connectbot.sshlib.SshClient invoke(org.connectbot.sshlib.transport.TransportFactory transportFactory, org.connectbot.sshlib.HostKeyVerifier hostKeyVerifier, optional java.lang.String clientVersion);
}

public final class SshClientConfig {
Expand Down Expand Up @@ -693,7 +694,7 @@ package org.connectbot.sshlib.blocking {
ctor public BlockingSshClient(java.lang.String host, optional int port, org.connectbot.sshlib.HostKeyVerifier hostKeyVerifier, optional java.lang.String clientVersion);
ctor public BlockingSshClient(java.lang.String host, org.connectbot.sshlib.HostKeyVerifier hostKeyVerifier);
ctor public BlockingSshClient(org.connectbot.sshlib.SshClientConfig config);
ctor public BlockingSshClient(org.connectbot.sshlib.transport.TransportFactory transportFactory, optional java.lang.String clientVersion);
ctor public BlockingSshClient(org.connectbot.sshlib.transport.TransportFactory transportFactory, org.connectbot.sshlib.HostKeyVerifier hostKeyVerifier, optional java.lang.String clientVersion);
method @kotlin.jvm.Throws(exceptionClasses=SshException::class) public void authenticate(java.lang.String username, org.connectbot.sshlib.AuthHandler handler) throws org.connectbot.sshlib.SshException;
method @kotlin.jvm.Throws(exceptionClasses=SshException::class) public void authenticateKeyboardInteractive(java.lang.String username, org.connectbot.sshlib.KeyboardInteractiveCallback callback) throws org.connectbot.sshlib.SshException;
method @kotlin.jvm.Throws(exceptionClasses=SshException::class) public void authenticatePassword(java.lang.String username, java.lang.String password) throws org.connectbot.sshlib.SshException;
Expand Down
10 changes: 0 additions & 10 deletions sshlib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ plugins {
alias(libs.plugins.metalava)
alias(libs.plugins.kover)
alias(libs.plugins.cyclonedx)
alias(libs.plugins.sonarqube)
`java-library`
}

Expand Down Expand Up @@ -147,12 +146,3 @@ mavenPublishing {
}
}
}

sonar {
properties {
property("sonar.projectKey", "connectbot_cbssh")
property("sonar.organization", "connectbot")
property("sonar.host.url", "https://sonarcloud.io")
property("sonar.coverage.jacoco.xmlReportPaths", "build/reports/kover/report.xml")
}
}
8 changes: 6 additions & 2 deletions sshlib/src/main/kotlin/org/connectbot/sshlib/AuthHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ interface AuthHandler {
* Called when the server reports which authentication methods are available.
* Override to observe or log.
*/
suspend fun onAuthMethodsAvailable(methods: Set<String>) {}
suspend fun onAuthMethodsAvailable(methods: Set<String>) {
// Optional notification hook; default handlers do not need to observe it.
}

/**
* Return public keys to probe. Empty list skips public key auth.
Expand Down Expand Up @@ -76,7 +78,9 @@ interface AuthHandler {
* Called when the server sends an authentication banner (SSH_MSG_USERAUTH_BANNER).
* This is often used for out-of-band authentication instructions (e.g., a URL to visit).
*/
suspend fun onBanner(message: String) {}
suspend fun onBanner(message: String) {
// Optional notification hook; default handlers may ignore banners.
}
}

/**
Expand Down
11 changes: 8 additions & 3 deletions sshlib/src/main/kotlin/org/connectbot/sshlib/HostKeyVerifier.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* ConnectBot SSH Library
* Copyright 2025 Kenny Root
* Copyright 2025-2026 Kenny Root
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -35,8 +35,13 @@ interface HostKeyVerifier {
*/
suspend fun verify(key: PublicKey): Boolean

suspend fun addKeys(keys: List<PublicKey>) {}
suspend fun removeKeys(keys: List<PublicKey>) {}
suspend fun addKeys(keys: List<PublicKey>) {
// Optional persistence hook; read-only verifiers have nothing to store.
}

suspend fun removeKeys(keys: List<PublicKey>) {
// Optional persistence hook; read-only verifiers have nothing to remove.
}
}

/**
Expand Down
Loading
Loading