From dd6f43daf3264b82eaef5963c4c4dc64563dc9ec Mon Sep 17 00:00:00 2001
From: Matteo Provendola <82465468+mpedy@users.noreply.github.com>
Date: Sun, 21 Sep 2025 13:05:29 +0200
Subject: [PATCH] Lock and get geolocation
---
app/build.gradle | 3 +-
app/src/main/AndroidManifest.xml | 35 +-
.../com/meshcentral/agent/BootReceiver.kt | 21 +
.../java/com/meshcentral/agent/BootService.kt | 89 +
.../java/com/meshcentral/agent/DeviceAdmin.kt | 18 +
.../com/meshcentral/agent/MainActivity.kt | 80 +
.../java/com/meshcentral/agent/MeshAgent.kt | 2093 +++++++++--------
.../java/com/meshcentral/agent/MeshTunnel.kt | 27 +
app/src/main/res/values-de/strings.xml | 6 +
app/src/main/res/values/strings.xml | 6 +-
.../main/res/xml/device_admin_receiver.xml | 10 +
app/src/main/res/xml/root_preferences.xml | 7 +
build.gradle | 1 +
13 files changed, 1355 insertions(+), 1041 deletions(-)
create mode 100644 app/src/main/java/com/meshcentral/agent/BootReceiver.kt
create mode 100644 app/src/main/java/com/meshcentral/agent/BootService.kt
create mode 100644 app/src/main/java/com/meshcentral/agent/DeviceAdmin.kt
create mode 100644 app/src/main/res/xml/device_admin_receiver.xml
diff --git a/app/build.gradle b/app/build.gradle
index 1712de1..c462c68 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -56,7 +56,8 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
- implementation 'com.budiyev.android:code-scanner:2.1.0'
+ //implementation 'com.budiyev.android:code-scanner:2.1.0'
+ implementation 'com.github.yuriy-budiyev:code-scanner:2.3.0'
implementation 'com.karumi:dexter:6.2.2'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e618687..2221cb2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -17,6 +17,13 @@
+
+
+
+
+
+
+
-
-
-
@@ -63,5 +67,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/meshcentral/agent/BootReceiver.kt b/app/src/main/java/com/meshcentral/agent/BootReceiver.kt
new file mode 100644
index 0000000..5aba6de
--- /dev/null
+++ b/app/src/main/java/com/meshcentral/agent/BootReceiver.kt
@@ -0,0 +1,21 @@
+package com.meshcentral.agent
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.widget.Toast
+
+class BootReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent?) {
+ if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
+ val serviceIntent = Intent(context, BootService::class.java)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(serviceIntent)
+ } else {
+ context.startService(serviceIntent)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/meshcentral/agent/BootService.kt b/app/src/main/java/com/meshcentral/agent/BootService.kt
new file mode 100644
index 0000000..37349a1
--- /dev/null
+++ b/app/src/main/java/com/meshcentral/agent/BootService.kt
@@ -0,0 +1,89 @@
+package com.meshcentral.agent
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.content.SharedPreferences
+import android.os.Build
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import androidx.preference.PreferenceManager
+
+class BootService : Service() {
+ private val CHANNEL_ID = "BootServiceChannel"
+
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel()
+
+ val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle(getString(R.string.starting_meshcentral_bootservice))
+ .setContentText("")
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .build()
+
+ startForeground(1, notification)
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ performStartupTasks()
+
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ "Boot Service Channel",
+ NotificationManager.IMPORTANCE_HIGH
+ ).apply {
+ description = "Notification channel for BootService"
+ }
+
+ val manager = getSystemService(NotificationManager::class.java)
+ manager?.createNotificationChannel(channel)
+ }
+ }
+
+ private fun performStartupTasks() {
+ val pm: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
+ val g_autoStart = pm.getBoolean("pref_autostart", false)
+ if (g_autoStart == true) {
+ Thread {
+ try {
+ Thread.sleep(5000)
+ val launchIntent = Intent(this, MainActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val pendingIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ launchIntent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ pendingIntent.send()
+ } else {
+ startActivity(launchIntent)
+ }
+
+ } catch (e: Exception) {
+ }
+ }.start()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/meshcentral/agent/DeviceAdmin.kt b/app/src/main/java/com/meshcentral/agent/DeviceAdmin.kt
new file mode 100644
index 0000000..bcc059e
--- /dev/null
+++ b/app/src/main/java/com/meshcentral/agent/DeviceAdmin.kt
@@ -0,0 +1,18 @@
+package com.meshcentral.agent
+
+import android.app.admin.DeviceAdminReceiver
+import android.content.Context
+import android.content.Intent
+import android.widget.Toast
+
+class DeviceAdmin : DeviceAdminReceiver() {
+ override fun onEnabled(context: Context, intent: Intent) {
+ Toast.makeText(context,"Device Admin activated", Toast.LENGTH_SHORT).show()
+ //super.onEnabled(context, intent)
+ }
+
+ override fun onDisabled(context: Context, intent: Intent) {
+ Toast.makeText(context,"Device Admin disactivated", Toast.LENGTH_SHORT).show()
+ //super.onDisabled(context, intent)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/meshcentral/agent/MainActivity.kt b/app/src/main/java/com/meshcentral/agent/MainActivity.kt
index aca1448..94b412b 100644
--- a/app/src/main/java/com/meshcentral/agent/MainActivity.kt
+++ b/app/src/main/java/com/meshcentral/agent/MainActivity.kt
@@ -20,6 +20,7 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
+import android.os.PowerManager
import android.provider.Settings
import android.text.InputType
import android.util.Base64
@@ -54,6 +55,9 @@ import java.util.Date
import java.util.Random
import kotlin.math.absoluteValue
+import android.app.admin.DevicePolicyManager
+import android.content.ComponentName
+
// You can hardcode a server connection string into this application by setting this string.
// Make sure to replace all $ with \$ if your link string contains the $ character
@@ -79,6 +83,7 @@ var pageUrl : String? = null
var cameraPresent : Boolean = false
var pendingActivities : ArrayList = ArrayList()
var pushMessagingToken : String? = null
+var g_autoStart : Boolean = false
var g_autoConnect : Boolean = true
var g_autoConsent : Boolean = false
var g_userDisconnect : Boolean = false // Indicate user initiated disconnection
@@ -95,6 +100,10 @@ var g_desktop_frameRateLimiter : Int = 100
var g_auth_url : Uri? = null
class MainActivity : AppCompatActivity() {
+ lateinit var dpm: DevicePolicyManager
+ lateinit var compName: ComponentName
+ val REQUEST_ENABLE_ADMIN = 1001
+
var alert : AlertDialog? = null
lateinit var notificationChannel: NotificationChannel
lateinit var notificationManager: NotificationManager
@@ -105,7 +114,26 @@ class MainActivity : AppCompatActivity() {
Security.insertProviderAt(BouncyCastleProvider(), 1)
}
+ fun lockDevice() {
+ if(dpm.isAdminActive(compName)){
+ try{
+ dpm.lockNow()
+ }catch (e: java.lang.Exception){
+ println("Error in lockDevice: ${e.message}")
+ }
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
+ dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
+ compName = ComponentName(this, DeviceAdmin::class.java)
+ if (!dpm.isAdminActive(compName)) {
+ val intent = Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN).apply {
+ putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, compName)
+ putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, "Necessary for being able to lock device remotely")
+ }
+ startActivityForResult(intent, REQUEST_ENABLE_ADMIN)
+ }
g_mainActivity = this
val sharedPreferences = getSharedPreferences("meshagent", Context.MODE_PRIVATE)
if (hardCodedServerLink != null) {
@@ -182,6 +210,19 @@ class MainActivity : AppCompatActivity() {
if (g_autoConnect && !g_userDisconnect && (meshAgent == null)) {
toggleAgentConnection(false)
}
+ if(intent.getBooleanExtra("autoconnect",false)){
+ toggleAgentConnection(false)
+ }
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ setIntent(intent)
+ if (intent != null) {
+ if(intent.getBooleanExtra("autoconnect",false)){
+ toggleAgentConnection(false)
+ }
+ }
}
private fun sendConsoleMessage(msg: String) {
@@ -320,6 +361,12 @@ class MainActivity : AppCompatActivity() {
}
return
}
+ }else if(requestCode == REQUEST_ENABLE_ADMIN){
+ if (dpm.isAdminActive(compName)) {
+ Toast.makeText(this, "Device Admin activated", Toast.LENGTH_SHORT).show()
+ } else {
+ Toast.makeText(this, "Device Admin non activated", Toast.LENGTH_SHORT).show()
+ }
}
var pad : PendingActivityData? = null
@@ -491,6 +538,38 @@ class MainActivity : AppCompatActivity() {
if (permissions.isNotEmpty()) {
ActivityCompat.requestPermissions(this, permissions.toTypedArray(), REQUEST_ALL_PERMISSIONS)
}
+
+ if (!Settings.canDrawOverlays(this)) {
+ val intent = Intent(
+ Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
+ Uri.parse("package:$packageName")
+ )
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(intent)
+ }
+
+ // Check and add ignore battery optimization permissions if necessary
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
+ if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
+ val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
+ data = Uri.parse("package:$packageName")
+ }
+ startActivity(intent)
+ }
+ }
+
+ // Check and add post notifications permissions if necessary
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
+ != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
+ }
+ }
+
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION),100)
+ }
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
@@ -680,6 +759,7 @@ class MainActivity : AppCompatActivity() {
fun settingsChanged() {
this.runOnUiThread {
val pm: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
+ g_autoStart = pm.getBoolean("pref_autostart",false)
g_autoConnect = pm.getBoolean("pref_autoconnect", false)
g_autoConsent = pm.getBoolean("pref_autoconsent", false)
g_userDisconnect = false
diff --git a/app/src/main/java/com/meshcentral/agent/MeshAgent.kt b/app/src/main/java/com/meshcentral/agent/MeshAgent.kt
index 33158be..76d6728 100644
--- a/app/src/main/java/com/meshcentral/agent/MeshAgent.kt
+++ b/app/src/main/java/com/meshcentral/agent/MeshAgent.kt
@@ -1,1036 +1,1057 @@
-package com.meshcentral.agent
-
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.content.pm.PackageManager
-import android.graphics.*
-import android.hardware.camera2.CameraAccessException
-import android.hardware.camera2.CameraManager
-import android.net.Uri
-import android.os.*
-import android.provider.Settings
-import android.util.Base64
-import okhttp3.*
-import okio.ByteString
-import okio.ByteString.Companion.toByteString
-import org.json.JSONArray
-import org.json.JSONObject
-import java.io.ByteArrayInputStream
-import java.io.ByteArrayOutputStream
-import java.io.DataOutputStream
-import java.net.NetworkInterface
-import java.security.MessageDigest
-import java.security.Signature
-import java.security.cert.CertificateFactory
-import java.security.cert.X509Certificate
-import java.security.interfaces.RSAPublicKey
-import java.util.concurrent.TimeUnit
-import javax.net.ssl.HostnameVerifier
-import javax.net.ssl.SSLContext
-import javax.net.ssl.TrustManager
-import javax.net.ssl.X509TrustManager
-import kotlin.concurrent.thread
-import kotlin.random.Random
-
-
-class MeshUserInfo(userid: String, realname: String?, image: Bitmap?) {
- val userid: String = userid
- val realname: String? = realname
- val image: Bitmap? = image
- init {
- println("MeshUserInfo: $userid, $realname")
- }
-}
-
-class MeshAgent(parent: MainActivity, host: String, certHash: String, devGroupId: String) : WebSocketListener() {
- val parent : MainActivity = parent
- val host : String = host
- val serverCertHash: String = certHash
- val devGroupId: String = devGroupId
- var state : Int = 0 // 0 = Disconnected, 1 = Connecting, 2 = Authenticating, 3 = Connected
- var nonce : ByteArray? = null
- var serverNonce: ByteArray? = null
- var serverTlsCertHash: ByteArray? = null
- var serverTitle : String? = null
- var serverSubTitle : String? = null
- var serverImage : Bitmap? = null
- private var _webSocket: WebSocket? = null
- private var connectionState: Int = 0
- private var connectionTimer: CountDownTimer? = null
- private var lastBattState : JSONObject? = null
- private var lastNetInfo : String? = null
- var tunnels : ArrayList = ArrayList()
- var userinfo : HashMap = HashMap() // UserID -> MeshUserInfo
-
- init {
- //println("MeshAgent Constructor: ${host}, ${certHash}, $devGroupId")
- }
-
- fun Start() {
- //println("MeshAgent Start")
- UpdateState(1) // Switch to connecting
- startSocket()
- }
-
- fun Stop() {
- //println("MeshAgent Stop")
- stopSocket()
- UpdateState(0) // Switch to disconnected
- }
-
- fun UpdateState(newState: Int) {
- if (newState != state) {
- state = newState
- parent.agentStateChanged()
- }
- }
-
- fun ByteArray.toHex(): String {
- return joinToString("") { "%02x".format(it) }
- }
-
- private fun getUnsafeOkHttpClient(): OkHttpClient {
- // Create a trust manager that does not validate certificate chains
- val trustAllCerts = arrayOf(object : X509TrustManager {
- override fun checkClientTrusted(chain: Array?, authType: String?) {}
-
- override fun checkServerTrusted(chain: Array?, authType: String?) {
- serverTlsCertHash = MessageDigest.getInstance("SHA-384").digest(chain?.get(0)?.encoded)
- }
-
- override fun getAcceptedIssuers() = arrayOf()
- })
-
- // Install the special trust manager that records the certificate hash of the server
- val sslContext = SSLContext.getInstance("SSL")
- sslContext.init(null, trustAllCerts, java.security.SecureRandom())
-
- val sslSocketFactory = sslContext.socketFactory
-
- return OkHttpClient.Builder()
- .connectTimeout(20, TimeUnit.SECONDS)
- .readTimeout(60, TimeUnit.MINUTES)
- .writeTimeout(60, TimeUnit.MINUTES)
- .hostnameVerifier(hostnameVerifier = HostnameVerifier { _, _ -> true })
- .sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
- .build()
- }
-
-
- fun startSocket() {
- _webSocket = getUnsafeOkHttpClient().newWebSocket(
- Request.Builder().url("wss://$host/agent.ashx").build(),
- this
- )
- //socketOkHttpClient.dispatcher.executorService.shutdown()
- }
-
- fun stopSocket() {
- // Disconnect and clear the control web socket
- if (_webSocket != null) {
- try {
- _webSocket?.close(NORMAL_CLOSURE_STATUS, null)
- _webSocket = null
- } catch (ex: Exception) { }
- }
- // Clear the connection timer
- if (connectionTimer != null) {
- connectionTimer?.cancel()
- connectionTimer = null
- }
- // Clear all relay tunnels, create a mutable list since the list may change when calling Stop()
- var tunnelsClone : MutableList = tunnels.toMutableList()
- for (t in tunnelsClone) { t.Stop() }
- tunnels.clear()
- // Update the state to disconnected
- UpdateState(0) // Switch to disconnected
- }
-
- companion object {
- const val NORMAL_CLOSURE_STATUS = 1000
- }
-
- override fun onOpen(webSocket: WebSocket, response: Response) {
- //println("onOpen")
- UpdateState(2) // Switch to connected
- nonce = Random.Default.nextBytes(48)
-
- // Start authenticate the mesh agent by sending a auth nonce & server TLS cert hash.
- // Send 384 bits SHA384 hash of TLS cert public key + 384 bits nonce
- var header = ByteArray(2)
- header[1] = 1
- webSocket.send(header.plus(serverTlsCertHash!!).plus(nonce!!).toByteString()); // Command 1, hash + nonce
- }
-
- override fun onMessage(webSocket: WebSocket, text: String) {
- //println("onMessage: $text")
- }
-
- override fun onMessage(webSocket: WebSocket, msg: ByteString) {
- try {
- //println("onBinaryMessage: ${msg.size}, ${msg.toByteArray().toHex()}")
- if (msg.size < 2) return;
- if ((connectionState == 3) && (msg[0].toInt() == 123)) {
- // If we are authenticated, process JSON data
- processAgentData(String(msg.toByteArray(), Charsets.UTF_8))
- return
- }
-
- var cmd : Int = (msg[0].toInt() shl 8) + msg[1].toInt()
- //println("Cmd $cmd, Size: ${msg.size}")
- when (cmd) {
- 1 -> {
- // Server authentication request
- if (msg.size != 98) return;
- var serverCertHash = msg.substring(2, 50).toByteArray()
- if (!serverCertHash.contentEquals(serverTlsCertHash!!)) {
- println("Server Hash Mismatch, given=${serverCertHash.toHex()}, computed=${serverTlsCertHash?.toHex()}")
- stopSocket()
- return
- }
- serverNonce = msg.substring(50).toByteArray()
-
- // Hash the server cert hash, server nonce and client nonce and sign the result
- val sig = Signature.getInstance("SHA384withRSA")
- sig.initSign(agentCertificateKey)
- sig.update(msg.substring(2).toByteArray().plus(nonce!!))
- val signature = sig.sign()
-
- // Construct the response [2, sideOfCert, Cert, Signature]
- var header = ByteArray(2)
- header[1] = 2
- var certLen = agentCertificate!!.encoded.size
- var agentCertLenBytes = ByteArray(2)
- agentCertLenBytes[0] = (certLen shr 8).toByte()
- agentCertLenBytes[1] = (certLen and 0xFF).toByte()
-
- // Send the response
- webSocket.send(header.plus(agentCertLenBytes).plus(agentCertificate!!.encoded).plus(signature).toByteString())
- }
- 2 -> {
- // Server agent certificate
- var xcertLen: Int = (msg[2].toUByte().toInt() shl 8) + msg[3].toUByte().toInt()
- var xcertBytes = msg.substring(4, 4 + xcertLen)
- var xagentCertificate = CertificateFactory.getInstance("X509").generateCertificate(
- ByteArrayInputStream(xcertBytes.toByteArray())
- ) as X509Certificate
-
- // The private key DER encoding contains the private key type, we don't want
- // that when hashing the private so we remove the first 24 bytes.
- var pkey = xagentCertificate.publicKey as RSAPublicKey
- var serverid = MessageDigest.getInstance("SHA-384").digest(pkey.encoded.toByteString().substring(24).toByteArray())
- var serveridb64 = Base64.encodeToString(serverid, Base64.NO_WRAP)
- serveridb64 = serveridb64.replace('/', '$').replace('+', '@')
- // If invalid server certificate, disconnect
- if (serveridb64.compareTo(serverCertHash) != 0) {
- println("Invalid Server Certificate Hash"); stopSocket(); return
- }
-
- // Verify server signature
- var signBlock: ByteArray? = serverTlsCertHash!!.plus(nonce!!).plus(serverNonce!!)
- val sig = Signature.getInstance("SHA384withRSA")
- sig.initVerify(xagentCertificate)
- sig.update(signBlock)
- if (!sig.verify(msg.substring(4 + xcertLen).toByteArray())) {
- println("Invalid Server Signature"); stopSocket(); return
- }
-
- // Everything is ok, server is valid.
- connectionState = connectionState or 1
-
- //println("Host: ${android.os.Build.HOST}")
- var agentid = 14; // This of agent (14, Android in this case)
- var agentver = 0 // Agent version (TODO)
- var platfromType = 3; // This is the icon: 1 = Desktop, 2 = Laptop, 3 = Mobile, 4 = Server, 5 = Disk, 6 = Router
- var capabilities = 12; // Capabilities of the agent (bitmask): 1 = Desktop, 2 = Terminal, 4 = Files, 8 = Console, 16 = JavaScript
- var deviceName: String? = null;
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
- deviceName = Settings.Secure.getString(parent.contentResolver, "bluetooth_name");
- }
- if (deviceName == null) {
- deviceName = Settings.Global.getString(parent.contentResolver, Settings.Global.DEVICE_NAME) ?: "UNKNOWN_DEVICE_NAME";
- }
- val deviceNameUtf = deviceName.toByteArray(Charsets.UTF_8)
- //println("DeviceName: ${deviceName}")
-
- var devGroupIdBytes: ByteArray = Base64.decode(devGroupId.replace('$', '/').replace('@', '+'), Base64.DEFAULT)
-
- // Command 3: infover, agentid, agentversion, platformtype, meshid, capabilities, computername
- var bytesOut = ByteArrayOutputStream()
- DataOutputStream(bytesOut).use { dos ->
- with(dos) {
- writeShort(3)
- writeInt(1)
- writeInt(agentid)
- writeInt(agentver)
- writeInt(platfromType)
- write(devGroupIdBytes)
- writeInt(capabilities)
- writeShort(deviceNameUtf.size)
- write(deviceNameUtf)
- }
- }
- webSocket.send(bytesOut.toByteArray().toByteString())
- if (connectionState == 3) connectHandler()
- }
- 4 -> {
- // Server confirmed authentication, we are allowed to send commands to the server
- connectionState = connectionState or 2
- if (connectionState == 3) connectHandler()
- }
- else -> {
- // Unknown command, ignore it.
-
- }
- }
- }
- catch (e: Exception) {
- println("Exception: ${e.toString()}")
- }
- }
-
- override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
- //println("onClosing")
- stopSocket()
- }
-
- override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
- println("onFailure ${t.toString()}, ${response.toString()}")
- stopSocket()
- }
-
- private fun connectHandler() {
- //println("Connected and verified")
- UpdateState(3) // Switch to connected and verified
- startConnectionTimer()
- sendCoreInfo()
- sendNetworkUpdate(false)
- sendServerImageRequest()
-
- if (g_autoConsent) {
- parent.startProjection()
- }
-
- // Send battery state
- if (_webSocket != null) { _webSocket?.send(getSysBatteryInfo().toString().toByteArray().toByteString()) }
- }
-
- // Cause some data to be sent over the websocket control channel every 2 minutes to keep it open
- private fun startConnectionTimer() {
- parent.runOnUiThread {
- connectionTimer = object: CountDownTimer(120000000, 120000) {
- override fun onTick(millisUntilFinished: Long) {
- if (sendNetworkUpdate(false) == false) { // See if we need to update network information
- if (_webSocket != null) {
- _webSocket?.send(ByteArray(1).toByteString()) // If not, sent a single zero byte
- }
- }
- }
- override fun onFinish() { startConnectionTimer() }
- }
- connectionTimer?.start()
- }
- }
-
- private fun processAgentData(jsonStr: String) {
- //println("JSON: $jsonStr")
- try {
- val json = JSONObject(jsonStr)
- var action = json.getString("action")
- //println("action: $action")
- when (action) {
- "ping" -> {
- // Return a pong
- val r = JSONObject()
- r.put("action", "pong")
- if (_webSocket != null) {
- _webSocket?.send(r.toString().toByteArray().toByteString())
- }
- }
- "pong" -> {
- // Nop
- }
- "sysinfo" -> {
- //println("SysInfo")
- val s = JSONObject()
- s.put("mobile", getSysBuildInfo())
- var identifiers = JSONObject()
- identifiers.put("storage_devices", getStorageInfo())
- s.put("identifiers", identifiers)
- val t = JSONObject()
- t.put("hardware", s)
- var hash1 = MessageDigest.getInstance("SHA-384").digest(t.toString().toByteArray()).toHex()
- var hash2: String? = null
- if (json.isNull("hash") == false) {
- hash2 = json.getString("hash")
- }
- if ((hash2 == null) || (hash1.contentEquals(hash2) == false)) {
- t.put("hash", hash1)
- val r = JSONObject()
- r.put("action", "sysinfo")
- r.put("data", t)
- //println(r.toString())
- if (_webSocket != null) {
- _webSocket?.send(r.toString().toByteArray().toByteString())
- }
- }
- }
- "netinfo" -> {
- sendNetworkUpdate(true)
- }
- "openUrl" -> {
- /*
- if (visibleScreen != 2) { // Device is busy in QR code scanner
- // Open the URL
- var xurl = json.optString("url")
- //println("Opening: $xurl")
- if ((xurl != null) && (parent.openUrl(xurl))) {
- // Event to the server
- var eventArgs = JSONArray()
- eventArgs.put(xurl)
- logServerEventEx(20, eventArgs, "Opening: ${xurl}", json);
- }
- }
- */
-
- var xurl = json.optString("url")
- if (xurl != null) {
- var getintent: Intent = Intent(Intent.ACTION_VIEW, Uri.parse(xurl));
- parent.startActivity(getintent);
- }
- }
- "msg" -> {
- var msgtype = json.getString("type")
- when (msgtype) {
- "console" -> {
- processConsoleMessage(json.getString("value"), json.getString("sessionid"), json)
- }
- "tunnel" -> {
- /*
- {"action":"msg",
- "type":"tunnel",
- "value":"*\/meshrelay.ashx?...",
- "usage":5,
- "servertlshash":"97eaf674eab131d3775f12cfa9c978d185a0e9caaf3a854bf0eb4ff94c2d6c53ca3dc456da149002804666fbbdae2fc9",
- "soptions":{
- "consentTitle":"MeshCentral",
- "consentMsgDesktop":"{0} ({1}) requesting remote desktop access. Grant access?",
- "consentMsgTerminal":"{0} ({1}) requesting remote terminal access. Grant access?",
- "consentMsgFiles":"{0} ({1}) requesting remote files access. Grant access?",
- "notifyTitle":"MeshCentral",
- "notifyMsgDesktop":"{0} ({1}) started a remote desktop session.",
- "notifyMsgTerminal":"{0} ({1}) started a remote terminal session.",
- "notifyMsgFiles":"{0} ({1}) started a remote files session."},
- "userid":"user//admin","perMessageDeflate":true,
- "sessionid":"user//admin/1fc5a4e5ab144a721b852118de898e5f1af5b791",
- "rights":4294967295,
- "consent":0,
- "username":"admin",
- "remoteaddr":"192.168.2.182",
- "privacybartext":"This is a test: {0} ({1})"}
- */
-
- var url = json.getString("value")
- if (url.startsWith("*/")) {
- var hostdns: String = host
- var i = host.indexOf('/') // If the hostname includes an extra domain, remove it.
- if (i > 0) {
- hostdns = host.substring(0, i); }
- url = "wss://$hostdns" + url.substring(1)
- }
- val tunnel = MeshTunnel(this, url, json)
- tunnels.add(tunnel)
- tunnel.Start()
- }
- else -> {
- // Unknown message type, ignore it.
- println("Unhandled msg type: $msgtype")
- }
- }
- }
- "coredump" -> {
- // Nop
- }
- "getcoredump" -> {
- // Nop
- }
- "getUserImage" -> {
- // User real name and optional image
- var xuserid: String? = json.optString("userid")
- var xrealname: String? = json.optString("realname")
- var ximage: String? = json.optString("image")
- var xuserImage: Bitmap? = null
-
- if ((ximage != null) && (!ximage.startsWith("data:image/jpeg;base64,"))) {
- ximage = null; }
-
- if (ximage != null) {
- try {
- val imageBytes = android.util.Base64.decode(ximage.substring(23), 0)
- xuserImage = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
-
- // Round the image edges
- val imageRounded = Bitmap.createBitmap(xuserImage.getWidth(), xuserImage.getHeight(), xuserImage.getConfig())
- val canvas = Canvas(imageRounded)
- val mpaint = Paint()
- mpaint.setAntiAlias(true)
- mpaint.setShader(BitmapShader(xuserImage, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP))
- canvas.drawRoundRect(RectF(0F, 0F, xuserImage.getWidth().toFloat(), xuserImage.getHeight().toFloat()), 32F, 32F, mpaint) // Round Image Corner 100 100 100 100
- xuserImage.recycle()
- xuserImage = imageRounded
- } catch (ex: java.lang.Exception) { }
- }
-
- if ((xuserid != null) && (xrealname != null)) {
- var xuserinfo: MeshUserInfo = MeshUserInfo(xuserid, xrealname, xuserImage)
- userinfo[xuserid] = xuserinfo
- }
-
- // Notify of user information change
- parent.refreshInfo()
- }
- "getServerImage" -> {
- // Server title and image
- serverTitle = json.optString("title")
- serverSubTitle = json.optString("subtitle")
- var ximage: String? = json.optString("image")
- if ((ximage != null) && (!ximage.startsWith("data:image/jpeg;base64,"))) { ximage = null; }
-
- // Decode the image
- if (ximage != null) {
- try {
- val imageBytes = android.util.Base64.decode(ximage.substring(23), 0)
- serverImage =
- BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
- } catch (ex: java.lang.Exception) { }
- }
-
- // Notify of user information change
- if ((serverTitle != null) || (serverImage != null)) {
- parent.refreshInfo()
- }
- }
- else -> {
- // Unknown command, ignore it.
- println("Unhandled action: $action")
- }
- }
- }
- catch (e: Exception) {
- println("processAgentData Exception: ${e.toString()}")
- }
- }
-
- // Send the latest core information to the server
- fun sendCoreInfo() {
- val r = JSONObject()
- r.put("action", "coreinfo")
- r.put("value", "Android Agent v${BuildConfig.VERSION_NAME}")
- r.put("caps", 13) // Capability bitmask: 1 = Desktop, 2 = Terminal, 4 = Files, 8 = Console, 16 = JavaScript, 32 = Temporary Agent, 64 = Recovery Agent
- if (pushMessagingToken != null) { r.put("pmt", pushMessagingToken) }
- if (_webSocket != null) { _webSocket?.send(r.toString().toByteArray().toByteString()) }
- }
-
- // Send 2FA authentication URL and approval/reject back
- fun send2faAuth(url: Uri, approved: Boolean) {
- val r = JSONObject()
- r.put("action", "2faauth")
- r.put("url", url.toString())
- r.put("approved", approved)
- if (_webSocket != null) { _webSocket?.send(r.toString().toByteArray().toByteString()) }
- }
-
- // Request user image and real name if needed
- fun sendUserImageRequest(userid: String) {
- if (userinfo.containsKey(userid)) {
- parent.refreshInfo()
- return
- } else {
- userinfo[userid] = MeshUserInfo(userid, null, null)
- val r = JSONObject()
- r.put("action", "getUserImage")
- r.put("userid", userid)
- if (_webSocket != null) {
- _webSocket?.send(r.toString().toByteArray().toByteString())
- }
- }
- }
-
- // Request user image and real name if needed
- fun sendServerImageRequest() {
- val r = JSONObject()
- r.put("action", "getServerImage")
- r.put("agent", "android")
- if (_webSocket != null) {
- _webSocket?.send(r.toString().toByteArray().toByteString())
- }
- }
-
- fun removeTunnel(tunnel: MeshTunnel) {
- tunnels.remove(tunnel)
- parent.refreshInfo()
- }
-
- fun sendNetworkUpdate(force: Boolean) : Boolean {
- var netinfo = getNetInfo();
- if ((force == false) && (lastNetInfo != null)) {
- var netinfostr = netinfo.toString()
- if (lastNetInfo.equals(netinfostr)) return false
- lastNetInfo = netinfostr
- }
- if (force == true) { lastNetInfo = netinfo.toString() }
- lastNetInfo = netinfo.toString()
- val r = JSONObject()
- r.put("action", "netinfo")
- r.put("netif2", netinfo)
- if (_webSocket != null) {_webSocket?.send(r.toString().toByteArray().toByteString()); return true }
- return false
- }
-
- private fun getSysBuildInfo() : JSONObject {
- var r = JSONObject()
- r.put("board", android.os.Build.BOARD)
- r.put("bootloader", android.os.Build.BOOTLOADER)
- r.put("brand", android.os.Build.BRAND)
- r.put("device", android.os.Build.DEVICE)
- r.put("display", android.os.Build.DISPLAY)
- //r.put("fingerprint", android.os.Build.FINGERPRINT)
- r.put("androidapi", android.os.Build.VERSION.SDK_INT)
- r.put("androidrelease", android.os.Build.VERSION.RELEASE)
- r.put("host", android.os.Build.HOST)
- r.put("id", android.os.Build.ID)
- r.put("hardware", android.os.Build.HARDWARE)
- r.put("model", android.os.Build.MODEL)
- r.put("product", android.os.Build.PRODUCT)
- //r.put("supported_32_bit_abis", android.os.Build.SUPPORTED_32_BIT_ABIS)
- //r.put("supported_64_bit_abis", android.os.Build.SUPPORTED_64_BIT_ABIS)
- //r.put("supported_abis", android.os.Build.SUPPORTED_ABIS)
- r.put("tags", android.os.Build.TAGS)
- r.put("type", android.os.Build.TYPE)
- r.put("user", android.os.Build.USER)
- r.put("radioVersion", android.os.Build.getRadioVersion())
- return r;
- }
-
- private fun getStorageInfo() : JSONArray {
- var r = JSONArray()
- val internalStat = StatFs(Environment.getDataDirectory().path)
- val totalSpace = internalStat.blockCountLong * internalStat.blockSizeLong
- var onboard = JSONObject()
- onboard.put("Size", totalSpace)
- onboard.put("Caption", "Onboard Storage")
- onboard.put("Model", "Onboard Storage")
- r.put(onboard)
- return r
- }
-
- private fun getNetInfo() : JSONObject {
- var r = JSONObject()
- for (n in NetworkInterface.getNetworkInterfaces()) {
- var s = JSONArray()
- var count = 0
- for (j in n.interfaceAddresses) {
- var x = JSONObject()
- x.put("address", j.address.hostAddress)
- if (n.hardwareAddress != null) {
- var mac = n.hardwareAddress.toHex().toUpperCase()
- x.put("mac", mac.substring(0, 2) + ":" + mac.substring(2, 4) + ":" + mac.substring(4, 6) + ":" + mac.substring(6, 8) + ":" + mac.substring(8, 10) + ":" + mac.substring(10, 12))
- }
- if (n.isUp) {
- x.put("status", "up")
- } else {
- x.put("status", "down")
- }
- if (j.address.hostAddress.indexOf(':') >= 0) {
- x.put("family", "IPv6")
- } else {
- x.put("family", "IPv4")
- }
- x.put("index", n.index)
- s.put(x)
- count = count + 1
- }
- if (count > 0) { r.put(n.displayName, s) }
- }
- return r
- }
-
- fun batteryStateChanged(intent: Intent) {
- // Get the battery status, if it did not chance, don't send anything to the server
- var battState : JSONObject? = getSysBatteryInfo();
- if (battState != null) {
- if ((lastBattState != null) &&
- ((lastBattState?.getInt("level") == battState.getInt("level"))
- && (lastBattState?.getString("state")?.compareTo(battState.getString("state")) == 0))) return
-
- // Battery state changed, send update to the server
- lastBattState = battState
- if (_webSocket != null) { _webSocket?.send(battState.toString().toByteArray().toByteString()) }
- }
- }
-
- private fun getSysBatteryInfo() : JSONObject? {
- try {
- val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
- parent.applicationContext.registerReceiver(null, ifilter)
- }
- val status: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
- val isCharging: Boolean = status == BatteryManager.BATTERY_STATUS_CHARGING
- || status == BatteryManager.BATTERY_STATUS_FULL
-
- val batteryPct: Float? = batteryStatus?.let { intent ->
- val level: Int = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
- val scale: Int = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
- level * 100 / scale.toFloat()
- }
-
- var r = JSONObject()
- r.put("action", "battery")
- if (isCharging) {
- r.put("state", "ac")
- } else {
- r.put("state", "dc")
- }
- r.put("level", batteryPct)
- return r;
- }
- catch (e: Exception) { }
- return null
- }
-
- private fun parseArgString(s: String) : List {
- var r = ArrayList()
- var acc : String = ""
- var q = false
- for (i in 0..(s.length - 1)) {
- var c = s[i]
- if ((c == ' ') && (q == false)) {
- if (acc.length > 0) { r.add(acc); acc = ""; }
- } else {
- if (c == '"') { q = !q; } else { acc += c; }
- }
- }
- if (acc.length > 0) { r.add(acc); }
- return r.toList()
- }
-
- private fun processConsoleMessage(cmdLine: String, sessionid: String, jsoncmd: JSONObject) {
- //println("Console: $cmdLine")
-
- // Parse the incoming console command
- var splitCmd = parseArgString(cmdLine)
- var cmd = splitCmd[0]
- var r : String? = null
- if (cmd == "") return
-
- // Log the incoming console command to the server
- var eventArgs = JSONArray()
- eventArgs.put(cmdLine)
- logServerEventEx(17, eventArgs, "Processing console command: $cmdLine", jsoncmd);
-
- when (cmd) {
- "help" -> {
- // Return the list of available console commands
- r = "Available commands: alert, battery, dial, flash, netinfo, openurl, openbrowser,\r\n serverlog, sysinfo, storageinfo, toast, uiclose, uistate, vibrate"
- }
- "alert" -> {
- // Display alert message
- if (splitCmd.size < 2) {
- r = "Usage:\r\n alert \"Message\" \"Title\"";
- } else if (splitCmd.size == 2) {
- // Event to the server
- var eventArgs = JSONArray()
- eventArgs.put("Alert")
- eventArgs.put(splitCmd[1])
- logServerEventEx(18, eventArgs, "Displaying message box, title=" + splitCmd[2] + ", message=" + splitCmd[1], jsoncmd);
-
- // Show the alert
- parent.showAlertMessage("Alert", splitCmd[1])
- r = "Ok";
- } else if (splitCmd.size > 2) {
- // Event to the server
- var eventArgs = JSONArray()
- eventArgs.put(splitCmd[2])
- eventArgs.put(splitCmd[1])
- logServerEventEx(18, eventArgs, "Displaying message box, title=" + splitCmd[2] + ", message=" + splitCmd[1], jsoncmd);
-
- // Show the alert
- parent.showAlertMessage(splitCmd[2], splitCmd[1])
- r = "Ok";
- }
- }
- "toast" -> {
- // Display toast message
- if (splitCmd.size < 2) {
- r = "Usage:\r\n toast \"Message\"";
- } else if (splitCmd.size >= 2) {
- parent.showToastMessage(splitCmd[1])
-
- // Event to the server
- var eventArgs = JSONArray()
- eventArgs.put("None")
- eventArgs.put(splitCmd[1])
- logServerEventEx(26, eventArgs, "Displaying toast message, title=None, message=${splitCmd[1]}", jsoncmd);
- r = "Ok";
- }
- }
- "dial" -> {
- if (splitCmd.size < 2) {
- r = "Usage:\r\n dial [phonenumber]";
- } else if (splitCmd.size >= 2) {
- var getintent: Intent = Intent(Intent.ACTION_VIEW, Uri.parse("tel:${splitCmd[1]}"));
- parent.startActivity(getintent);
- r = "ok"
- }
- }
- "sysinfo" -> {
- // System info
- r = getSysBuildInfo().toString(2)
- }
- "storageinfo" -> {
- // Storage info
- r = getStorageInfo().toString(2)
- }
- "netinfo" -> {
- // Network info
- r = getNetInfo().toString(2)
- }
- "battery" -> {
- // Battery info
- var battState: JSONObject? = getSysBatteryInfo();
- if (battState == null) {
- r = "No battery"
- } else {
- r = battState.toString(2)
- }
- }
- "openbrowser" -> {
- // Open a URL
- if (splitCmd.size < 2) {
- r = "Usage:\r\n openbrowser \"url\"";
- } else if (splitCmd.size >= 2) {
- if (splitCmd[1].startsWith("https://") || splitCmd[1].startsWith("http://")) {
- if (visibleScreen == 2) {
- r = "Device is busy in QR code scanner"
- } else {
- // Open the URL
- try {
- var getintent: Intent = Intent(Intent.ACTION_VIEW, Uri.parse(splitCmd[1]));
- parent.startActivity(getintent);
- r = "Ok"
- } catch (ex: Exception) {
- r = "Error opening: ${splitCmd[1]}"
- }
- }
- } else {
- r = "Url must start with http:// or https://"
- }
- }
- }
- "openurl" -> {
- // Open a URL in the agent application
- if (splitCmd.size < 2) {
- r = "Usage:\r\n openurl \"url\"";
- } else if (splitCmd.size >= 2) {
- if (splitCmd[1].startsWith("https://") || splitCmd[1].startsWith("http://")) {
- if (visibleScreen == 2) {
- r = "Device is busy in QR code scanner"
- } else {
- // Open the URL
- if (parent.openUrl(splitCmd[1])) {
- r = "Ok";
-
- // Event to the server
- var eventArgs = JSONArray()
- eventArgs.put(splitCmd[1])
- logServerEventEx(20, eventArgs, "Opening: ${splitCmd[1]}", jsoncmd);
- } else {
- r = "Busy";
- }
- }
- } else {
- r = "Url must start with http:// or https://"
- }
- }
- }
- "uiclose" -> {
- if (visibleScreen == 1) {
- r = "Application is at main screen"
- } else {
- parent.returnToMainScreen()
- r = "ok"
- }
- }
- "uistate" -> {
- if (visibleScreen == 1) {
- r = "Application is at main screen"
- } else if (visibleScreen == 2) {
- r = "Application is at QR code scanner screen"
- } else if (visibleScreen == 3) {
- r = "Application is at browser screen: ${pageUrl}"
- } else {
- r = "Application is in unknown screen ${visibleScreen}"
- }
- }
- "serverlog" -> {
- // Log an event to the server
- if (splitCmd.size < 2) {
- r = "Usage:\r\n serverlog \"event\"";
- } else if (splitCmd.size >= 2) {
- logServerEvent(splitCmd[1], jsoncmd)
- r = "ok"
- }
- }
- "kvmstart" -> {
- // Start remote desktop
- if (g_ScreenCaptureService == null) {
- parent.startProjection()
- r = "ok"
- } else {
- r = "Already started"
- }
- }
- "kvmstop" -> {
- // Stop remote desktop
- if (g_ScreenCaptureService != null) {
- parent.stopProjection()
- r = "ok"
- } else {
- r = "Already stopped"
- }
- }
- "vibrate" -> {
- // Vibrate the device
- if (splitCmd.size < 2) {
- r = "Usage:\r\n vibrate [milliseconds]";
- } else if (splitCmd.size >= 2) {
- var t: Long = 0
- try {
- t = splitCmd[1].toLong()
- } catch (e: Exception) {
- }
- if ((t > 0) && (t <= 10000)) {
- val v = parent.getApplicationContext()
- .getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- if (v == null) {
- r = "Not supported"
- } else {
- // Vibrate for 500 milliseconds
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- v.vibrate(
- VibrationEffect.createOneShot(
- t,
- VibrationEffect.DEFAULT_AMPLITUDE
- )
- )
- } else {
- v.vibrate(t)
- }
- r = "ok"
- }
- } else {
- r = "Value must be between 1 and 10000"
- }
- }
- }
- "flash" -> {
- if (splitCmd.size < 2) {
- r = "Usage:\r\n flash [milliseconds]";
- } else if (splitCmd.size >= 2) {
- var isFlashAvailable = parent.getApplicationContext().getPackageManager()
- .hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
- if (!isFlashAvailable) {
- r = "Flash not available"
- } else {
- var t: Long = 0
- try {
- t = splitCmd[1].toLong()
- } catch (e: Exception) {
- }
- if ((t > 0) && (t <= 10000)) {
- var mCameraManager = parent.getApplicationContext().getSystemService(Context.CAMERA_SERVICE) as CameraManager
- try {
- var mCameraId = mCameraManager.getCameraIdList()[0];
- mCameraManager.setTorchMode(mCameraId, true);
- thread {
- Thread.sleep(t)
- mCameraManager.setTorchMode(mCameraId, false);
- }
- r = "ok"
- } catch (e: CameraAccessException) {
- r = "Flash error"
- }
- } else {
- r = "Value must be between 1 and 10000"
- }
- }
- }
- }
- else -> {
- // Unknown console command
- r = "Unknown command \"$cmd\", type \"help\" for command list."
- }
- }
-
- if (r != null) sendConsoleResponse(r, sessionid)
- }
-
- fun sendConsoleResponse(r: String, sessionid: String?) {
- val json = JSONObject()
- json.put("action", "msg")
- json.put("type", "console")
- json.put("value", r)
- if (sessionid != null) { json.put("sessionid", sessionid) }
- if (_webSocket != null) { _webSocket?.send(json.toString().toByteArray().toByteString()) }
- }
-
- fun hexToByteArray(hex: String) : ByteArray {
- val HEX_CHARS = "0123456789abcdef"
- val len = hex.length
- val result = ByteArray(len / 2)
- (0 until len step 2).forEach { i ->
- result[i.shr(1)] = HEX_CHARS.indexOf(hex[i]).shl(4).or(HEX_CHARS.indexOf(hex[i + 1])).toByte()
- }
- return result
- }
-
- fun logServerEvent(msg: String, jsoncmd: JSONObject?) {
- val json = JSONObject()
- json.put("action", "log")
- json.put("msg", msg)
- if (jsoncmd != null) {
- if (!jsoncmd.isNull("userid")) { json.put("userid", jsoncmd.optString("userid")) }
- if (!jsoncmd.isNull("username")) { json.put("username", jsoncmd.optString("username")) }
- if (!jsoncmd.isNull("sessionid")) { json.put("sessionid", jsoncmd.optString("sessionid")) }
- if (!jsoncmd.isNull("remoteaddr")) { json.put("remoteaddr", jsoncmd.optString("remoteaddr")) }
- if (!jsoncmd.isNull("soptions")) {
- var soptions : JSONObject = jsoncmd.getJSONObject("soptions")
- if (!soptions.isNull("userid")) { json.put("userid", soptions.optString("userid")) }
- if (!soptions.isNull("sessionid")) { json.put("sessionid", soptions.optString("sessionid")) }
- }
- }
- if (_webSocket != null) { _webSocket?.send(json.toString().toByteArray().toByteString()) }
- }
-
- fun logServerEventEx(id: Int, args: JSONArray?, msg: String, jsoncmd: JSONObject?) {
- val json = JSONObject()
- json.put("action", "log")
- json.put("msgid", id)
- if (args != null) { json.put("msgArgs", args) }
- json.put("msg", msg)
- if (jsoncmd != null) {
- if (!jsoncmd.isNull("userid")) { json.put("userid", jsoncmd.optString("userid")) }
- if (!jsoncmd.isNull("username")) { json.put("username", jsoncmd.optString("username")) }
- if (!jsoncmd.isNull("sessionid")) { json.put("sessionid", jsoncmd.optString("sessionid")) }
- if (!jsoncmd.isNull("remoteaddr")) { json.put("remoteaddr", jsoncmd.optString("remoteaddr")) }
- if (!jsoncmd.isNull("soptions")) {
- var soptions : JSONObject = jsoncmd.getJSONObject("soptions")
- if (!soptions.isNull("userid")) { json.put("userid", soptions.optString("userid")) }
- if (!soptions.isNull("sessionid")) { json.put("sessionid", soptions.optString("sessionid")) }
- }
- }
- if (_webSocket != null) { _webSocket?.send(json.toString().toByteArray().toByteString()) }
- }
-
-}
+package com.meshcentral.agent
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager
+import android.graphics.*
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CameraManager
+import android.location.LocationManager
+import android.net.Uri
+import android.os.*
+import android.provider.Settings
+import android.util.Base64
+import okhttp3.*
+import okio.ByteString
+import okio.ByteString.Companion.toByteString
+import org.json.JSONArray
+import org.json.JSONObject
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.DataOutputStream
+import java.net.InetAddress
+import java.net.NetworkInterface
+import java.security.MessageDigest
+import java.security.Signature
+import java.security.cert.CertificateFactory
+import java.security.cert.X509Certificate
+import java.security.interfaces.RSAPublicKey
+import java.util.Collections
+import java.util.concurrent.TimeUnit
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLContext
+import javax.net.ssl.TrustManager
+import javax.net.ssl.X509TrustManager
+import kotlin.concurrent.thread
+import kotlin.random.Random
+
+
+class MeshUserInfo(userid: String, realname: String?, image: Bitmap?) {
+ val userid: String = userid
+ val realname: String? = realname
+ val image: Bitmap? = image
+ init {
+ //println("MeshUserInfo: $userid, $realname")
+ }
+}
+
+class MeshAgent(parent: MainActivity, host: String, certHash: String, devGroupId: String) : WebSocketListener() {
+ val parent : MainActivity = parent
+ val host : String = host
+ val serverCertHash: String = certHash
+ val devGroupId: String = devGroupId
+ var state : Int = 0 // 0 = Disconnected, 1 = Connecting, 2 = Authenticating, 3 = Connected
+ var nonce : ByteArray? = null
+ var serverNonce: ByteArray? = null
+ var serverTlsCertHash: ByteArray? = null
+ var serverTitle : String? = null
+ var serverSubTitle : String? = null
+ var serverImage : Bitmap? = null
+ private var _webSocket: WebSocket? = null
+ private var connectionState: Int = 0
+ private var connectionTimer: CountDownTimer? = null
+ private var lastBattState : JSONObject? = null
+ private var lastNetInfo : String? = null
+ var tunnels : ArrayList = ArrayList()
+ var userinfo : HashMap = HashMap() // UserID -> MeshUserInfo
+
+ init {
+ //println("MeshAgent Constructor: ${host}, ${certHash}, $devGroupId")
+ }
+
+ fun Start() {
+ //println("MeshAgent Start")
+ UpdateState(1) // Switch to connecting
+ startSocket()
+ }
+
+ fun Stop() {
+ //println("MeshAgent Stop")
+ stopSocket()
+ UpdateState(0) // Switch to disconnected
+ }
+
+ fun UpdateState(newState: Int) {
+ if (newState != state) {
+ state = newState
+ parent.agentStateChanged()
+ }
+ }
+
+ fun ByteArray.toHex(): String {
+ return joinToString("") { "%02x".format(it) }
+ }
+
+ private fun getUnsafeOkHttpClient(): OkHttpClient {
+ // Create a trust manager that does not validate certificate chains
+ val trustAllCerts = arrayOf(object : X509TrustManager {
+ override fun checkClientTrusted(chain: Array?, authType: String?) {}
+
+ override fun checkServerTrusted(chain: Array?, authType: String?) {
+ serverTlsCertHash = MessageDigest.getInstance("SHA-384").digest(chain?.get(0)?.encoded)
+ }
+
+ override fun getAcceptedIssuers() = arrayOf()
+ })
+
+ // Install the special trust manager that records the certificate hash of the server
+ val sslContext = SSLContext.getInstance("SSL")
+ sslContext.init(null, trustAllCerts, java.security.SecureRandom())
+
+ val sslSocketFactory = sslContext.socketFactory
+
+ return OkHttpClient.Builder()
+ .connectTimeout(20, TimeUnit.SECONDS)
+ .readTimeout(60, TimeUnit.MINUTES)
+ .writeTimeout(60, TimeUnit.MINUTES)
+ .hostnameVerifier(hostnameVerifier = HostnameVerifier { _, _ -> true })
+ .sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
+ .build()
+ }
+
+
+ fun startSocket() {
+ _webSocket = getUnsafeOkHttpClient().newWebSocket(
+ Request.Builder().url("wss://$host/agent.ashx").build(),
+ this
+ )
+ //socketOkHttpClient.dispatcher.executorService.shutdown()
+ }
+
+ fun stopSocket() {
+ // Disconnect and clear the control web socket
+ if (_webSocket != null) {
+ try {
+ _webSocket?.close(NORMAL_CLOSURE_STATUS, null)
+ _webSocket = null
+ } catch (ex: Exception) { }
+ }
+ // Clear the connection timer
+ if (connectionTimer != null) {
+ connectionTimer?.cancel()
+ connectionTimer = null
+ }
+ // Clear all relay tunnels, create a mutable list since the list may change when calling Stop()
+ var tunnelsClone : MutableList = tunnels.toMutableList()
+ for (t in tunnelsClone) { t.Stop() }
+ tunnels.clear()
+ // Update the state to disconnected
+ UpdateState(0) // Switch to disconnected
+ }
+
+ companion object {
+ const val NORMAL_CLOSURE_STATUS = 1000
+ }
+
+ override fun onOpen(webSocket: WebSocket, response: Response) {
+ //println("onOpen")
+ UpdateState(2) // Switch to connected
+ nonce = Random.Default.nextBytes(48)
+
+ // Start authenticate the mesh agent by sending a auth nonce & server TLS cert hash.
+ // Send 384 bits SHA384 hash of TLS cert public key + 384 bits nonce
+ var header = ByteArray(2)
+ header[1] = 1
+ webSocket.send(header.plus(serverTlsCertHash!!).plus(nonce!!).toByteString()); // Command 1, hash + nonce
+ }
+
+ override fun onMessage(webSocket: WebSocket, text: String) {
+ //println("onMessage: $text")
+ }
+
+ override fun onMessage(webSocket: WebSocket, msg: ByteString) {
+ try {
+ //println("onBinaryMessage: ${msg.size}, ${msg.toByteArray().toHex()}")
+ if (msg.size < 2) return;
+ if ((connectionState == 3) && (msg[0].toInt() == 123)) {
+ // If we are authenticated, process JSON data
+ processAgentData(String(msg.toByteArray(), Charsets.UTF_8))
+ return
+ }
+
+ var cmd : Int = (msg[0].toInt() shl 8) + msg[1].toInt()
+ //println("Cmd $cmd, Size: ${msg.size}")
+ when (cmd) {
+ 1 -> {
+ // Server authentication request
+ if (msg.size != 98) return;
+ var serverCertHash = msg.substring(2, 50).toByteArray()
+ if (!serverCertHash.contentEquals(serverTlsCertHash!!)) {
+ println("Server Hash Mismatch, given=${serverCertHash.toHex()}, computed=${serverTlsCertHash?.toHex()}")
+ stopSocket()
+ return
+ }
+ serverNonce = msg.substring(50).toByteArray()
+
+ // Hash the server cert hash, server nonce and client nonce and sign the result
+ val sig = Signature.getInstance("SHA384withRSA")
+ sig.initSign(agentCertificateKey)
+ sig.update(msg.substring(2).toByteArray().plus(nonce!!))
+ val signature = sig.sign()
+
+ // Construct the response [2, sideOfCert, Cert, Signature]
+ var header = ByteArray(2)
+ header[1] = 2
+ var certLen = agentCertificate!!.encoded.size
+ var agentCertLenBytes = ByteArray(2)
+ agentCertLenBytes[0] = (certLen shr 8).toByte()
+ agentCertLenBytes[1] = (certLen and 0xFF).toByte()
+
+ // Send the response
+ webSocket.send(header.plus(agentCertLenBytes).plus(agentCertificate!!.encoded).plus(signature).toByteString())
+ }
+ 2 -> {
+ // Server agent certificate
+ var xcertLen: Int = (msg[2].toUByte().toInt() shl 8) + msg[3].toUByte().toInt()
+ var xcertBytes = msg.substring(4, 4 + xcertLen)
+ var xagentCertificate = CertificateFactory.getInstance("X509").generateCertificate(
+ ByteArrayInputStream(xcertBytes.toByteArray())
+ ) as X509Certificate
+
+ // The private key DER encoding contains the private key type, we don't want
+ // that when hashing the private so we remove the first 24 bytes.
+ var pkey = xagentCertificate.publicKey as RSAPublicKey
+ var serverid = MessageDigest.getInstance("SHA-384").digest(pkey.encoded.toByteString().substring(24).toByteArray())
+ var serveridb64 = Base64.encodeToString(serverid, Base64.NO_WRAP)
+ serveridb64 = serveridb64.replace('/', '$').replace('+', '@')
+ // If invalid server certificate, disconnect
+ if (serveridb64.compareTo(serverCertHash) != 0) {
+ println("Invalid Server Certificate Hash"); stopSocket(); return
+ }
+
+ // Verify server signature
+ var signBlock: ByteArray? = serverTlsCertHash!!.plus(nonce!!).plus(serverNonce!!)
+ val sig = Signature.getInstance("SHA384withRSA")
+ sig.initVerify(xagentCertificate)
+ sig.update(signBlock)
+ if (!sig.verify(msg.substring(4 + xcertLen).toByteArray())) {
+ println("Invalid Server Signature"); stopSocket(); return
+ }
+
+ // Everything is ok, server is valid.
+ connectionState = connectionState or 1
+
+ //println("Host: ${android.os.Build.HOST}")
+ var agentid = 14; // This of agent (14, Android in this case)
+ var agentver = 0 // Agent version (TODO)
+ var platfromType = 3; // This is the icon: 1 = Desktop, 2 = Laptop, 3 = Mobile, 4 = Server, 5 = Disk, 6 = Router
+ var capabilities = 12; // Capabilities of the agent (bitmask): 1 = Desktop, 2 = Terminal, 4 = Files, 8 = Console, 16 = JavaScript
+ var deviceName: String? = null;
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
+ deviceName = Settings.Secure.getString(parent.contentResolver, "bluetooth_name");
+ }
+ if (deviceName == null) {
+ deviceName = Settings.Global.getString(parent.contentResolver, Settings.Global.DEVICE_NAME) ?: "UNKNOWN_DEVICE_NAME";
+ }
+ val deviceNameUtf = deviceName.toByteArray(Charsets.UTF_8)
+ //println("DeviceName: ${deviceName}")
+
+ var devGroupIdBytes: ByteArray = Base64.decode(devGroupId.replace('$', '/').replace('@', '+'), Base64.DEFAULT)
+
+ // Command 3: infover, agentid, agentversion, platformtype, meshid, capabilities, computername
+ var bytesOut = ByteArrayOutputStream()
+ DataOutputStream(bytesOut).use { dos ->
+ with(dos) {
+ writeShort(3)
+ writeInt(1)
+ writeInt(agentid)
+ writeInt(agentver)
+ writeInt(platfromType)
+ write(devGroupIdBytes)
+ writeInt(capabilities)
+ writeShort(deviceNameUtf.size)
+ write(deviceNameUtf)
+ }
+ }
+ webSocket.send(bytesOut.toByteArray().toByteString())
+ if (connectionState == 3) connectHandler()
+ }
+ 4 -> {
+ // Server confirmed authentication, we are allowed to send commands to the server
+ connectionState = connectionState or 2
+ if (connectionState == 3) connectHandler()
+ }
+ else -> {
+ // Unknown command, ignore it.
+
+ }
+ }
+ }
+ catch (e: Exception) {
+ println("Exception: ${e.toString()}")
+ }
+ }
+
+ override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
+ //println("onClosing")
+ stopSocket()
+ }
+
+ override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
+ println("onFailure ${t.toString()}, ${response.toString()}")
+ stopSocket()
+ }
+
+ private fun connectHandler() {
+ //println("Connected and verified")
+ UpdateState(3) // Switch to connected and verified
+ startConnectionTimer()
+ sendCoreInfo()
+ sendNetworkUpdate(false)
+ sendServerImageRequest()
+
+ if (g_autoConsent) {
+ parent.startProjection()
+ }
+
+ // Send battery state
+ if (_webSocket != null) { _webSocket?.send(getSysBatteryInfo().toString().toByteArray().toByteString()) }
+ }
+
+ // Cause some data to be sent over the websocket control channel every 2 minutes to keep it open
+ private fun startConnectionTimer() {
+ parent.runOnUiThread {
+ connectionTimer = object: CountDownTimer(120000000, 120000) {
+ override fun onTick(millisUntilFinished: Long) {
+ if (sendNetworkUpdate(false) == false) { // See if we need to update network information
+ if (_webSocket != null) {
+ _webSocket?.send(ByteArray(1).toByteString()) // If not, sent a single zero byte
+ }
+ }
+ }
+ override fun onFinish() { startConnectionTimer() }
+ }
+ connectionTimer?.start()
+ }
+ }
+
+ private fun processAgentData(jsonStr: String) {
+ //println("JSON: $jsonStr")
+ try {
+ val json = JSONObject(jsonStr)
+ var action = json.getString("action")
+ //println("action: $action")
+ when (action) {
+ "ping" -> {
+ // Return a pong
+ val r = JSONObject()
+ r.put("action", "pong")
+ if (_webSocket != null) {
+ _webSocket?.send(r.toString().toByteArray().toByteString())
+ }
+ }
+ "pong" -> {
+ // Nop
+ }
+ "sysinfo" -> {
+ //println("SysInfo")
+ val s = JSONObject()
+ s.put("mobile", getSysBuildInfo())
+ var identifiers = JSONObject()
+ identifiers.put("storage_devices", getStorageInfo())
+ s.put("identifiers", identifiers)
+ val t = JSONObject()
+ t.put("hardware", s)
+ var hash1 = MessageDigest.getInstance("SHA-384").digest(t.toString().toByteArray()).toHex()
+ var hash2: String? = null
+ if (json.isNull("hash") == false) {
+ hash2 = json.getString("hash")
+ }
+ if ((hash2 == null) || (hash1.contentEquals(hash2) == false)) {
+ t.put("hash", hash1)
+ val r = JSONObject()
+ r.put("action", "sysinfo")
+ r.put("data", t)
+ //println(r.toString())
+ if (_webSocket != null) {
+ _webSocket?.send(r.toString().toByteArray().toByteString())
+ }
+ }
+ }
+ "netinfo" -> {
+ sendNetworkUpdate(true)
+ }
+ "openUrl" -> {
+ /*
+ if (visibleScreen != 2) { // Device is busy in QR code scanner
+ // Open the URL
+ var xurl = json.optString("url")
+ //println("Opening: $xurl")
+ if ((xurl != null) && (parent.openUrl(xurl))) {
+ // Event to the server
+ var eventArgs = JSONArray()
+ eventArgs.put(xurl)
+ logServerEventEx(20, eventArgs, "Opening: ${xurl}", json);
+ }
+ }
+ */
+
+ var xurl = json.optString("url")
+ if (xurl != null) {
+ var getintent: Intent = Intent(Intent.ACTION_VIEW, Uri.parse(xurl));
+ parent.startActivity(getintent);
+ }
+ }
+ "msg" -> {
+ var msgtype = json.getString("type")
+ when (msgtype) {
+ "console" -> {
+ processConsoleMessage(json.getString("value"), json.getString("sessionid"), json)
+ }
+ "tunnel" -> {
+ /*
+ {"action":"msg",
+ "type":"tunnel",
+ "value":"*\/meshrelay.ashx?...",
+ "usage":5,
+ "servertlshash":"97eaf674eab131d3775f12cfa9c978d185a0e9caaf3a854bf0eb4ff94c2d6c53ca3dc456da149002804666fbbdae2fc9",
+ "soptions":{
+ "consentTitle":"MeshCentral",
+ "consentMsgDesktop":"{0} ({1}) requesting remote desktop access. Grant access?",
+ "consentMsgTerminal":"{0} ({1}) requesting remote terminal access. Grant access?",
+ "consentMsgFiles":"{0} ({1}) requesting remote files access. Grant access?",
+ "notifyTitle":"MeshCentral",
+ "notifyMsgDesktop":"{0} ({1}) started a remote desktop session.",
+ "notifyMsgTerminal":"{0} ({1}) started a remote terminal session.",
+ "notifyMsgFiles":"{0} ({1}) started a remote files session."},
+ "userid":"user//admin","perMessageDeflate":true,
+ "sessionid":"user//admin/1fc5a4e5ab144a721b852118de898e5f1af5b791",
+ "rights":4294967295,
+ "consent":0,
+ "username":"admin",
+ "remoteaddr":"192.168.2.182",
+ "privacybartext":"This is a test: {0} ({1})"}
+ */
+
+ var url = json.getString("value")
+ if (url.startsWith("*/")) {
+ var hostdns: String = host
+ var i = host.indexOf('/') // If the hostname includes an extra domain, remove it.
+ if (i > 0) {
+ hostdns = host.substring(0, i); }
+ url = "wss://$hostdns" + url.substring(1)
+ }
+ val tunnel = MeshTunnel(this, url, json)
+ tunnels.add(tunnel)
+ tunnel.Start()
+ }
+ else -> {
+ // Unknown message type, ignore it.
+ println("Unhandled msg type: $msgtype")
+ }
+ }
+ }
+ "coredump" -> {
+ // Nop
+ }
+ "getcoredump" -> {
+ // Nop
+ }
+ "getUserImage" -> {
+ // User real name and optional image
+ var xuserid: String? = json.optString("userid")
+ var xrealname: String? = json.optString("realname")
+ var ximage: String? = json.optString("image")
+ var xuserImage: Bitmap? = null
+
+ if ((ximage != null) && (!ximage.startsWith("data:image/jpeg;base64,"))) {
+ ximage = null; }
+
+ if (ximage != null) {
+ try {
+ val imageBytes = android.util.Base64.decode(ximage.substring(23), 0)
+ xuserImage = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
+
+ // Round the image edges
+ val imageRounded = Bitmap.createBitmap(xuserImage.getWidth(), xuserImage.getHeight(), xuserImage.getConfig())
+ val canvas = Canvas(imageRounded)
+ val mpaint = Paint()
+ mpaint.setAntiAlias(true)
+ mpaint.setShader(BitmapShader(xuserImage, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP))
+ canvas.drawRoundRect(RectF(0F, 0F, xuserImage.getWidth().toFloat(), xuserImage.getHeight().toFloat()), 32F, 32F, mpaint) // Round Image Corner 100 100 100 100
+ xuserImage.recycle()
+ xuserImage = imageRounded
+ } catch (ex: java.lang.Exception) { }
+ }
+
+ if ((xuserid != null) && (xrealname != null)) {
+ var xuserinfo: MeshUserInfo = MeshUserInfo(xuserid, xrealname, xuserImage)
+ userinfo[xuserid] = xuserinfo
+ }
+
+ // Notify of user information change
+ parent.refreshInfo()
+ }
+ "getServerImage" -> {
+ // Server title and image
+ serverTitle = json.optString("title")
+ serverSubTitle = json.optString("subtitle")
+ var ximage: String? = json.optString("image")
+ if ((ximage != null) && (!ximage.startsWith("data:image/jpeg;base64,"))) { ximage = null; }
+
+ // Decode the image
+ if (ximage != null) {
+ try {
+ val imageBytes = android.util.Base64.decode(ximage.substring(23), 0)
+ serverImage =
+ BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
+ } catch (ex: java.lang.Exception) { }
+ }
+
+ // Notify of user information change
+ if ((serverTitle != null) || (serverImage != null)) {
+ parent.refreshInfo()
+ }
+ }
+ else -> {
+ // Unknown command, ignore it.
+ println("Unhandled action: $action")
+ }
+ }
+ }
+ catch (e: Exception) {
+ println("processAgentData Exception: ${e.toString()}")
+ }
+ }
+
+ // Send the latest core information to the server
+ fun sendCoreInfo() {
+ val r = JSONObject()
+ r.put("action", "coreinfo")
+ r.put("value", "Android Agent v${BuildConfig.VERSION_NAME}")
+ r.put("caps", 13) // Capability bitmask: 1 = Desktop, 2 = Terminal, 4 = Files, 8 = Console, 16 = JavaScript, 32 = Temporary Agent, 64 = Recovery Agent
+ if (pushMessagingToken != null) { r.put("pmt", pushMessagingToken) }
+ if (_webSocket != null) { _webSocket?.send(r.toString().toByteArray().toByteString()) }
+ }
+
+ // Send 2FA authentication URL and approval/reject back
+ fun send2faAuth(url: Uri, approved: Boolean) {
+ val r = JSONObject()
+ r.put("action", "2faauth")
+ r.put("url", url.toString())
+ r.put("approved", approved)
+ if (_webSocket != null) { _webSocket?.send(r.toString().toByteArray().toByteString()) }
+ }
+
+ // Request user image and real name if needed
+ fun sendUserImageRequest(userid: String) {
+ if (userinfo.containsKey(userid)) {
+ parent.refreshInfo()
+ return
+ } else {
+ userinfo[userid] = MeshUserInfo(userid, null, null)
+ val r = JSONObject()
+ r.put("action", "getUserImage")
+ r.put("userid", userid)
+ if (_webSocket != null) {
+ _webSocket?.send(r.toString().toByteArray().toByteString())
+ }
+ }
+ }
+
+ // Request user image and real name if needed
+ fun sendServerImageRequest() {
+ val r = JSONObject()
+ r.put("action", "getServerImage")
+ r.put("agent", "android")
+ if (_webSocket != null) {
+ _webSocket?.send(r.toString().toByteArray().toByteString())
+ }
+ }
+
+ fun removeTunnel(tunnel: MeshTunnel) {
+ tunnels.remove(tunnel)
+ parent.refreshInfo()
+ }
+
+ fun sendNetworkUpdate(force: Boolean) : Boolean {
+ var netinfo = getNetInfo();
+ if ((force == false) && (lastNetInfo != null)) {
+ var netinfostr = netinfo.toString()
+ if (lastNetInfo.equals(netinfostr)) return false
+ lastNetInfo = netinfostr
+ }
+ if (force == true) { lastNetInfo = netinfo.toString() }
+ lastNetInfo = netinfo.toString()
+ val r = JSONObject()
+ r.put("action", "netinfo")
+ r.put("netif2", netinfo)
+ if (_webSocket != null) {_webSocket?.send(r.toString().toByteArray().toByteString()); return true }
+ return false
+ }
+
+ private fun getSysBuildInfo() : JSONObject {
+ var r = JSONObject()
+ r.put("board", android.os.Build.BOARD)
+ r.put("bootloader", android.os.Build.BOOTLOADER)
+ r.put("brand", android.os.Build.BRAND)
+ r.put("device", android.os.Build.DEVICE)
+ r.put("display", android.os.Build.DISPLAY)
+ //r.put("fingerprint", android.os.Build.FINGERPRINT)
+ r.put("androidapi", android.os.Build.VERSION.SDK_INT)
+ r.put("androidrelease", android.os.Build.VERSION.RELEASE)
+ r.put("host", android.os.Build.HOST)
+ r.put("id", android.os.Build.ID)
+ r.put("hardware", android.os.Build.HARDWARE)
+ r.put("model", android.os.Build.MODEL)
+ r.put("product", android.os.Build.PRODUCT)
+ //r.put("supported_32_bit_abis", android.os.Build.SUPPORTED_32_BIT_ABIS)
+ //r.put("supported_64_bit_abis", android.os.Build.SUPPORTED_64_BIT_ABIS)
+ //r.put("supported_abis", android.os.Build.SUPPORTED_ABIS)
+ r.put("tags", android.os.Build.TAGS)
+ r.put("type", android.os.Build.TYPE)
+ r.put("user", android.os.Build.USER)
+ r.put("radioVersion", android.os.Build.getRadioVersion())
+ return r;
+ }
+
+ private fun getStorageInfo() : JSONArray {
+ var r = JSONArray()
+ val internalStat = StatFs(Environment.getDataDirectory().path)
+ val totalSpace = internalStat.blockCountLong * internalStat.blockSizeLong
+ var onboard = JSONObject()
+ onboard.put("Size", totalSpace)
+ onboard.put("Caption", "Onboard Storage")
+ onboard.put("Model", "Onboard Storage")
+ r.put(onboard)
+ return r
+ }
+
+ private fun getNetInfo() : JSONObject {
+ var r = JSONObject()
+ for (n in NetworkInterface.getNetworkInterfaces()) {
+ var s = JSONArray()
+ var count = 0
+ for (j in n.interfaceAddresses) {
+ var x = JSONObject()
+ x.put("address", j.address.hostAddress)
+ if (n.hardwareAddress != null) {
+ var mac = n.hardwareAddress.toHex().toUpperCase()
+ x.put("mac", mac.substring(0, 2) + ":" + mac.substring(2, 4) + ":" + mac.substring(4, 6) + ":" + mac.substring(6, 8) + ":" + mac.substring(8, 10) + ":" + mac.substring(10, 12))
+ }
+ if (n.isUp) {
+ x.put("status", "up")
+ } else {
+ x.put("status", "down")
+ }
+ if (j.address.hostAddress.indexOf(':') >= 0) {
+ x.put("family", "IPv6")
+ } else {
+ x.put("family", "IPv4")
+ }
+ x.put("index", n.index)
+ s.put(x)
+ count = count + 1
+ }
+ if (count > 0) { r.put(n.displayName, s) }
+ }
+ return r
+ }
+
+ fun batteryStateChanged(intent: Intent) {
+ // Get the battery status, if it did not chance, don't send anything to the server
+ var battState : JSONObject? = getSysBatteryInfo();
+ if (battState != null) {
+ if ((lastBattState != null) &&
+ ((lastBattState?.getInt("level") == battState.getInt("level"))
+ && (lastBattState?.getString("state")?.compareTo(battState.getString("state")) == 0))) return
+
+ // Battery state changed, send update to the server
+ lastBattState = battState
+ if (_webSocket != null) { _webSocket?.send(battState.toString().toByteArray().toByteString()) }
+ }
+ }
+
+ private fun getSysBatteryInfo() : JSONObject? {
+ try {
+ val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
+ parent.applicationContext.registerReceiver(null, ifilter)
+ }
+ val status: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
+ val isCharging: Boolean = status == BatteryManager.BATTERY_STATUS_CHARGING
+ || status == BatteryManager.BATTERY_STATUS_FULL
+
+ val batteryPct: Float? = batteryStatus?.let { intent ->
+ val level: Int = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
+ val scale: Int = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
+ level * 100 / scale.toFloat()
+ }
+
+ var r = JSONObject()
+ r.put("action", "battery")
+ if (isCharging) {
+ r.put("state", "ac")
+ } else {
+ r.put("state", "dc")
+ }
+ r.put("level", batteryPct)
+ return r;
+ }
+ catch (e: Exception) { }
+ return null
+ }
+
+ private fun parseArgString(s: String) : List {
+ var r = ArrayList()
+ var acc : String = ""
+ var q = false
+ for (i in 0..(s.length - 1)) {
+ var c = s[i]
+ if ((c == ' ') && (q == false)) {
+ if (acc.length > 0) { r.add(acc); acc = ""; }
+ } else {
+ if (c == '"') { q = !q; } else { acc += c; }
+ }
+ }
+ if (acc.length > 0) { r.add(acc); }
+ return r.toList()
+ }
+
+ private fun processConsoleMessage(cmdLine: String, sessionid: String, jsoncmd: JSONObject) {
+ //println("Console: $cmdLine")
+
+ // Parse the incoming console command
+ var splitCmd = parseArgString(cmdLine)
+ var cmd = splitCmd[0]
+ var r : String? = null
+ if (cmd == "") return
+
+ // Log the incoming console command to the server
+ var eventArgs = JSONArray()
+ eventArgs.put(cmdLine)
+ logServerEventEx(17, eventArgs, "Processing console command: $cmdLine", jsoncmd);
+
+ when (cmd) {
+ "help" -> {
+ // Return the list of available console commands
+ r = "Available commands: alert, battery, dial, flash, lock, netinfo, openurl, openbrowser,\r\n serverlog, sysinfo, storageinfo, toast, uiclose, uistate, vibrate"
+ }
+ "alert" -> {
+ // Display alert message
+ if (splitCmd.size < 2) {
+ r = "Usage:\r\n alert \"Message\" \"Title\"";
+ } else if (splitCmd.size == 2) {
+ // Event to the server
+ var eventArgs = JSONArray()
+ eventArgs.put("Alert")
+ eventArgs.put(splitCmd[1])
+ logServerEventEx(18, eventArgs, "Displaying message box, title=" + splitCmd[2] + ", message=" + splitCmd[1], jsoncmd);
+
+ // Show the alert
+ parent.showAlertMessage("Alert", splitCmd[1])
+ r = "Ok";
+ } else if (splitCmd.size > 2) {
+ // Event to the server
+ var eventArgs = JSONArray()
+ eventArgs.put(splitCmd[2])
+ eventArgs.put(splitCmd[1])
+ logServerEventEx(18, eventArgs, "Displaying message box, title=" + splitCmd[2] + ", message=" + splitCmd[1], jsoncmd);
+
+ // Show the alert
+ parent.showAlertMessage(splitCmd[2], splitCmd[1])
+ r = "Ok";
+ }
+ }
+ "toast" -> {
+ // Display toast message
+ if (splitCmd.size < 2) {
+ r = "Usage:\r\n toast \"Message\"";
+ } else if (splitCmd.size >= 2) {
+ parent.showToastMessage(splitCmd[1])
+
+ // Event to the server
+ var eventArgs = JSONArray()
+ eventArgs.put("None")
+ eventArgs.put(splitCmd[1])
+ logServerEventEx(26, eventArgs, "Displaying toast message, title=None, message=${splitCmd[1]}", jsoncmd);
+ r = "Ok";
+ }
+ }
+ "dial" -> {
+ if (splitCmd.size < 2) {
+ r = "Usage:\r\n dial [phonenumber]";
+ } else if (splitCmd.size >= 2) {
+ var getintent: Intent = Intent(Intent.ACTION_VIEW, Uri.parse("tel:${splitCmd[1]}"));
+ parent.startActivity(getintent);
+ r = "ok"
+ }
+ }
+ "sysinfo" -> {
+ // System info
+ r = getSysBuildInfo().toString(2)
+ }
+ "storageinfo" -> {
+ // Storage info
+ r = getStorageInfo().toString(2)
+ }
+ "netinfo" -> {
+ // Network info
+ r = getNetInfo().toString(2)
+ }
+ "battery" -> {
+ // Battery info
+ var battState: JSONObject? = getSysBatteryInfo();
+ if (battState == null) {
+ r = "No battery"
+ } else {
+ r = battState.toString(2)
+ }
+ }
+ "openbrowser" -> {
+ // Open a URL
+ if (splitCmd.size < 2) {
+ r = "Usage:\r\n openbrowser \"url\"";
+ } else if (splitCmd.size >= 2) {
+ if (splitCmd[1].startsWith("https://") || splitCmd[1].startsWith("http://")) {
+ if (visibleScreen == 2) {
+ r = "Device is busy in QR code scanner"
+ } else {
+ // Open the URL
+ try {
+ var getintent: Intent = Intent(Intent.ACTION_VIEW, Uri.parse(splitCmd[1]));
+ parent.startActivity(getintent);
+ r = "Ok"
+ } catch (ex: Exception) {
+ r = "Error opening: ${splitCmd[1]}"
+ }
+ }
+ } else {
+ r = "Url must start with http:// or https://"
+ }
+ }
+ }
+ "openurl" -> {
+ // Open a URL in the agent application
+ if (splitCmd.size < 2) {
+ r = "Usage:\r\n openurl \"url\"";
+ } else if (splitCmd.size >= 2) {
+ if (splitCmd[1].startsWith("https://") || splitCmd[1].startsWith("http://")) {
+ if (visibleScreen == 2) {
+ r = "Device is busy in QR code scanner"
+ } else {
+ // Open the URL
+ if (parent.openUrl(splitCmd[1])) {
+ r = "Ok";
+
+ // Event to the server
+ var eventArgs = JSONArray()
+ eventArgs.put(splitCmd[1])
+ logServerEventEx(20, eventArgs, "Opening: ${splitCmd[1]}", jsoncmd);
+ } else {
+ r = "Busy";
+ }
+ }
+ } else {
+ r = "Url must start with http:// or https://"
+ }
+ }
+ }
+ "uiclose" -> {
+ if (visibleScreen == 1) {
+ r = "Application is at main screen"
+ } else {
+ parent.returnToMainScreen()
+ r = "ok"
+ }
+ }
+ "uistate" -> {
+ if (visibleScreen == 1) {
+ r = "Application is at main screen"
+ } else if (visibleScreen == 2) {
+ r = "Application is at QR code scanner screen"
+ } else if (visibleScreen == 3) {
+ r = "Application is at browser screen: ${pageUrl}"
+ } else {
+ r = "Application is in unknown screen ${visibleScreen}"
+ }
+ }
+ "serverlog" -> {
+ // Log an event to the server
+ if (splitCmd.size < 2) {
+ r = "Usage:\r\n serverlog \"event\"";
+ } else if (splitCmd.size >= 2) {
+ logServerEvent(splitCmd[1], jsoncmd)
+ r = "ok"
+ }
+ }
+ "kvmstart" -> {
+ // Start remote desktop
+ if (g_ScreenCaptureService == null) {
+ parent.startProjection()
+ r = "ok"
+ } else {
+ r = "Already started"
+ }
+ }
+ "kvmstop" -> {
+ // Stop remote desktop
+ if (g_ScreenCaptureService != null) {
+ parent.stopProjection()
+ r = "ok"
+ } else {
+ r = "Already stopped"
+ }
+ }
+ "vibrate" -> {
+ // Vibrate the device
+ if (splitCmd.size < 2) {
+ r = "Usage:\r\n vibrate [milliseconds]";
+ } else if (splitCmd.size >= 2) {
+ var t: Long = 0
+ try {
+ t = splitCmd[1].toLong()
+ } catch (e: Exception) {
+ }
+ if ((t > 0) && (t <= 10000)) {
+ val v = parent.getApplicationContext()
+ .getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+ if (v == null) {
+ r = "Not supported"
+ } else {
+ // Vibrate for 500 milliseconds
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ v.vibrate(
+ VibrationEffect.createOneShot(
+ t,
+ VibrationEffect.DEFAULT_AMPLITUDE
+ )
+ )
+ } else {
+ v.vibrate(t)
+ }
+ r = "ok"
+ }
+ } else {
+ r = "Value must be between 1 and 10000"
+ }
+ }
+ }
+ "flash" -> {
+ if (splitCmd.size < 2) {
+ r = "Usage:\r\n flash [milliseconds]";
+ } else if (splitCmd.size >= 2) {
+ var isFlashAvailable = parent.getApplicationContext().getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
+ if (!isFlashAvailable) {
+ r = "Flash not available"
+ } else {
+ var t: Long = 0
+ try {
+ t = splitCmd[1].toLong()
+ } catch (e: Exception) {
+ }
+ if ((t > 0) && (t <= 10000)) {
+ var mCameraManager = parent.getApplicationContext().getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ try {
+ var mCameraId = mCameraManager.getCameraIdList()[0];
+ mCameraManager.setTorchMode(mCameraId, true);
+ thread {
+ Thread.sleep(t)
+ mCameraManager.setTorchMode(mCameraId, false);
+ }
+ r = "ok"
+ } catch (e: CameraAccessException) {
+ r = "Flash error"
+ }
+ } else {
+ r = "Value must be between 1 and 10000"
+ }
+ }
+ }
+ }
+ "lock" -> {
+ parent.dpm.lockNow()
+ r = "Device locked correctly"
+ }
+ "geolocation" -> {
+ val json = JSONObject()
+ try {
+ var locationManager =
+ parent.getSystemService(Context.LOCATION_SERVICE) as LocationManager
+ json.put("action", "geolocation")
+ var location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
+ if(location==null){
+ location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
+ }
+ json.put("altitude",location?.altitude)
+ json.put("longitude", location?.longitude)
+ json.put("latitude",location?.latitude)
+ }catch (securityException: SecurityException){
+ println("Can't access location ${securityException.message}")
+ }
+ r = json.toString()
+ }
+ }
+
+ if (r != null) sendConsoleResponse(r, sessionid)
+ }
+
+ fun sendConsoleResponse(r: String, sessionid: String?) {
+ val json = JSONObject()
+ json.put("action", "msg")
+ json.put("type", "console")
+ json.put("value", r)
+ if (sessionid != null) { json.put("sessionid", sessionid) }
+ if (_webSocket != null) { _webSocket?.send(json.toString().toByteArray().toByteString()) }
+ }
+
+ fun hexToByteArray(hex: String) : ByteArray {
+ val HEX_CHARS = "0123456789abcdef"
+ val len = hex.length
+ val result = ByteArray(len / 2)
+ (0 until len step 2).forEach { i ->
+ result[i.shr(1)] = HEX_CHARS.indexOf(hex[i]).shl(4).or(HEX_CHARS.indexOf(hex[i + 1])).toByte()
+ }
+ return result
+ }
+
+ fun logServerEvent(msg: String, jsoncmd: JSONObject?) {
+ val json = JSONObject()
+ json.put("action", "log")
+ json.put("msg", msg)
+ if (jsoncmd != null) {
+ if (!jsoncmd.isNull("userid")) { json.put("userid", jsoncmd.optString("userid")) }
+ if (!jsoncmd.isNull("username")) { json.put("username", jsoncmd.optString("username")) }
+ if (!jsoncmd.isNull("sessionid")) { json.put("sessionid", jsoncmd.optString("sessionid")) }
+ if (!jsoncmd.isNull("remoteaddr")) { json.put("remoteaddr", jsoncmd.optString("remoteaddr")) }
+ if (!jsoncmd.isNull("soptions")) {
+ var soptions : JSONObject = jsoncmd.getJSONObject("soptions")
+ if (!soptions.isNull("userid")) { json.put("userid", soptions.optString("userid")) }
+ if (!soptions.isNull("sessionid")) { json.put("sessionid", soptions.optString("sessionid")) }
+ }
+ }
+ if (_webSocket != null) { _webSocket?.send(json.toString().toByteArray().toByteString()) }
+ }
+
+ fun logServerEventEx(id: Int, args: JSONArray?, msg: String, jsoncmd: JSONObject?) {
+ val json = JSONObject()
+ json.put("action", "log")
+ json.put("msgid", id)
+ if (args != null) { json.put("msgArgs", args) }
+ json.put("msg", msg)
+ if (jsoncmd != null) {
+ if (!jsoncmd.isNull("userid")) { json.put("userid", jsoncmd.optString("userid")) }
+ if (!jsoncmd.isNull("username")) { json.put("username", jsoncmd.optString("username")) }
+ if (!jsoncmd.isNull("sessionid")) { json.put("sessionid", jsoncmd.optString("sessionid")) }
+ if (!jsoncmd.isNull("remoteaddr")) { json.put("remoteaddr", jsoncmd.optString("remoteaddr")) }
+ if (!jsoncmd.isNull("soptions")) {
+ var soptions : JSONObject = jsoncmd.getJSONObject("soptions")
+ if (!soptions.isNull("userid")) { json.put("userid", soptions.optString("userid")) }
+ if (!soptions.isNull("sessionid")) { json.put("sessionid", soptions.optString("sessionid")) }
+ }
+ }
+ if (_webSocket != null) { _webSocket?.send(json.toString().toByteArray().toByteString()) }
+ }
+
+}
diff --git a/app/src/main/java/com/meshcentral/agent/MeshTunnel.kt b/app/src/main/java/com/meshcentral/agent/MeshTunnel.kt
index 793494e..4aa41cd 100644
--- a/app/src/main/java/com/meshcentral/agent/MeshTunnel.kt
+++ b/app/src/main/java/com/meshcentral/agent/MeshTunnel.kt
@@ -1,16 +1,20 @@
package com.meshcentral.agent
import android.app.RecoverableSecurityException
+import android.app.admin.DevicePolicyManager
import android.content.ContentResolver
import android.content.ContentUris
import android.content.ContentValues
+import android.content.Context
import android.database.Cursor
+import android.location.LocationManager
import android.net.Uri
import android.os.Build
import android.os.CountDownTimer
import android.os.Environment
import android.provider.MediaStore
import android.util.Base64
+import androidx.core.content.ContextCompat.getSystemService
import okhttp3.*
import okio.ByteString
import okio.ByteString.Companion.toByteString
@@ -18,6 +22,8 @@ import org.json.JSONArray
import org.json.JSONObject
//import org.webrtc.PeerConnectionFactory
import java.io.*
+import java.net.InetAddress
+import java.net.NetworkInterface
import java.nio.charset.Charset
import java.security.MessageDigest
import java.security.cert.CertificateException
@@ -401,6 +407,27 @@ class MeshTunnel(parent: MeshAgent, url: String, serverData: JSONObject) : WebSo
var action = json.getString("action")
//println("action: $action")
when (action) {
+ "geolocation" -> {
+ val json = JSONObject()
+ try {
+ var locationManager = parent.parent.getSystemService(Context.LOCATION_SERVICE) as LocationManager
+ json.put("action", "geolocation")
+ var location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
+ if(location==null){
+ location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
+ }
+ json.put("altitude",location?.altitude)
+ json.put("longitude", location?.longitude)
+ json.put("latitude",location?.latitude)
+ }catch (securityException: SecurityException){
+ println("Can't access location ${securityException.message}")
+ json.put("error", securityException.message)
+ }
+ //if(_webSocket != null) { _webSocket?.send(json.toString().toByteArray().toByteString()) }
+ if (_webSocket != null){
+ _webSocket?.send(json.toString().toByteArray(Charsets.UTF_8).toByteString())
+ }
+ }
"ls" -> {
val path = json.getString("path")
if (path == "") {
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index dfcf188..b9a576e 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -58,4 +58,10 @@
Immer Zustimmung für Verbindung anfordern
Verbinden mit: %1$s?
Server-Setup löschen?
+ Server Pairing Link
+ Server Pairing Link ungültig
+ Autostart
+ Manuell starten
+ Mit System starten
+ Starte MeshCentral BootService...
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e4a6ccd..7c468d9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -61,6 +61,10 @@
Always ask for consent when remote agent connects
Setup to: %1$s?
Clear server setup?
+ Autostart
+ Start manually
+ Start with system
+ Start MeshCentral BootService ...
Server Pairing Link
- Invalid Server Pairing Linbk
+ Invalid Server Pairing Link
\ No newline at end of file
diff --git a/app/src/main/res/xml/device_admin_receiver.xml b/app/src/main/res/xml/device_admin_receiver.xml
new file mode 100644
index 0000000..6ecab93
--- /dev/null
+++ b/app/src/main/res/xml/device_admin_receiver.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index 50f85ef..29a4e10 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -3,6 +3,13 @@
+