fix gradle
This commit is contained in:
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,11 +1,9 @@
|
|||||||
.gradle/
|
.gradle/
|
||||||
build/
|
|
||||||
/build/
|
|
||||||
app/build/
|
|
||||||
local.properties
|
|
||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
.DS_Store
|
local.properties
|
||||||
/captures
|
/build/
|
||||||
.externalNativeBuild
|
/captures/
|
||||||
.cxx
|
.externalNativeBuild/
|
||||||
|
.cxx/
|
||||||
|
app/build/
|
||||||
|
|||||||
48
README.md
48
README.md
@@ -1,36 +1,34 @@
|
|||||||
# KosSSH Mobile
|
# KosSSH Mobile
|
||||||
|
|
||||||
Android SSH mobile client starter project with interactive PTY, ANSI color rendering, custom terminal font size, keyboard helper keys, and password authentication.
|
Android SSH mobile client MVP, Kotlin + Android Views + JSch.
|
||||||
|
|
||||||
## Build
|
## SDK / Build
|
||||||
|
|
||||||
Open this folder in Android Studio (Ladybug or newer) and let Gradle sync. Then run on an Android 8.0+ device/emulator.
|
- `compileSdk = 37`
|
||||||
|
- `targetSdk = 37`
|
||||||
|
- `minSdk = 26`
|
||||||
|
- Android Gradle Plugin: `9.4.0`
|
||||||
|
- Gradle: `9.6.0`
|
||||||
|
- Java: 17
|
||||||
|
- Android Studio: use a version that supports API 37 (Android Studio Panda 3 or newer).
|
||||||
|
|
||||||
Or from a machine with Gradle/Android SDK configured:
|
Android 17 / API 37 requires apps targeting API 37+ that connect to LAN devices to declare and request `ACCESS_LOCAL_NETWORK` at runtime. KosSSH requests it when the user starts an SSH connection.
|
||||||
|
|
||||||
```bash
|
## Features
|
||||||
./gradlew assembleDebug
|
|
||||||
```
|
|
||||||
|
|
||||||
APK output:
|
- SSH-2 interactive PTY (`xterm-256color`)
|
||||||
`app/build/outputs/apk/debug/app-debug.apk`
|
- Password authentication
|
||||||
|
- ANSI SGR basic colors + bold
|
||||||
|
- Custom terminal font size
|
||||||
|
- GitHub Dark theme
|
||||||
|
- CTRL dialog, ESC, TAB and arrow keys
|
||||||
|
- Android software keyboard integration
|
||||||
|
- Scrollback buffer
|
||||||
|
|
||||||
## Current features
|
## Security note
|
||||||
|
|
||||||
- SSH shell over JSch (SSH-2)
|
The current MVP still disables strict host-key verification in `SshClient.kt` to simplify initial testing. Do not ship this configuration to production. The next hardening step should add a persistent known-hosts store and explicit host-key verification, followed by encrypted server profiles/private-key storage.
|
||||||
- PTY `xterm-256color`
|
|
||||||
- Interactive keyboard via InputConnection
|
|
||||||
- ANSI SGR basic 16-color + bright color + bold
|
|
||||||
- Adjustable monospace font size
|
|
||||||
- Dark terminal theme
|
|
||||||
- Ctrl+A-Z helper
|
|
||||||
- Tab/Esc/arrow helper buttons
|
|
||||||
- Host/port/user/password fields
|
|
||||||
|
|
||||||
## Important production hardening
|
## Android 17 note
|
||||||
|
|
||||||
This is a functional MVP, not yet a production-grade SSH client. Before shipping, replace `StrictHostKeyChecking=no` in `SshClient.kt` with persistent known-hosts / host-key fingerprint verification, add encrypted profile storage with Android Keystore, private-key authentication, SFTP, terminal resize (`windowChange`), robust VT100/VT220/VT420 parsing, selection/copy, scrollback UI, and connection lifecycle handling.
|
Android 17 (API 37) adds a runtime local-network permission. For SSH servers on a LAN, the app asks for `ACCESS_LOCAL_NETWORK` immediately before the connection is opened.
|
||||||
|
|
||||||
## Package
|
|
||||||
|
|
||||||
`com.bozz.kosssh`
|
|
||||||
|
|||||||
@@ -1,16 +1,33 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android { namespace = "com.bozz.kosssh"; compileSdk = 37
|
android {
|
||||||
defaultConfig { applicationId = "com.bozz.kosssh"; minSdk = 26; targetSdk = 35; versionCode = 1; versionName = "1.0.0" }
|
namespace = "com.bozz.kosssh"
|
||||||
buildToolsVersion = "35.0.0"
|
compileSdk = 37
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.bozz.kosssh"
|
||||||
|
minSdk = 26
|
||||||
|
targetSdk = 37
|
||||||
|
versionCode = 2
|
||||||
|
versionName = "1.1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
|
}
|
||||||
|
buildToolsVersion = "37.0.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.core:core-ktx:1.15.0")
|
implementation("androidx.core:core-ktx:1.17.0")
|
||||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||||
implementation("com.google.android.material:material:1.12.0")
|
implementation("com.google.android.material:material:1.13.0")
|
||||||
implementation("com.github.mwiede:jsch:0.2.23")
|
implementation("com.github.mwiede:jsch:0.2.23")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
|
||||||
<application android:theme="@style/Theme.KosSSHMobile" android:label="KosSSH Mobile" android:allowBackup="true" android:supportsRtl="true">
|
<application android:theme="@style/Theme.KosSSHMobile" android:label="KosSSH Mobile" android:allowBackup="true" android:supportsRtl="true">
|
||||||
<activity android:name=".ui.MainActivity" android:screenOrientation="unspecified" android:exported="true">
|
<activity android:name=".ui.MainActivity" android:screenOrientation="unspecified" android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|||||||
@@ -3,12 +3,17 @@ package com.bozz.kosssh.terminal
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.*
|
import android.graphics.*
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
|
import android.util.AttributeSet
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.inputmethod.*
|
import android.view.inputmethod.*
|
||||||
import java.util.ArrayDeque
|
import java.util.ArrayDeque
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class TerminalView(context: Context) : View(context) {
|
class TerminalView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : View(context, attrs, defStyleAttr) {
|
||||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { typeface = Typeface.MONOSPACE }
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { typeface = Typeface.MONOSPACE }
|
||||||
private val lines = ArrayDeque<MutableList<Cell>>()
|
private val lines = ArrayDeque<MutableList<Cell>>()
|
||||||
private var current = mutableListOf<Cell>()
|
private var current = mutableListOf<Cell>()
|
||||||
@@ -154,5 +159,6 @@ class TerminalView(context: Context) : View(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sendCtrl(letter: Char) { send(byteArrayOf((letter.uppercaseChar().code - 64).toByte())) }
|
fun sendCtrl(letter: Char) { send(byteArrayOf((letter.uppercaseChar().code - 64).toByte())) }
|
||||||
private fun send(s: String) { sender?.invoke(s.toByteArray(Charsets.UTF_8)) }
|
private fun send(s: String) { send(s.toByteArray(Charsets.UTF_8)) }
|
||||||
|
private fun send(data: ByteArray) { sender?.invoke(data) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.bozz.kosssh.ui
|
package com.bozz.kosssh.ui
|
||||||
|
|
||||||
import android.app.*
|
import android.app.*
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
@@ -10,6 +12,9 @@ import com.bozz.kosssh.ssh.SshClient
|
|||||||
import com.bozz.kosssh.terminal.TerminalView
|
import com.bozz.kosssh.terminal.TerminalView
|
||||||
|
|
||||||
class MainActivity : Activity() {
|
class MainActivity : Activity() {
|
||||||
|
companion object {
|
||||||
|
private const val REQUEST_LOCAL_NETWORK = 3701
|
||||||
|
}
|
||||||
private lateinit var terminal: TerminalView
|
private lateinit var terminal: TerminalView
|
||||||
private val ssh = SshClient()
|
private val ssh = SshClient()
|
||||||
private lateinit var status: TextView
|
private lateinit var status: TextView
|
||||||
@@ -38,9 +43,43 @@ class MainActivity : Activity() {
|
|||||||
val pass = findViewById<EditText>(R.id.password).text.toString()
|
val pass = findViewById<EditText>(R.id.password).text.toString()
|
||||||
if (host.isEmpty() || user.isEmpty()) { Toast.makeText(this,"Host dan username wajib diisi",Toast.LENGTH_SHORT).show(); return }
|
if (host.isEmpty() || user.isEmpty()) { Toast.makeText(this,"Host dan username wajib diisi",Toast.LENGTH_SHORT).show(); return }
|
||||||
terminal.clear()
|
terminal.clear()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 37 &&
|
||||||
|
checkSelfPermission(android.Manifest.permission.ACCESS_LOCAL_NETWORK) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
requestPermissions(
|
||||||
|
arrayOf(android.Manifest.permission.ACCESS_LOCAL_NETWORK),
|
||||||
|
REQUEST_LOCAL_NETWORK
|
||||||
|
)
|
||||||
|
pendingConnection = ConnectionRequest(host, port, user, pass)
|
||||||
|
status.text = "Menunggu izin jaringan lokal…"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
startSsh(host, port, user, pass)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ConnectionRequest(val host: String, val port: Int, val user: String, val pass: String)
|
||||||
|
private var pendingConnection: ConnectionRequest? = null
|
||||||
|
|
||||||
|
private fun startSsh(host: String, port: Int, user: String, pass: String) {
|
||||||
ssh.connect(host, port, user, pass, { data -> runOnUiThread { terminal.feed(data) } }, { s -> runOnUiThread { status.text = s } })
|
ssh.connect(host, port, user, pass, { data -> runOnUiThread { terminal.feed(data) } }, { s -> runOnUiThread { status.text = s } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
if (requestCode == REQUEST_LOCAL_NETWORK) {
|
||||||
|
val request = pendingConnection
|
||||||
|
pendingConnection = null
|
||||||
|
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED && request != null) {
|
||||||
|
startSsh(request.host, request.port, request.user, request.pass)
|
||||||
|
} else {
|
||||||
|
status.text = "Izin jaringan lokal ditolak"
|
||||||
|
Toast.makeText(this, "KosSSH membutuhkan izin jaringan lokal untuk koneksi SSH LAN.", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showCtrlDialog() {
|
private fun showCtrlDialog() {
|
||||||
val letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
val letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
AlertDialog.Builder(this).setTitle("CTRL + …").setItems(letters.map { "Ctrl + $it" }.toTypedArray()) { _, which -> terminal.sendCtrl(letters[which]) }.show()
|
AlertDialog.Builder(this).setTitle("CTRL + …").setItems(letters.map { "Ctrl + $it" }.toTypedArray()) { _, which -> terminal.sendCtrl(letters[which]) }.show()
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "8.6.1" apply false
|
id("com.android.application") version "9.3.2" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.4.10" apply false
|
|
||||||
}
|
}
|
||||||
|
|||||||
13
gradle/gradle-daemon-jvm.properties
Normal file
13
gradle/gradle-daemon-jvm.properties
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#This file is generated by updateDaemonJvm
|
||||||
|
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/b96cc4b9c59b5bff244af296e50d5ea3/redirect
|
||||||
|
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/fdfa2f9fadcf75fb9616fde209d60b55/redirect
|
||||||
|
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/b96cc4b9c59b5bff244af296e50d5ea3/redirect
|
||||||
|
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/fdfa2f9fadcf75fb9616fde209d60b55/redirect
|
||||||
|
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/97b61e8788c7490a5f53d8951b11ea7a/redirect
|
||||||
|
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/feb8af2956f2e263086e0a8f9a1df390/redirect
|
||||||
|
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/b96cc4b9c59b5bff244af296e50d5ea3/redirect
|
||||||
|
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/fdfa2f9fadcf75fb9616fde209d60b55/redirect
|
||||||
|
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/eff42deed1d8947129a4e66cd07a89f4/redirect
|
||||||
|
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/8fe418192094ee8db0da620bcb6b1629/redirect
|
||||||
|
toolchainVendor=JETBRAINS
|
||||||
|
toolchainVersion=25
|
||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -1,4 +1,21 @@
|
|||||||
pluginManagement { repositories { google(); mavenCentral(); gradlePluginPortal() } }
|
pluginManagement {
|
||||||
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS); repositories { google(); mavenCentral() } }
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugins {
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rootProject.name = "KosSSHMobile"
|
rootProject.name = "KosSSHMobile"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
Reference in New Issue
Block a user