From c359d78bcd45cb506bac51a616ef62af0845df85 Mon Sep 17 00:00:00 2001 From: pacien Date: Fri, 16 Feb 2018 18:23:01 +0100 Subject: Refactor activities and service, locking app at daemon startup and shutdown --- app/src/main/AndroidManifest.xml | 15 +- .../pacien/tincapp/activities/LaunchActivity.kt | 98 ------------- .../org/pacien/tincapp/activities/StartActivity.kt | 152 ++++++++++++++++----- .../pacien/tincapp/activities/StatusActivity.kt | 99 +++++++++----- .../main/java/org/pacien/tincapp/commands/Tinc.kt | 4 + .../main/java/org/pacien/tincapp/intent/Actions.kt | 19 +++ .../tincapp/intent/SimpleBroadcastReceiver.kt | 19 +++ .../org/pacien/tincapp/intent/action/Actions.kt | 14 -- .../org/pacien/tincapp/service/TincVpnService.kt | 113 ++++++++------- .../java/org/pacien/tincapp/utils/TincKeyring.kt | 26 ++++ app/src/main/res/values/strings.xml | 4 + 11 files changed, 323 insertions(+), 240 deletions(-) delete mode 100644 app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt create mode 100644 app/src/main/java/org/pacien/tincapp/intent/Actions.kt create mode 100644 app/src/main/java/org/pacien/tincapp/intent/SimpleBroadcastReceiver.kt delete mode 100644 app/src/main/java/org/pacien/tincapp/intent/action/Actions.kt create mode 100644 app/src/main/java/org/pacien/tincapp/utils/TincKeyring.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f03a640..6826920 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,6 +24,11 @@ + + + + + - - - - - - - - diff --git a/app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt b/app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt deleted file mode 100644 index 0179040..0000000 --- a/app/src/main/java/org/pacien/tincapp/activities/LaunchActivity.kt +++ /dev/null @@ -1,98 +0,0 @@ -package org.pacien.tincapp.activities - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Intent -import android.net.Uri -import android.net.VpnService -import android.os.Bundle -import android.support.v7.app.AlertDialog -import android.support.v7.app.AppCompatActivity -import kotlinx.android.synthetic.main.dialog_decrypt_keys.view.* -import org.pacien.tincapp.R -import org.pacien.tincapp.commands.TincApp -import org.pacien.tincapp.context.App -import org.pacien.tincapp.intent.action.ACTION_CONNECT -import org.pacien.tincapp.intent.action.ACTION_DISCONNECT -import org.pacien.tincapp.intent.action.TINC_SCHEME -import org.pacien.tincapp.service.TincVpnService -import org.pacien.tincapp.utils.PemUtils -import java.io.FileNotFoundException - -/** - * @author pacien - */ -class LaunchActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - when (intent.action) { - ACTION_CONNECT -> requestPerm() - ACTION_DISCONNECT -> disconnect() - } - } - - override fun onActivityResult(request: Int, result: Int, data: Intent?) { - if (request == PERMISSION_REQUEST_CODE && result == Activity.RESULT_OK) askPassphrase() - } - - private fun requestPerm() = VpnService.prepare(this).let { - if (it != null) - startActivityForResult(it, PERMISSION_REQUEST_CODE) - else - onActivityResult(PERMISSION_REQUEST_CODE, Activity.RESULT_OK, null) - } - - @SuppressLint("InflateParams") - private fun askPassphrase() { - val netName = intent.data.schemeSpecificPart - - if (needPassphrase(netName) && intent.data.fragment == null) { - val dialog = layoutInflater.inflate(R.layout.dialog_decrypt_keys, null, false) - AlertDialog.Builder(this) - .setTitle(R.string.title_unlock_private_keys).setView(dialog) - .setPositiveButton(R.string.action_unlock) { _, _ -> connect(netName, dialog.passphrase.text.toString()) } - .setNegativeButton(R.string.action_cancel, { _, _ -> finish() }) - .show() - } else { - connect(netName, intent.data.fragment) - } - } - - private fun needPassphrase(netName: String) = try { - TincApp.listPrivateKeys(netName).filter { it.exists() }.any { PemUtils.isEncrypted(PemUtils.read(it)) } - } catch (e: FileNotFoundException) { - false - } - - private fun connect(netName: String, passphrase: String? = null) { - TincVpnService.startVpn(netName, passphrase) - finish() - } - - private fun disconnect() { - TincVpnService.stopVpn() - finish() - } - - companion object { - - private val PERMISSION_REQUEST_CODE = 0 - - fun connect(netName: String, passphrase: String? = null) { - App.getContext().startActivity(Intent(App.getContext(), LaunchActivity::class.java) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .setAction(ACTION_CONNECT) - .setData(Uri.Builder().scheme(TINC_SCHEME).opaquePart(netName).fragment(passphrase).build())) - } - - fun disconnect() { - App.getContext().startActivity(Intent(App.getContext(), LaunchActivity::class.java) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .setAction(ACTION_DISCONNECT)) - } - - } - -} diff --git a/app/src/main/java/org/pacien/tincapp/activities/StartActivity.kt b/app/src/main/java/org/pacien/tincapp/activities/StartActivity.kt index 719bbc1..9fa5e44 100644 --- a/app/src/main/java/org/pacien/tincapp/activities/StartActivity.kt +++ b/app/src/main/java/org/pacien/tincapp/activities/StartActivity.kt @@ -1,8 +1,13 @@ package org.pacien.tincapp.activities +import android.app.Activity +import android.app.ProgressDialog import android.content.Intent +import android.content.IntentFilter +import android.net.VpnService import android.os.Bundle import android.support.v4.widget.SwipeRefreshLayout +import android.support.v7.app.AlertDialog import android.view.Menu import android.view.MenuItem import android.view.View @@ -10,29 +15,117 @@ import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.TextView import kotlinx.android.synthetic.main.base.* +import kotlinx.android.synthetic.main.dialog_decrypt_keys.view.* import kotlinx.android.synthetic.main.fragment_list_view.* import kotlinx.android.synthetic.main.fragment_network_list_header.* import org.pacien.tincapp.R import org.pacien.tincapp.context.AppPaths import org.pacien.tincapp.extensions.Android.setElements +import org.pacien.tincapp.intent.Actions +import org.pacien.tincapp.intent.SimpleBroadcastReceiver import org.pacien.tincapp.service.TincVpnService +import org.pacien.tincapp.utils.TincKeyring /** * @author pacien */ -class StartActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRefreshLayout.OnRefreshListener { +class StartActivity : BaseActivity() { + companion object { + private const val PERMISSION_REQUEST = 0 + } + + private val networkList = object : AdapterView.OnItemClickListener, SwipeRefreshLayout.OnRefreshListener { + private var networkListAdapter: ArrayAdapter? = null + + fun init() { + networkListAdapter = ArrayAdapter(this@StartActivity, R.layout.fragment_list_item) + layoutInflater.inflate(R.layout.fragment_list_view, main_content) + list_wrapper.setOnRefreshListener(this) + list.addHeaderView(layoutInflater.inflate(R.layout.fragment_network_list_header, list, false), null, false) + list.addFooterView(View(this@StartActivity), null, false) + list.adapter = networkListAdapter + list.onItemClickListener = this + } + + fun destroy() { + networkListAdapter = null + } + + override fun onRefresh() { + val networks = AppPaths.confDir()?.list()?.toList() ?: emptyList() + runOnUiThread { + networkListAdapter?.setElements(networks) + setPlaceholderVisibility() + list_wrapper.isRefreshing = false + } + } + + override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + connectionStarter.tryStart(netName = (view as TextView).text.toString(), displayStatus = true) + } + + private fun setPlaceholderVisibility() = if (networkListAdapter?.isEmpty != false) { + network_list_placeholder.text = getListPlaceholderText() + network_list_placeholder.visibility = View.VISIBLE + } else { + network_list_placeholder.visibility = View.GONE + } + + private fun getListPlaceholderText() = if (!AppPaths.storageAvailable()) { + getText(R.string.message_storage_unavailable) + } else { + getText(R.string.message_no_network_configuration_found) + } + } + + private val connectionStarter = object { + private var netName: String? = null + private var passphrase: String? = null + private var displayStatus = false + + fun displayStatus() = displayStatus - private var networkListAdapter: ArrayAdapter? = null + fun tryStart(netName: String? = null, passphrase: String? = null, displayStatus: Boolean? = null) { + if (netName != null) this.netName = netName + if (passphrase != null) this.passphrase = passphrase + if (displayStatus != null) this.displayStatus = displayStatus + + val permissionRequestIntent = VpnService.prepare(this@StartActivity) + if (permissionRequestIntent != null) + return startActivityForResult(permissionRequestIntent, PERMISSION_REQUEST) + + if (TincKeyring.needsPassphrase(this.netName!!) && this.passphrase == null) + return askForPassphrase() + + startVpn(this.netName!!, this.passphrase) + } + + private fun askForPassphrase() { + layoutInflater.inflate(R.layout.dialog_decrypt_keys, main_content, false).let { dialog -> + AlertDialog.Builder(this@StartActivity) + .setTitle(R.string.title_unlock_private_keys).setView(dialog) + .setPositiveButton(R.string.action_unlock) { _, _ -> tryStart(passphrase = dialog.passphrase.text.toString()) } + .setNegativeButton(R.string.action_cancel, { _, _ -> Unit }) + .show() + } + } + + private fun startVpn(netName: String, passphrase: String? = null) { + connectDialog = showProgressDialog(R.string.message_starting_vpn) + TincVpnService.connect(netName, passphrase) + } + } + + private val startupBroadcastReceiver = SimpleBroadcastReceiver(IntentFilter(Actions.EVENT_CONNECTED), this::onVpnStart) + + private var connectDialog: ProgressDialog? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - networkListAdapter = ArrayAdapter(this, R.layout.fragment_list_item) - layoutInflater.inflate(R.layout.fragment_list_view, main_content) - list_wrapper.setOnRefreshListener(this) - list.addHeaderView(layoutInflater.inflate(R.layout.fragment_network_list_header, list, false), null, false) - list.addFooterView(View(this), null, false) - list.adapter = networkListAdapter - list.onItemClickListener = this + networkList.init() + + if (intent.action == Actions.ACTION_CONNECT && intent.data?.schemeSpecificPart != null) + connectionStarter.tryStart(intent.data.schemeSpecificPart, intent.data.fragment, false) } override fun onCreateOptionsMenu(m: Menu): Boolean { @@ -41,48 +134,41 @@ class StartActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRefr } override fun onDestroy() { - networkListAdapter = null + networkList.destroy() + connectDialog?.dismiss() super.onDestroy() } override fun onStart() { super.onStart() - onRefresh() + networkList.onRefresh() } override fun onResume() { super.onResume() if (TincVpnService.isConnected()) openStatusActivity() + startupBroadcastReceiver.register() } - override fun onRefresh() { - val networks = AppPaths.confDir()?.list()?.toList() ?: emptyList() - runOnUiThread { - networkListAdapter?.setElements(networks) - setPlaceholderVisibility() - list_wrapper.isRefreshing = false - } + override fun onPause() { + startupBroadcastReceiver.unregister() + super.onPause() } - override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) = - LaunchActivity.connect((view as TextView).text.toString()) + override fun onActivityResult(request: Int, result: Int, data: Intent?): Unit = when (request) { + PERMISSION_REQUEST -> if (result == Activity.RESULT_OK) connectionStarter.tryStart() else Unit + else -> throw IllegalArgumentException("Result for unknown request received.") + } fun openConfigureActivity(@Suppress("UNUSED_PARAMETER") i: MenuItem) = startActivity(Intent(this, ConfigureActivity::class.java)) - fun openStatusActivity() = - startActivity(Intent(this, StatusActivity::class.java) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)) - - private fun setPlaceholderVisibility() = if (networkListAdapter?.isEmpty != false) { - network_list_placeholder.text = getListPlaceholderText() - network_list_placeholder.visibility = View.VISIBLE - } else { - network_list_placeholder.visibility = View.GONE + private fun onVpnStart() { + connectDialog?.dismiss() + if (connectionStarter.displayStatus()) openStatusActivity() + finish() } - private fun getListPlaceholderText() = - if (!AppPaths.storageAvailable()) getText(R.string.message_storage_unavailable) - else getText(R.string.message_no_network_configuration_found) - + private fun openStatusActivity() = + startActivity(Intent(this, StatusActivity::class.java)) } diff --git a/app/src/main/java/org/pacien/tincapp/activities/StatusActivity.kt b/app/src/main/java/org/pacien/tincapp/activities/StatusActivity.kt index 4b5384c..dc45947 100644 --- a/app/src/main/java/org/pacien/tincapp/activities/StatusActivity.kt +++ b/app/src/main/java/org/pacien/tincapp/activities/StatusActivity.kt @@ -1,6 +1,8 @@ package org.pacien.tincapp.activities +import android.app.ProgressDialog import android.content.Intent +import android.content.IntentFilter import android.os.Bundle import android.support.v4.widget.SwipeRefreshLayout import android.support.v7.app.AlertDialog @@ -20,6 +22,8 @@ import org.pacien.tincapp.commands.Tinc import org.pacien.tincapp.data.VpnInterfaceConfiguration import org.pacien.tincapp.extensions.Android.setElements import org.pacien.tincapp.extensions.Android.setText +import org.pacien.tincapp.intent.Actions +import org.pacien.tincapp.intent.SimpleBroadcastReceiver import org.pacien.tincapp.service.TincVpnService import java.util.* import kotlin.concurrent.timerTask @@ -28,16 +32,15 @@ import kotlin.concurrent.timerTask * @author pacien */ class StatusActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRefreshLayout.OnRefreshListener { - + private val shutdownBroadcastReceiver = SimpleBroadcastReceiver(IntentFilter(Actions.EVENT_DISCONNECTED), this::onVpnShutdown) + private var shutdownDialog: ProgressDialog? = null private var nodeListAdapter: ArrayAdapter? = null private var refreshTimer: Timer? = null - private var updateView: Boolean = false + private var listNetworksAfterExit = true override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - nodeListAdapter = ArrayAdapter(this, R.layout.fragment_list_item) - refreshTimer = Timer(true) layoutInflater.inflate(R.layout.fragment_list_view, main_content) list_wrapper.setOnRefreshListener(this) @@ -45,6 +48,13 @@ class StatusActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRef list.addFooterView(View(this), null, false) list.onItemClickListener = this list.adapter = nodeListAdapter + + if (intent.action == Actions.ACTION_DISCONNECT) { + listNetworksAfterExit = false + stopVpn() + } else { + listNetworksAfterExit = true + } } override fun onCreateOptionsMenu(m: Menu): Boolean { @@ -54,38 +64,35 @@ class StatusActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRef override fun onDestroy() { super.onDestroy() - refreshTimer?.cancel() nodeListAdapter = null refreshTimer = null } override fun onStart() { super.onStart() + refreshTimer = Timer(true) + refreshTimer?.schedule(timerTask { updateView() }, NOW, REFRESH_RATE) writeNetworkInfo(TincVpnService.getCurrentInterfaceCfg() ?: VpnInterfaceConfiguration()) - updateView = true - onRefresh() - updateNodeList() } override fun onStop() { + refreshTimer?.cancel() super.onStop() - updateView = false } override fun onResume() { super.onResume() - if (!TincVpnService.isConnected()) openStartActivity() + shutdownBroadcastReceiver.register() + updateView() + } + + override fun onPause() { + shutdownBroadcastReceiver.unregister() + super.onPause() } override fun onRefresh() { - getNodeNames().thenAccept { - runOnUiThread { - nodeListAdapter?.setElements(it) - node_list_placeholder.visibility = if (nodeListAdapter?.isEmpty != false) View.VISIBLE else View.GONE - list_wrapper.isRefreshing = false - if (!TincVpnService.isConnected()) openStartActivity() - } - } + refreshTimer?.schedule(timerTask { updateView() }, NOW) } override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { @@ -97,13 +104,26 @@ class StatusActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRef AlertDialog.Builder(this) .setTitle(R.string.title_node_info) .setView(dialogTextView) - .setPositiveButton(R.string.action_close) { _, _ -> /* nop */ } + .setPositiveButton(R.string.action_close) { _, _ -> Unit } .show() } } } - fun writeNetworkInfo(cfg: VpnInterfaceConfiguration) { + private fun onVpnShutdown() { + shutdownDialog?.dismiss() + if (listNetworksAfterExit) openStartActivity() + finish() + } + + fun stopVpn(@Suppress("UNUSED_PARAMETER") i: MenuItem? = null) { + refreshTimer?.cancel() + list_wrapper.isRefreshing = false + shutdownDialog = showProgressDialog(R.string.message_disconnecting_vpn) + TincVpnService.disconnect() + } + + private fun writeNetworkInfo(cfg: VpnInterfaceConfiguration) { text_network_name.text = TincVpnService.getCurrentNetName() ?: getString(R.string.value_none) text_network_ip_addresses.setText(cfg.addresses.map { it.toSlashSeparated() }) text_network_routes.setText(cfg.routes.map { it.toSlashSeparated() }) @@ -116,28 +136,35 @@ class StatusActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRef text_network_disallowed_applications.setText(cfg.disallowedApplications) } - fun updateNodeList() { - refreshTimer?.schedule(timerTask { - onRefresh() - if (updateView) updateNodeList() - }, REFRESH_RATE) + private fun writeNodeList(nodeList: List) = runOnUiThread { + nodeListAdapter?.setElements(nodeList) + node_list_placeholder.visibility = if (nodeListAdapter?.isEmpty != false) View.VISIBLE else View.GONE + list_wrapper.isRefreshing = false } - fun stopVpn(@Suppress("UNUSED_PARAMETER") i: MenuItem) { - TincVpnService.stopVpn() - openStartActivity() - finish() + private fun updateNodeList() { + getNodeNames().whenComplete { nodeList, _ -> runOnUiThread { writeNodeList(nodeList) } } } - fun openStartActivity() = startActivity(Intent(this, StartActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)) + private fun updateView() = when { + TincVpnService.isConnected() -> updateNodeList() + else -> openStartActivity() + } - companion object { - private val REFRESH_RATE = 5000L + private fun openStartActivity() { + startActivity(Intent(this, StartActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)) + finish() + } - fun getNodeNames(): CompletableFuture> = when (TincVpnService.isConnected()) { - true -> Tinc.dumpNodes(TincVpnService.getCurrentNetName()!!).thenApply> { it.map { it.substringBefore(' ') } } - false -> CompletableFuture.supplyAsync> { emptyList() } + companion object { + private const val REFRESH_RATE = 5000L + private const val NOW = 0L + + fun getNodeNames(): CompletableFuture> = when { + TincVpnService.isConnected() -> + Tinc.dumpNodes(TincVpnService.getCurrentNetName()!!).thenApply> { it.map { it.substringBefore(' ') } } + else -> + CompletableFuture.supplyAsync> { emptyList() } } } - } diff --git a/app/src/main/java/org/pacien/tincapp/commands/Tinc.kt b/app/src/main/java/org/pacien/tincapp/commands/Tinc.kt index 0b1240a..e0cdb12 100644 --- a/app/src/main/java/org/pacien/tincapp/commands/Tinc.kt +++ b/app/src/main/java/org/pacien/tincapp/commands/Tinc.kt @@ -17,6 +17,10 @@ object Tinc { Executor.call(newCommand(netName).withArguments("stop")) .thenApply { } + fun pid(netName: String): CompletableFuture = + Executor.call(newCommand(netName).withArguments("pid")) + .thenApply { Integer.parseInt(it.first()) } + fun dumpNodes(netName: String, reachable: Boolean = false): CompletableFuture> = Executor.call( if (reachable) newCommand(netName).withArguments("dump", "reachable", "nodes") diff --git a/app/src/main/java/org/pacien/tincapp/intent/Actions.kt b/app/src/main/java/org/pacien/tincapp/intent/Actions.kt new file mode 100644 index 0000000..4650952 --- /dev/null +++ b/app/src/main/java/org/pacien/tincapp/intent/Actions.kt @@ -0,0 +1,19 @@ +package org.pacien.tincapp.intent + +import android.net.Uri +import org.pacien.tincapp.BuildConfig + +/** + * @author pacien + */ +object Actions { + const val PREFIX = "${BuildConfig.APPLICATION_ID}.intent.action" + const val ACTION_CONNECT = "$PREFIX.CONNECT" + const val ACTION_DISCONNECT = "$PREFIX.DISCONNECT" + const val EVENT_CONNECTED = "$PREFIX.CONNECTED" + const val EVENT_DISCONNECTED = "$PREFIX.DISCONNECTED" + const val TINC_SCHEME = "tinc" + + fun buildNetworkUri(netName: String, passphrase: String? = null): Uri = + Uri.Builder().scheme(Actions.TINC_SCHEME).opaquePart(netName).fragment(passphrase).build() +} diff --git a/app/src/main/java/org/pacien/tincapp/intent/SimpleBroadcastReceiver.kt b/app/src/main/java/org/pacien/tincapp/intent/SimpleBroadcastReceiver.kt new file mode 100644 index 0000000..fb77174 --- /dev/null +++ b/app/src/main/java/org/pacien/tincapp/intent/SimpleBroadcastReceiver.kt @@ -0,0 +1,19 @@ +package org.pacien.tincapp.intent + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.support.v4.content.LocalBroadcastManager +import org.pacien.tincapp.context.App + +/** + * @author pacien + */ +class SimpleBroadcastReceiver(private val intentFilter: IntentFilter, private val eventHandler: () -> Unit) : BroadcastReceiver() { + private val broadcastManager = LocalBroadcastManager.getInstance(App.getContext()) + + fun register() = broadcastManager.registerReceiver(this, intentFilter) + fun unregister() = broadcastManager.unregisterReceiver(this) + override fun onReceive(context: Context?, intent: Intent?) = eventHandler() +} diff --git a/app/src/main/java/org/pacien/tincapp/intent/action/Actions.kt b/app/src/main/java/org/pacien/tincapp/intent/action/Actions.kt deleted file mode 100644 index ece9b68..0000000 --- a/app/src/main/java/org/pacien/tincapp/intent/action/Actions.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.pacien.tincapp.intent.action - -import org.pacien.tincapp.BuildConfig - -/** - * @author pacien - */ - -private val PREFIX = "${BuildConfig.APPLICATION_ID}.intent.action" - -val ACTION_CONNECT = "$PREFIX.CONNECT" -val ACTION_DISCONNECT = "$PREFIX.DISCONNECT" - -val TINC_SCHEME = "tinc" diff --git a/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt b/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt index ec0512a..ce41b89 100644 --- a/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt +++ b/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt @@ -2,9 +2,9 @@ package org.pacien.tincapp.service import android.app.Service import android.content.Intent -import android.net.Uri import android.net.VpnService import android.os.ParcelFileDescriptor +import android.support.v4.content.LocalBroadcastManager import android.util.Log import java8.util.concurrent.CompletableFuture import org.apache.commons.configuration2.ex.ConversionException @@ -19,32 +19,41 @@ import org.pacien.tincapp.data.TincConfiguration import org.pacien.tincapp.data.VpnInterfaceConfiguration import org.pacien.tincapp.extensions.Java.applyIgnoringException import org.pacien.tincapp.extensions.VpnServiceBuilder.applyCfg -import org.pacien.tincapp.intent.action.TINC_SCHEME -import org.pacien.tincapp.utils.PemUtils -import java.io.File +import org.pacien.tincapp.intent.Actions +import org.pacien.tincapp.utils.TincKeyring import java.io.FileNotFoundException -import java.io.IOException /** * @author pacien */ class TincVpnService : VpnService() { - override fun onDestroy() { stopVpn() super.onDestroy() } override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - if (isConnected()) stopVpn() - startVpn(intent.data.schemeSpecificPart, intent.data.fragment) - return Service.START_REDELIVER_INTENT + Log.i(TAG, intent.action) + + when { + intent.action == Actions.ACTION_CONNECT && intent.scheme == Actions.TINC_SCHEME -> + startVpn(intent.data.schemeSpecificPart, intent.data.fragment) + intent.action == Actions.ACTION_DISCONNECT -> + stopVpn() + else -> + throw IllegalArgumentException("Invalid intent action received.") + } + + return Service.START_NOT_STICKY } - private fun startVpn(netName: String, passphrase: String? = null) { + private fun startVpn(netName: String, passphrase: String? = null): Unit = synchronized(this) { if (netName.isBlank()) return reportError(resources.getString(R.string.message_no_network_name_provided), docTopic = "intent-api") + if (TincKeyring.needsPassphrase(netName) && passphrase == null) + return reportError(resources.getString(R.string.message_passphrase_required)) + if (!AppPaths.storageAvailable()) return reportError(resources.getString(R.string.message_storage_unavailable)) @@ -52,6 +61,7 @@ class TincVpnService : VpnService() { return reportError(resources.getString(R.string.message_no_configuration_for_network_format, netName), docTopic = "configuration") Log.i(TAG, "Starting tinc daemon for network \"$netName\".") + if (isConnected()) stopVpn() val interfaceCfg = try { VpnInterfaceConfiguration.fromIfaceConfiguration(AppPaths.existing(AppPaths.netConfFile(netName))) @@ -65,37 +75,41 @@ class TincVpnService : VpnService() { Builder().setSession(netName) .applyCfg(interfaceCfg) .also { applyIgnoringException(it::addDisallowedApplication, BuildConfig.APPLICATION_ID) } - .establish() + .establish()!! } catch (e: IllegalArgumentException) { return reportError(resources.getString(R.string.message_network_config_invalid_format, e.message!!), e, "network-interface") } val privateKeys = try { - val tincCfg = TincConfiguration.fromTincConfiguration(AppPaths.existing(AppPaths.tincConfFile(netName))) - - Pair( - openPrivateKey(tincCfg.ed25519PrivateKeyFile ?: AppPaths.defaultEd25519PrivateKeyFile(netName), passphrase), - openPrivateKey(tincCfg.privateKeyFile ?: AppPaths.defaultRsaPrivateKeyFile(netName), passphrase) - ) + TincConfiguration.fromTincConfiguration(AppPaths.existing(AppPaths.tincConfFile(netName))).let { tincCfg -> + Pair( + TincKeyring.openPrivateKey(tincCfg.ed25519PrivateKeyFile ?: AppPaths.defaultEd25519PrivateKeyFile(netName), passphrase), + TincKeyring.openPrivateKey(tincCfg.privateKeyFile ?: AppPaths.defaultRsaPrivateKeyFile(netName), passphrase)) + } } catch (e: FileNotFoundException) { Pair(null, null) } catch (e: PEMException) { return reportError(resources.getString(R.string.message_could_not_decrypt_private_keys_format, e.message)) } - val daemon = Tincd.start(netName, deviceFd!!.fd, privateKeys.first?.fd, privateKeys.second?.fd) + val daemon = Tincd.start(netName, deviceFd.fd, privateKeys.first?.fd, privateKeys.second?.fd) setState(netName, interfaceCfg, deviceFd, daemon) - Log.i(TAG, "tinc daemon started.") + waitForDaemonStartup().thenRun { + deviceFd.close() + Log.i(TAG, "tinc daemon started.") + broadcastEvent(Actions.EVENT_CONNECTED) + } } - private fun openPrivateKey(f: File?, passphrase: String?): ParcelFileDescriptor? { - if (f == null || !f.exists() || passphrase == null) return null - - val pipe = ParcelFileDescriptor.createPipe() - val decryptedKey = PemUtils.decrypt(PemUtils.read(f), passphrase) - val outputStream = ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]) - PemUtils.write(decryptedKey, outputStream.writer()) - return pipe[0] + private fun stopVpn(): Unit = synchronized(this) { + Log.i(TAG, "Stopping any running tinc daemon.") + netName?.let { + Tinc.stop(it).thenRun { + Log.i(TAG, "All tinc daemons stopped.") + broadcastEvent(Actions.EVENT_DISCONNECTED) + setState(null, null, null, null) + } + } } private fun reportError(msg: String, e: Throwable? = null, docTopic: String? = null) { @@ -108,10 +122,18 @@ class TincVpnService : VpnService() { if (docTopic != null) resources.getString(R.string.app_doc_url_format, docTopic) else null) } - companion object { + private fun broadcastEvent(event: String) { + LocalBroadcastManager.getInstance(this).sendBroadcast(Intent(event)) + } - val TAG = this::class.java.canonicalName!! + private fun waitForDaemonStartup() = + CompletableFuture + .runAsync { Thread.sleep(SETUP_DELAY) } + .thenCompose { netName?.let { Tinc.pid(it) } ?: CompletableFuture.completedFuture(0) } + companion object { + private const val SETUP_DELAY = 500L // ms + private val TAG = this::class.java.canonicalName!! private var netName: String? = null private var interfaceCfg: VpnInterfaceConfiguration? = null private var fd: ParcelFileDescriptor? = null @@ -119,35 +141,28 @@ class TincVpnService : VpnService() { private fun setState(netName: String?, interfaceCfg: VpnInterfaceConfiguration?, fd: ParcelFileDescriptor?, daemon: CompletableFuture?) { + TincVpnService.netName = netName TincVpnService.interfaceCfg = interfaceCfg TincVpnService.fd = fd TincVpnService.daemon = daemon } - fun startVpn(netName: String, passphrase: String? = null) { - App.getContext().startService(Intent(App.getContext(), TincVpnService::class.java) - .setData(Uri.Builder().scheme(TINC_SCHEME).opaquePart(netName).fragment(passphrase).build())) - } - - fun stopVpn() { - try { - Log.i(TAG, "Stopping any running tinc daemon.") - if (netName != null) Tinc.stop(netName!!) - daemon?.get() - fd?.close() - Log.i(TAG, "All tinc daemons stopped.") - } catch (e: IOException) { - Log.wtf(TAG, e) - } finally { - setState(null, null, null, null) - } - } - fun getCurrentNetName() = netName fun getCurrentInterfaceCfg() = interfaceCfg fun isConnected() = !(daemon?.isDone ?: true) - } + fun connect(netName: String, passphrase: String? = null) { + App.getContext().startService( + Intent(App.getContext(), TincVpnService::class.java) + .setAction(Actions.ACTION_CONNECT) + .setData(Actions.buildNetworkUri(netName, passphrase))) + } + fun disconnect() { + App.getContext().startService( + Intent(App.getContext(), TincVpnService::class.java) + .setAction(Actions.ACTION_DISCONNECT)) + } + } } diff --git a/app/src/main/java/org/pacien/tincapp/utils/TincKeyring.kt b/app/src/main/java/org/pacien/tincapp/utils/TincKeyring.kt new file mode 100644 index 0000000..422763f --- /dev/null +++ b/app/src/main/java/org/pacien/tincapp/utils/TincKeyring.kt @@ -0,0 +1,26 @@ +package org.pacien.tincapp.utils + +import android.os.ParcelFileDescriptor +import org.pacien.tincapp.commands.TincApp +import java.io.File +import java.io.FileNotFoundException + +/** + * @author pacien + */ +object TincKeyring { + fun needsPassphrase(netName: String) = try { + TincApp.listPrivateKeys(netName).filter { it.exists() }.any { PemUtils.isEncrypted(PemUtils.read(it)) } + } catch (e: FileNotFoundException) { + false + } + + fun openPrivateKey(f: File?, passphrase: String?): ParcelFileDescriptor? { + if (f == null || !f.exists() || passphrase == null) return null + val pipe = ParcelFileDescriptor.createPipe() + val decryptedKey = PemUtils.decrypt(PemUtils.read(f), passphrase) + val outputStream = ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]) + PemUtils.write(decryptedKey, outputStream.writer()) + return pipe[0] + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2849c8f..23469c0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,6 +61,7 @@ Unlock Apply Encrypt or decrypt private keys + Dismiss No network configuration has been found. No known node @@ -75,6 +76,9 @@ Encrypting/decrypting private keys Could not decrypt private keys:\n\n%1$s Storage directory is unavailable. + Starting VPN… + Disconnecting VPN… + A passphrase is required to unlock the keyring. none yes -- cgit v1.2.3