Export/import of configuration filed on Android

This commit is contained in:
Dmitriy Karpushin 2022-12-08 11:51:28 +03:00
parent a9217810e7
commit cad0dabe42
15 changed files with 568 additions and 147 deletions

View file

@ -0,0 +1,5 @@
package org.amnezia.vpn
const val IMPORT_COMMAND_CODE = 1
const val IMPORT_ACTION_CODE = "import_action"
const val IMPORT_CONFIG_KEY = "CONFIG_DATA_KEY"

View file

@ -15,6 +15,8 @@ import android.os.*
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import android.text.TextUtils
import androidx.core.content.FileProvider
import com.wireguard.android.util.SharedLibraryLoader
import com.wireguard.config.*
import com.wireguard.crypto.Key
@ -151,6 +153,31 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface {
private var flags = 0
private var startId = 0
private lateinit var mMessenger: Messenger
internal class ExternalConfigImportHandler(
context: Context,
private val serviceBinder: VPNServiceBinder,
private val applicationContext: Context = context.applicationContext
) : Handler() {
override fun handleMessage(msg: Message) {
when (msg.what) {
IMPORT_COMMAND_CODE -> {
val data = msg.data.getString(IMPORT_CONFIG_KEY)
if (data != null) {
serviceBinder.importConfig(data)
}
}
else -> {
super.handleMessage(msg)
}
}
}
}
fun init() {
if (mAlreadyInitialised) {
return
@ -188,6 +215,14 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface {
*/
override fun onBind(intent: Intent): IBinder {
Log.v(tag, "Aman: onBind....................")
if (intent.action != null && intent.action == IMPORT_ACTION_CODE) {
Log.v(tag, "Service bind for import of config")
mMessenger = Messenger(ExternalConfigImportHandler(this, mBinder))
return mMessenger.binder
}
Log.v(tag, "Regular service bind")
when (mProtocol) {
"shadowsocks" -> {
when (intent.action) {
@ -840,4 +875,44 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface {
override fun close() = Os.close(fd)
}
fun saveAsFile(configContent: String?, suggestedFileName: String): String {
val rootDirPath = cacheDir.absolutePath
val rootDir = File(rootDirPath)
if (!rootDir.exists()) {
rootDir.mkdirs()
}
val fileName = if (!TextUtils.isEmpty(suggestedFileName)) suggestedFileName else "amnezia.cfg"
val file = File(rootDir, fileName)
try {
file.bufferedWriter().use { out -> out.write(configContent) }
return file.toString()
} catch (e: Exception) {
e.printStackTrace()
}
return ""
}
fun shareFile(attachmentFile: String?) {
try {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/*"
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val file = File(attachmentFile)
val uri = FileProvider.getUriForFile(this, "${BuildConfig.APPLICATION_ID}.fileprovider", file)
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val createChooser = Intent.createChooser(intent, "Config sharing")
createChooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(createChooser)
} catch (e: Exception) {
Log.i(tag, e.message.toString())
}
}
}

View file

@ -17,6 +17,7 @@ class VPNServiceBinder(service: VPNService) : Binder() {
private val tag = "VPNServiceBinder"
private var mListener: IBinder? = null
private var mResumeConfig: JSONObject? = null
private var mImportedConfig: String? = null
/**
* The codes this Binder does accept in [onTransact]
@ -31,6 +32,7 @@ class VPNServiceBinder(service: VPNService) : Binder() {
const val resumeActivate = 7
const val setNotificationText = 8
const val setFallBackNotification = 9
const val shareConfig = 10
}
/**
@ -70,101 +72,148 @@ class VPNServiceBinder(service: VPNService) : Binder() {
return true
}
ACTIONS.resumeActivate -> {
// [data] is empty
// Activate the current tunnel
try {
mResumeConfig?.let { this.mService.turnOn(it) }
} catch (e: Exception) {
Log.e(tag, "An Error occurred while enabling the VPN: ${e.localizedMessage}")
}
return true
}
ACTIONS.deactivate -> {
// [data] here is empty
this.mService.turnOff()
return true
}
ACTIONS.registerEventListener -> {
// [data] contains the Binder that we need to dispatch the Events
val binder = data.readStrongBinder()
mListener = binder
val obj = JSONObject()
obj.put("connected", mService.isUp)
obj.put("time", mService.connectionTime)
dispatchEvent(EVENTS.init, obj.toString())
return true
}
ACTIONS.requestStatistic -> {
dispatchEvent(EVENTS.statisticUpdate, mService.status.toString())
return true
}
ACTIONS.requestGetLog -> {
// Grabs all the Logs and dispatch new Log Event
dispatchEvent(EVENTS.backendLogs, Log.getContent())
return true
}
ACTIONS.requestCleanupLog -> {
Log.clearFile()
return true
}
ACTIONS.setNotificationText -> {
NotificationUtil.update(data)
return true
}
ACTIONS.setFallBackNotification -> {
NotificationUtil.saveFallBackMessage(data, mService)
return true
}
IBinder.LAST_CALL_TRANSACTION -> {
Log.e(tag, "The OS Requested to shut down the VPN")
this.mService.turnOff()
return true
}
else -> {
Log.e(tag, "Received invalid bind request \t Code -> $code")
// If we're hitting this there is probably something wrong in the client.
return false
}
}
return false
}
/**
* Dispatches an Event to all registered Binders
* [code] the Event that happened - see [EVENTS]
* To register an Eventhandler use [onTransact] with
* [ACTIONS.registerEventListener]
*/
fun dispatchEvent(code: Int, payload: String?) {
ACTIONS.resumeActivate -> {
// [data] is empty
// Activate the current tunnel
try {
mListener?.let {
if (it.isBinderAlive) {
val data = Parcel.obtain()
data.writeByteArray(payload?.toByteArray(charset("UTF-8")))
it.transact(code, data, Parcel.obtain(), 0)
}
}
} catch (e: DeadObjectException) {
// If the QT Process is killed (not just inactive)
// we cant access isBinderAlive, so nothing to do here.
mResumeConfig?.let { this.mService.turnOn(it) }
} catch (e: Exception) {
Log.e(tag, "An Error occurred while enabling the VPN: ${e.localizedMessage}")
}
return true
}
/**
* The codes we Are Using in case of [dispatchEvent]
*/
object EVENTS {
const val init = 0
const val connected = 1
const val disconnected = 2
const val statisticUpdate = 3
const val backendLogs = 4
const val activationError = 5
ACTIONS.deactivate -> {
// [data] here is empty
this.mService.turnOff()
return true
}
ACTIONS.registerEventListener -> {
// [data] contains the Binder that we need to dispatch the Events
val binder = data.readStrongBinder()
mListener = binder
val obj = JSONObject()
obj.put("connected", mService.isUp)
obj.put("time", mService.connectionTime)
dispatchEvent(EVENTS.init, obj.toString())
////
if (mImportedConfig != null) {
Log.i(tag, "register: config not null")
dispatchEvent(EVENTS.configImport, mImportedConfig)
mImportedConfig = null
} else {
Log.i(tag, "register: config is null")
}
return true
}
ACTIONS.requestStatistic -> {
dispatchEvent(EVENTS.statisticUpdate, mService.status.toString())
return true
}
ACTIONS.requestGetLog -> {
// Grabs all the Logs and dispatch new Log Event
dispatchEvent(EVENTS.backendLogs, Log.getContent())
return true
}
ACTIONS.requestCleanupLog -> {
Log.clearFile()
return true
}
ACTIONS.setNotificationText -> {
NotificationUtil.update(data)
return true
}
ACTIONS.setFallBackNotification -> {
NotificationUtil.saveFallBackMessage(data, mService)
return true
}
ACTIONS.shareConfig -> {
val byteArray = data.createByteArray()
val json = byteArray?.let { String(it) }
val config = JSONObject(json)
val configContent = config.getString("data")
val suggestedName = config.getString("suggestedName")
val filePath = mService.saveAsFile(configContent, suggestedName)
Log.i(tag, "save file: $filePath")
mService.shareFile(filePath)
return true
}
IBinder.LAST_CALL_TRANSACTION -> {
Log.e(tag, "The OS Requested to shut down the VPN")
this.mService.turnOff()
return true
}
else -> {
Log.e(tag, "Received invalid bind request \t Code -> $code")
// If we're hitting this there is probably something wrong in the client.
return false
}
}
return false
}
/**
* Dispatches an Event to all registered Binders
* [code] the Event that happened - see [EVENTS]
* To register an Eventhandler use [onTransact] with
* [ACTIONS.registerEventListener]
*/
fun dispatchEvent(code: Int, payload: String?) {
try {
mListener?.let {
if (it.isBinderAlive) {
val data = Parcel.obtain()
data.writeByteArray(payload?.toByteArray(charset("UTF-8")))
it.transact(code, data, Parcel.obtain(), 0)
}
}
} catch (e: DeadObjectException) {
// If the QT Process is killed (not just inactive)
// we cant access isBinderAlive, so nothing to do here.
}
}
/**
* The codes we Are Using in case of [dispatchEvent]
*/
object EVENTS {
const val init = 0
const val connected = 1
const val disconnected = 2
const val statisticUpdate = 3
const val backendLogs = 4
const val activationError = 5
const val configImport = 6
}
fun importConfig(config: String) {
val obj = JSONObject()
obj.put("config", config)
val resultString = obj.toString()
Log.i(tag, "Transact import config request")
if (mListener != null) {
Log.i(tag, "binder alive")
dispatchEvent(EVENTS.configImport, resultString)
} else {
Log.i(tag, "binder NOT alive")
mImportedConfig = resultString
}
}
}

View file

@ -1,37 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.amnezia.vpn.qt;
import android.view.KeyEvent;
public class VPNActivity extends org.qtproject.qt5.android.bindings.QtActivity {
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
onBackPressed();
return true;
}
return super.onKeyDown(keyCode, event);
}
// TODO finalize
// https://github.com/mozilla-mobile/mozilla-vpn-client/blob/6acff5dd9f072380a04c3fa12e9f3c98dbdd7a26/src/platforms/android/androidvpnactivity.h
@Override
public void onBackPressed() {
// super.onBackPressed();
try {
if (!handleBackButton()) {
// Move the activity into paused state if back button was pressed
moveTaskToBack(true);
// finish();
}
} catch (Exception e) {
}
}
// Returns true if MVPN has handled the back button
native boolean handleBackButton();
}

View file

@ -0,0 +1,196 @@
package org.amnezia.vpn.qt;
import android.Manifest
import android.content.ComponentName
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.net.Uri
import android.os.*
import android.provider.MediaStore
import android.util.Log
import android.view.KeyEvent
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import org.amnezia.vpn.VPNService
import org.amnezia.vpn.VPNServiceBinder
import org.amnezia.vpn.IMPORT_COMMAND_CODE
import org.amnezia.vpn.IMPORT_ACTION_CODE
import org.amnezia.vpn.IMPORT_CONFIG_KEY
import org.qtproject.qt5.android.bindings.QtActivity
import java.io.*
class VPNActivity : org.qtproject.qt5.android.bindings.QtActivity() {
private var configString: String? = null
private var vpnServiceBinder: Messenger? = null
private var isBound = false
private val TAG = "VPNActivity"
private val STORAGE_PERMISSION_CODE = 42
override fun onCreate(savedInstanceState: Bundle?) {
val newIntent = intent
val newIntentAction = newIntent.action
if (newIntent != null && newIntentAction != null) {
configString = processIntent(newIntent, newIntentAction)
}
super.onCreate(savedInstanceState)
}
override fun onNewIntent(newIntent: Intent) {
intent = newIntent
val newIntentAction = newIntent.action
if (newIntent != null && newIntentAction != null && newIntentAction != Intent.ACTION_MAIN) {
if (isReadStorageAllowed()) {
configString = processIntent(newIntent, newIntentAction)
} else {
requestStoragePermission()
}
}
super.onNewIntent(intent)
}
override fun onResume() {
super.onResume()
if (configString != null && !isBound) {
bindVpnService()
}
}
override fun onPause() {
if (vpnServiceBinder != null && isBound) {
unbindService(connection)
isBound = false
}
super.onPause()
}
private fun isReadStorageAllowed(): Boolean {
val permissionStatus = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
return permissionStatus == PackageManager.PERMISSION_GRANTED
}
private fun requestStoragePermission() {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), STORAGE_PERMISSION_CODE)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
if (requestCode == STORAGE_PERMISSION_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Storage read permission granted")
if (configString != null) {
bindVpnService()
}
} else {
Toast.makeText(this, "Oops you just denied the permission", Toast.LENGTH_LONG).show()
}
}
}
private fun bindVpnService() {
try {
val intent = Intent(this, VPNService::class.java)
intent.action = IMPORT_ACTION_CODE
bindService(intent, connection, Context.BIND_AUTO_CREATE)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun processIntent(intent: Intent, action: String): String? {
val scheme = intent.scheme
if (scheme == null) {
return null
}
if (action.compareTo(Intent.ACTION_VIEW) == 0) {
val resolver = contentResolver
if (scheme.compareTo(ContentResolver.SCHEME_CONTENT) == 0) {
val uri = intent.data
val name: String? = getContentName(resolver, uri)
Log.d(TAG, "Content intent detected: " + action + " : " + intent.dataString + " : " + intent.type + " : " + name)
val input = resolver.openInputStream(uri!!)
return input?.bufferedReader()?.use(BufferedReader::readText)
} else if (scheme.compareTo(ContentResolver.SCHEME_FILE) == 0) {
val uri = intent.data
val name = uri!!.lastPathSegment
Log.d(TAG, "File intent detected: " + action + " : " + intent.dataString + " : " + intent.type + " : " + name)
val input = resolver.openInputStream(uri)
return input?.bufferedReader()?.use(BufferedReader::readText)
}
}
return null
}
private fun getContentName(resolver: ContentResolver?, uri: Uri?): String? {
val cursor = resolver!!.query(uri!!, null, null, null, null)
cursor.use {
cursor!!.moveToFirst()
val nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
return if (nameIndex >= 0) {
return cursor.getString(nameIndex)
} else {
null
}
}
}
private var connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, binder: IBinder) {
vpnServiceBinder = Messenger(binder)
if (configString != null) {
val msg: Message = Message.obtain(null, IMPORT_COMMAND_CODE, 0, 0)
val bundle = Bundle()
bundle.putString(IMPORT_CONFIG_KEY, configString!!)
msg.data = bundle
try {
vpnServiceBinder?.send(msg)
} catch (e: RemoteException) {
e.printStackTrace()
}
configString = null
}
isBound = true
}
override fun onServiceDisconnected(className: ComponentName) {
vpnServiceBinder = null
isBound = false
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK && event.repeatCount == 0) {
onBackPressed()
return true
}
return super.onKeyDown(keyCode, event)
}
}