Skip to content
Closed
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 }}
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
1 change: 1 addition & 0 deletions protocol/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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 @@ -55,7 +56,7 @@
}
}

tasks.register<KaitaiTask>("kaitai") {

Check warning on line 59 in protocol/build.gradle.kts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define group and description for this task

See more on https://sonarcloud.io/project/issues?id=connectbot_cbssh&issues=AZ5L8aIuKEY278Pu0sBv&open=AZ5L8aIuKEY278Pu0sBv&pullRequest=155
ksyFiles.from(fileTree(kaitaiInputDir) { include("*.ksy") })
classpath = kaitaiCompiler
mainClass.set("io.kaitai.struct.JavaMain")
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
42 changes: 23 additions & 19 deletions sshlib/src/main/kotlin/org/connectbot/sshlib/SshClient.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 @@ -78,6 +78,10 @@ class SshClient private constructor(
) {
companion object {
private val logger = LoggerFactory.getLogger(SshClient::class.java)
private const val ERROR_NOT_CONNECTED = "Not connected"
private const val ERROR_NOT_CONNECTED_CONNECT_FIRST = "Not connected - call connect() first"
private const val ERROR_NOT_AUTHENTICATED = "Not authenticated"
private const val LOCALHOST = "127.0.0.1"

/**
* Create an SshClient for TCP connection to the specified host.
Expand Down Expand Up @@ -198,8 +202,8 @@ class SshClient private constructor(
suspend fun authenticatePassword(username: String, password: String): AuthResult {
val conn = connection
if (conn == null) {
logger.error("Not connected - call connect() first")
return AuthResult.Error("Not connected")
logger.error(ERROR_NOT_CONNECTED_CONNECT_FIRST)
return AuthResult.Error(ERROR_NOT_CONNECTED)
}

return try {
Expand Down Expand Up @@ -234,8 +238,8 @@ class SshClient private constructor(
): AuthResult {
val conn = connection
if (conn == null) {
logger.error("Not connected - call connect() first")
return AuthResult.Error("Not connected")
logger.error(ERROR_NOT_CONNECTED_CONNECT_FIRST)
return AuthResult.Error(ERROR_NOT_CONNECTED)
}

return try {
Expand Down Expand Up @@ -287,8 +291,8 @@ class SshClient private constructor(
): AuthResult {
val conn = connection
if (conn == null) {
logger.error("Not connected - call connect() first")
return AuthResult.Error("Not connected")
logger.error(ERROR_NOT_CONNECTED_CONNECT_FIRST)
return AuthResult.Error(ERROR_NOT_CONNECTED)
}

return try {
Expand Down Expand Up @@ -325,8 +329,8 @@ class SshClient private constructor(
suspend fun authenticate(username: String, handler: AuthHandler): AuthResult {
val conn = connection
if (conn == null) {
logger.error("Not connected - call connect() first")
return AuthResult.Error("Not connected")
logger.error(ERROR_NOT_CONNECTED_CONNECT_FIRST)
return AuthResult.Error(ERROR_NOT_CONNECTED)
}

return try {
Expand Down Expand Up @@ -427,7 +431,7 @@ class SshClient private constructor(
val conn = connection
if (conn == null || !authenticated) {
logger.error("Not authenticated - call connect() and authenticate first")
return SftpResult.IoError(IllegalStateException("Not authenticated"))
return SftpResult.IoError(IllegalStateException(ERROR_NOT_AUTHENTICATED))
}

return try {
Expand Down Expand Up @@ -463,7 +467,7 @@ class SshClient private constructor(
): PortForwarder? {
val conn = connection
if (conn == null || !authenticated) {
logger.error("Not authenticated")
logger.error(ERROR_NOT_AUTHENTICATED)
return null
}

Expand All @@ -487,7 +491,7 @@ class SshClient private constructor(
bindPort: Int,
remoteHost: String,
remotePort: Int,
): PortForwarder? = localPortForward(InetSocketAddress("127.0.0.1", bindPort), remoteHost, remotePort)
): PortForwarder? = localPortForward(InetSocketAddress(LOCALHOST, bindPort), remoteHost, remotePort)

/**
* Start remote port forwarding (RFC 4254 section 7.1).
Expand All @@ -509,7 +513,7 @@ class SshClient private constructor(
): PortForwarder? {
val conn = connection
if (conn == null || !authenticated) {
logger.error("Not authenticated")
logger.error(ERROR_NOT_AUTHENTICATED)
return null
}

Expand Down Expand Up @@ -537,7 +541,7 @@ class SshClient private constructor(
): PortForwarder? {
val conn = connection
if (conn == null || !authenticated) {
logger.error("Not authenticated")
logger.error(ERROR_NOT_AUTHENTICATED)
return null
}

Expand All @@ -559,7 +563,7 @@ class SshClient private constructor(
suspend fun dynamicPortForward(
bindPort: Int,
authenticator: Socks5Authenticator? = null,
): PortForwarder? = dynamicPortForward(InetSocketAddress("127.0.0.1", bindPort), authenticator)
): PortForwarder? = dynamicPortForward(InetSocketAddress(LOCALHOST, bindPort), authenticator)

/**
* Forward a pair of streams through an SSH direct-tcpip channel.
Expand All @@ -580,12 +584,12 @@ class SshClient private constructor(
writeChannel: ByteWriteChannel,
remoteHost: String,
remotePort: Int,
originAddr: String = "127.0.0.1",
originAddr: String = LOCALHOST,
originPort: Int = 0,
): StreamForwarder? {
val conn = connection
if (conn == null || !authenticated) {
logger.error("Not authenticated")
logger.error(ERROR_NOT_AUTHENTICATED)
return null
}

Expand Down Expand Up @@ -635,12 +639,12 @@ class SshClient private constructor(
fun openDirectTcpipTransport(
remoteHost: String,
remotePort: Int,
originAddr: String = "127.0.0.1",
originAddr: String = LOCALHOST,
originPort: Int = 0,
): TransportFactory? {
val conn = connection
if (conn == null || !authenticated) {
logger.error("Not authenticated")
logger.error(ERROR_NOT_AUTHENTICATED)
return null
}

Expand Down
Loading
Loading