From dbba24ef009e5fa9ad428b0ba426b41672fa6b2e Mon Sep 17 00:00:00 2001 From: pacien Date: Tue, 7 Aug 2018 00:19:57 +0200 Subject: Rename source directory --- .../org/pacien/tincapp/activities/BaseActivity.kt | 114 ++++++++++ .../pacien/tincapp/activities/ConfigureActivity.kt | 145 +++++++++++++ .../org/pacien/tincapp/activities/StartActivity.kt | 201 ++++++++++++++++++ .../pacien/tincapp/activities/StatusActivity.kt | 196 +++++++++++++++++ .../pacien/tincapp/activities/ViewLogActivity.kt | 164 ++++++++++++++ .../kotlin/org/pacien/tincapp/commands/Command.kt | 46 ++++ .../kotlin/org/pacien/tincapp/commands/Executor.kt | 85 ++++++++ .../kotlin/org/pacien/tincapp/commands/Tinc.kt | 72 +++++++ .../kotlin/org/pacien/tincapp/commands/TincApp.kt | 74 +++++++ .../kotlin/org/pacien/tincapp/commands/Tincd.kt | 37 ++++ .../main/kotlin/org/pacien/tincapp/context/App.kt | 85 ++++++++ .../kotlin/org/pacien/tincapp/context/AppInfo.kt | 47 ++++ .../kotlin/org/pacien/tincapp/context/AppLogger.kt | 63 ++++++ .../tincapp/context/AppNotificationManager.kt | 84 ++++++++ .../kotlin/org/pacien/tincapp/context/AppPaths.kt | 71 +++++++ .../org/pacien/tincapp/context/CrashRecorder.kt | 48 +++++ .../kotlin/org/pacien/tincapp/data/CidrAddress.kt | 39 ++++ .../org/pacien/tincapp/data/TincConfiguration.kt | 41 ++++ .../tincapp/data/VpnInterfaceConfiguration.kt | 92 ++++++++ .../org/pacien/tincapp/extensions/Android.kt | 41 ++++ .../tincapp/extensions/ApacheConfiguration.kt | 33 +++ .../kotlin/org/pacien/tincapp/extensions/Java.kt | 38 ++++ .../pacien/tincapp/extensions/VpnServiceBuilder.kt | 80 +++++++ .../kotlin/org/pacien/tincapp/intent/Actions.kt | 39 ++++ .../org/pacien/tincapp/intent/BroadcastMapper.kt | 38 ++++ .../org/pacien/tincapp/service/TincVpnService.kt | 236 +++++++++++++++++++++ .../kotlin/org/pacien/tincapp/utils/PemUtils.kt | 94 ++++++++ .../org/pacien/tincapp/utils/ProgressModal.kt | 49 +++++ .../kotlin/org/pacien/tincapp/utils/TincKeyring.kt | 44 ++++ 29 files changed, 2396 insertions(+) create mode 100644 app/src/main/kotlin/org/pacien/tincapp/activities/BaseActivity.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/activities/ConfigureActivity.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/activities/StartActivity.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/activities/StatusActivity.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/activities/ViewLogActivity.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/commands/Command.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/commands/Executor.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/commands/Tinc.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/commands/TincApp.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/commands/Tincd.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/context/App.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/context/AppInfo.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/context/AppLogger.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/context/AppNotificationManager.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/context/AppPaths.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/context/CrashRecorder.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/data/CidrAddress.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/data/TincConfiguration.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/data/VpnInterfaceConfiguration.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/extensions/Android.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/extensions/ApacheConfiguration.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/extensions/Java.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/extensions/VpnServiceBuilder.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/intent/Actions.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/intent/BroadcastMapper.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/service/TincVpnService.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/utils/PemUtils.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/utils/ProgressModal.kt create mode 100644 app/src/main/kotlin/org/pacien/tincapp/utils/TincKeyring.kt (limited to 'app/src/main/kotlin/org/pacien') diff --git a/app/src/main/kotlin/org/pacien/tincapp/activities/BaseActivity.kt b/app/src/main/kotlin/org/pacien/tincapp/activities/BaseActivity.kt new file mode 100644 index 0000000..4dc2381 --- /dev/null +++ b/app/src/main/kotlin/org/pacien/tincapp/activities/BaseActivity.kt @@ -0,0 +1,114 @@ +/* + * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon + * Copyright (C) 2017-2018 Pacien TRAN-GIRARD + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.pacien.tincapp.activities + +import android.os.Bundle +import android.support.annotation.StringRes +import android.support.design.widget.Snackbar +import android.support.v7.app.AlertDialog +import android.support.v7.app.AppCompatActivity +import android.view.Menu +import android.view.MenuItem +import kotlinx.android.synthetic.main.base.* +import org.pacien.tincapp.R +import org.pacien.tincapp.context.App +import org.pacien.tincapp.context.AppInfo +import org.pacien.tincapp.context.AppPaths +import org.pacien.tincapp.context.CrashRecorder +import org.pacien.tincapp.utils.ProgressModal + +/** + * @author pacien + */ +abstract class BaseActivity : AppCompatActivity() { + private var active = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.base) + } + + override fun onCreateOptionsMenu(m: Menu): Boolean { + menuInflater.inflate(R.menu.menu_base, m) + return true + } + + override fun onStart() { + super.onStart() + active = true + } + + override fun onResume() { + super.onResume() + active = true + } + + override fun onPause() { + active = false + super.onPause() + } + + override fun onStop() { + active = false + super.onStop() + } + + fun aboutDialog(@Suppress("UNUSED_PARAMETER") i: MenuItem) { + AlertDialog.Builder(this) + .setTitle(resources.getString(R.string.app_name)) + .setMessage(resources.getString(R.string.app_short_desc) + "\n\n" + + resources.getString(R.string.app_copyright) + " " + + resources.getString(R.string.app_license) + "\n\n" + + AppInfo.all()) + .setNeutralButton(R.string.action_open_project_website) { _, _ -> App.openURL(resources.getString(R.string.app_website_url)) } + .setPositiveButton(R.string.action_close) { _, _ -> Unit } + .show() + } + + fun runOnUiThread(action: () -> Unit) { + if (active) super.runOnUiThread(action) + } + + fun handleRecentCrash() { + if (!CrashRecorder.hasPreviouslyCrashed()) return + CrashRecorder.dismissPreviousCrash() + + AlertDialog.Builder(this) + .setTitle(R.string.title_app_crash) + .setMessage(listOf( + resources.getString(R.string.message_app_crash), + resources.getString(R.string.message_crash_logged, AppPaths.appLogFile().absolutePath) + ).joinToString("\n\n")) + .setNeutralButton(R.string.action_send_report) { _, _ -> + App.sendMail( + resources.getString(R.string.app_dev_email), + listOf(R.string.app_name, R.string.title_app_crash).joinToString(" / ", transform = resources::getString), + AppPaths.appLogFile().let { if (it.exists()) it.readText() else "" }) + } + .setPositiveButton(R.string.action_close) { _, _ -> Unit } + .show() + } + + protected fun notify(@StringRes msg: Int) = Snackbar.make(activity_base, msg, Snackbar.LENGTH_LONG).show() + protected fun notify(msg: String) = Snackbar.make(activity_base, msg, Snackbar.LENGTH_LONG).show() + protected fun showProgressDialog(@StringRes msg: Int): AlertDialog = ProgressModal.show(this, getString(msg)) + protected fun showErrorDialog(msg: String): AlertDialog = AlertDialog.Builder(this) + .setTitle(R.string.title_error).setMessage(msg) + .setPositiveButton(R.string.action_close) { _, _ -> Unit }.show() +} diff --git a/app/src/main/kotlin/org/pacien/tincapp/activities/ConfigureActivity.kt b/app/src/main/kotlin/org/pacien/tincapp/activities/ConfigureActivity.kt new file mode 100644 index 0000000..2aab304 --- /dev/null +++ b/app/src/main/kotlin/org/pacien/tincapp/activities/ConfigureActivity.kt @@ -0,0 +1,145 @@ +/* + * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon + * Copyright (C) 2017-2018 Pacien TRAN-GIRARD + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.pacien.tincapp.activities + +import android.content.Intent +import android.os.Bundle +import android.support.annotation.StringRes +import android.support.v7.app.AlertDialog +import android.view.View +import com.google.zxing.integration.android.IntentIntegrator +import com.google.zxing.integration.android.IntentResult +import java8.util.concurrent.CompletableFuture +import kotlinx.android.synthetic.main.base.* +import kotlinx.android.synthetic.main.dialog_encrypt_decrypt_keys.view.* +import kotlinx.android.synthetic.main.dialog_network_generate.view.* +import kotlinx.android.synthetic.main.dialog_network_join.view.* +import kotlinx.android.synthetic.main.page_configure.* +import org.pacien.tincapp.R +import org.pacien.tincapp.commands.Tinc +import org.pacien.tincapp.commands.TincApp +import org.pacien.tincapp.context.AppPaths +import org.pacien.tincapp.extensions.Java.exceptionallyAccept +import java.util.regex.Pattern + +/** + * @author pacien + */ +class ConfigureActivity : BaseActivity() { + companion object { + private val NETWORK_NAME_PATTERN = Pattern.compile("^[^\\x00/]*$") + } + + private var joinDialog: View? = null + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + IntentIntegrator.parseActivityResult(requestCode, resultCode, data) + ?.let(IntentResult::getContents) + ?.let(String::trim) + ?.let { joinDialog?.invitation_url?.setText(it) } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + layoutInflater.inflate(R.layout.page_configure, main_content) + writeContent() + } + + fun scanCode(@Suppress("UNUSED_PARAMETER") v: View) { + IntentIntegrator(this).initiateScan() + } + + fun openGenerateConfDialog(@Suppress("UNUSED_PARAMETER") v: View) { + val genDialog = layoutInflater.inflate(R.layout.dialog_network_generate, main_content, false) + AlertDialog.Builder(this).setTitle(R.string.title_new_network).setView(genDialog) + .setPositiveButton(R.string.action_create) { _, _ -> + generateConf( + genDialog.new_net_name.text.toString(), + genDialog.new_node_name.text.toString(), + genDialog.new_passphrase.text.toString()) + }.setNegativeButton(R.string.action_cancel) { _, _ -> Unit }.show() + } + + fun openJoinNetworkDialog(@Suppress("UNUSED_PARAMETER") v: View) { + joinDialog = layoutInflater.inflate(R.layout.dialog_network_join, main_content, false) + AlertDialog.Builder(this).setTitle(R.string.title_join_network).setView(joinDialog) + .setPositiveButton(R.string.action_join) { _, _ -> + joinNetwork( + joinDialog!!.net_name.text.toString(), + joinDialog!!.invitation_url.text.toString(), + joinDialog!!.join_passphrase.text.toString()) + }.setNegativeButton(R.string.action_cancel) { _, _ -> Unit }.show() + } + + fun openEncryptDecryptPrivateKeyDialog(@Suppress("UNUSED_PARAMETER") v: View) { + val encryptDecryptDialog = layoutInflater.inflate(R.layout.dialog_encrypt_decrypt_keys, main_content, false) + AlertDialog.Builder(this).setTitle(R.string.title_private_keys_encryption).setView(encryptDecryptDialog) + .setPositiveButton(R.string.action_apply) { _, _ -> + encryptDecryptPrivateKeys( + encryptDecryptDialog!!.enc_dec_net_name.text.toString(), + encryptDecryptDialog.enc_dec_current_passphrase.text.toString(), + encryptDecryptDialog.enc_dec_new_passphrase.text.toString()) + }.setNegativeButton(R.string.action_cancel) { _, _ -> Unit }.show() + } + + private fun writeContent() { + text_configuration_directory.text = AppPaths.confDir().absolutePath + text_log_directory.text = AppPaths.cacheDir().absolutePath + text_tinc_binary.text = AppPaths.tinc().absolutePath + } + + private fun generateConf(netName: String, nodeName: String, passphrase: String? = null) = execAction( + R.string.message_generating_configuration, + validateNetName(netName) + .thenCompose { Tinc.init(netName, nodeName) } + .thenCompose { TincApp.removeScripts(netName) } + .thenCompose { TincApp.generateIfaceCfgTemplate(netName) } + .thenCompose { TincApp.setPassphrase(netName, newPassphrase = passphrase) }) + + private fun joinNetwork(netName: String, url: String, passphrase: String? = null) = execAction( + R.string.message_joining_network, + validateNetName(netName) + .thenCompose { Tinc.join(netName, url) } + .thenCompose { TincApp.removeScripts(netName) } + .thenCompose { TincApp.generateIfaceCfg(netName) } + .thenCompose { TincApp.setPassphrase(netName, newPassphrase = passphrase) }) + + private fun encryptDecryptPrivateKeys(netName: String, currentPassphrase: String, newPassphrase: String) = execAction( + R.string.message_encrypting_decrypting_private_keys, + validateNetName(netName) + .thenCompose { TincApp.setPassphrase(netName, currentPassphrase, newPassphrase) }) + + private fun execAction(@StringRes label: Int, action: CompletableFuture) { + showProgressDialog(label).let { progressDialog -> + action + .whenComplete { _, _ -> progressDialog.dismiss() } + .thenAccept { notify(R.string.message_network_configuration_written) } + .exceptionallyAccept { runOnUiThread { showErrorDialog(it.cause!!.localizedMessage) } } + } + } + + private fun validateNetName(netName: String): CompletableFuture = + if (NETWORK_NAME_PATTERN.matcher(netName).matches()) + CompletableFuture.completedFuture(Unit) + else + CompletableFuture.failedFuture(IllegalArgumentException(resources.getString(R.string.message_invalid_network_name))) +} diff --git a/app/src/main/kotlin/org/pacien/tincapp/activities/StartActivity.kt b/app/src/main/kotlin/org/pacien/tincapp/activities/StartActivity.kt new file mode 100644 index 0000000..1e267d6 --- /dev/null +++ b/app/src/main/kotlin/org/pacien/tincapp/activities/StartActivity.kt @@ -0,0 +1,201 @@ +/* + * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon + * Copyright (C) 2017-2018 Pacien TRAN-GIRARD + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.pacien.tincapp.activities + +import android.app.Activity +import android.content.Intent +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 +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.BroadcastMapper +import org.pacien.tincapp.service.TincVpnService +import org.pacien.tincapp.utils.TincKeyring + +/** + * @author pacien + */ +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 + + fun tryStart(netName: String? = null, passphrase: String? = null, displayStatus: Boolean? = null) { + if (netName != null) this.netName = netName + 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 broadcastMapper = BroadcastMapper(mapOf( + Actions.EVENT_CONNECTED to this::onVpnStart, + Actions.EVENT_ABORTED to this::onVpnStartError)) + + private var connectDialog: AlertDialog? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + 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 { + menuInflater.inflate(R.menu.menu_start, m) + return super.onCreateOptionsMenu(m) + } + + override fun onDestroy() { + networkList.destroy() + connectDialog?.dismiss() + super.onDestroy() + } + + override fun onStart() { + super.onStart() + networkList.onRefresh() + } + + override fun onResume() { + super.onResume() + if (TincVpnService.isConnected()) openStatusActivity(false) + broadcastMapper.register() + handleRecentCrash() + } + + override fun onPause() { + broadcastMapper.unregister() + super.onPause() + } + + 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)) + + private fun onVpnStart() { + connectDialog?.dismiss() + if (connectionStarter.displayStatus()) openStatusActivity() + finish() + } + + private fun onVpnStartError() { + connectDialog?.dismiss() + } + + private fun openStatusActivity(transition: Boolean = true) = + startActivity( + Intent(this, StatusActivity::class.java) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + .apply { if (!transition) addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) }) +} diff --git a/app/src/main/kotlin/org/pacien/tincapp/activities/StatusActivity.kt b/app/src/main/kotlin/org/pacien/tincapp/activities/StatusActivity.kt new file mode 100644 index 0000000..68e008e --- /dev/null +++ b/app/src/main/kotlin/org/pacien/tincapp/activities/StatusActivity.kt @@ -0,0 +1,196 @@ +/* + * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon + * Copyright (C) 2017-2018 Pacien TRAN-GIRARD + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.pacien.tincapp.activities + +import android.content.Intent +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 +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.TextView +import java8.util.concurrent.CompletableFuture +import kotlinx.android.synthetic.main.base.* +import kotlinx.android.synthetic.main.dialog_node_details.view.* +import kotlinx.android.synthetic.main.fragment_list_view.* +import kotlinx.android.synthetic.main.fragment_network_status_header.* +import org.pacien.tincapp.R +import org.pacien.tincapp.commands.Executor +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.BroadcastMapper +import org.pacien.tincapp.service.TincVpnService +import java.util.* +import kotlin.concurrent.timerTask + +/** + * @author pacien + */ +class StatusActivity : BaseActivity(), AdapterView.OnItemClickListener, SwipeRefreshLayout.OnRefreshListener { + private val broadcastMapper = BroadcastMapper(mapOf(Actions.EVENT_DISCONNECTED to this::onVpnShutdown)) + private var shutdownDialog: AlertDialog? = null + private var nodeListAdapter: ArrayAdapter? = null + private var refreshTimer: Timer? = null + private var listNetworksAfterExit = true + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + nodeListAdapter = 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_status_header, list, false), null, false) + 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 { + menuInflater.inflate(R.menu.menu_status, m) + return super.onCreateOptionsMenu(m) + } + + override fun onDestroy() { + super.onDestroy() + nodeListAdapter = null + refreshTimer = null + } + + override fun onStart() { + super.onStart() + refreshTimer = Timer(true) + refreshTimer?.schedule(timerTask { updateView() }, NOW, REFRESH_RATE) + writeNetworkInfo(TincVpnService.getCurrentInterfaceCfg() ?: VpnInterfaceConfiguration()) + } + + override fun onStop() { + refreshTimer?.cancel() + super.onStop() + } + + override fun onResume() { + super.onResume() + broadcastMapper.register() + updateView() + handleRecentCrash() + } + + override fun onPause() { + broadcastMapper.unregister() + super.onPause() + } + + override fun onRefresh() { + refreshTimer?.schedule(timerTask { updateView() }, NOW) + } + + override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) = when (view) { + is TextView -> showNodeInfo(view.text.toString()) + else -> Unit + } + + 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() + } + + fun openLogViewer(@Suppress("UNUSED_PARAMETER") i: MenuItem) = + startActivity(Intent(this, ViewLogActivity::class.java)) + + 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() }) + text_network_dns_servers.setText(cfg.dnsServers) + text_network_search_domains.setText(cfg.searchDomains) + text_network_allow_bypass.text = getString(if (cfg.allowBypass) R.string.value_yes else R.string.value_no) + block_network_allowed_applications.visibility = if (cfg.allowedApplications.isNotEmpty()) View.VISIBLE else View.GONE + text_network_allowed_applications.setText(cfg.allowedApplications) + block_network_disallowed_applications.visibility = if (cfg.disallowedApplications.isNotEmpty()) View.VISIBLE else View.GONE + text_network_disallowed_applications.setText(cfg.disallowedApplications) + } + + private fun writeNodeList(nodeList: List) { + nodeListAdapter?.setElements(nodeList) + node_list_placeholder.visibility = View.GONE + list_wrapper.isRefreshing = false + } + + private fun updateNodeList() { + getNodeNames().thenAccept { nodeList -> runOnUiThread { writeNodeList(nodeList) } } + } + + private fun showNodeInfo(nodeName: String) { + val dialogTextView = layoutInflater.inflate(R.layout.dialog_node_details, main_content, false) + + runOnUiThread { + AlertDialog.Builder(this) + .setTitle(R.string.title_node_info) + .setView(dialogTextView) + .setPositiveButton(R.string.action_close) { _, _ -> Unit } + .show() + } + + TincVpnService.getCurrentNetName()?.let { netName -> + Tinc.info(netName, nodeName).thenAccept { nodeInfo -> + runOnUiThread { dialogTextView.dialog_node_details.text = nodeInfo } + } + } + } + + private fun updateView() = when { + TincVpnService.isConnected() -> updateNodeList() + else -> openStartActivity() + } + + private fun openStartActivity() { + startActivity(Intent(this, StartActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)) + finish() + } + + companion object { + private const val REFRESH_RATE = 5000L + private const val NOW = 0L + + fun getNodeNames(): CompletableFuture> = TincVpnService.getCurrentNetName()?.let { netName -> + Tinc.dumpNodes(netName).thenApply> { list -> list.map { it.substringBefore(' ') } } + } ?: Executor.supplyAsyncTask> { emptyList() } + } +} diff --git a/app/src/main/kotlin/org/pacien/tincapp/activities/ViewLogActivity.kt b/app/src/main/kotlin/org/pacien/tincapp/activities/ViewLogActivity.kt new file mode 100644 index 0000000..f3f7e24 --- /dev/null +++ b/app/src/main/kotlin/org/pacien/tincapp/activities/ViewLogActivity.kt @@ -0,0 +1,164 @@ +/* + * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon + * Copyright (C) 2017-2018 Pacien TRAN-GIRARD + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.pacien.tincapp.activities + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.ScrollView +import kotlinx.android.synthetic.main.base.* +import kotlinx.android.synthetic.main.page_viewlog.* +import org.pacien.tincapp.R +import org.pacien.tincapp.commands.Executor +import org.pacien.tincapp.commands.Tinc +import org.pacien.tincapp.service.TincVpnService +import java.util.* +import kotlin.concurrent.timer + +/** + * @author pacien + */ +class ViewLogActivity : BaseActivity() { + companion object { + private const val LOG_LINES = 250 + private const val LOG_LEVEL = 5 + private const val NEW_LINE = "\n" + private const val SPACED_NEW_LINE = "\n\n" + private const val UPDATE_INTERVAL = 250L // ms + private const val MIME_TYPE = "text/plain" + } + + private val log = LinkedList() + private var logUpdateTimer: Timer? = null + private var logger: Process? = null + private var toggleButton: MenuItem? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + layoutInflater.inflate(R.layout.page_viewlog, main_content) + toggleLogging(true) + } + + override fun onCreateOptionsMenu(m: Menu): Boolean { + menuInflater.inflate(R.menu.menu_viewlog, m) + toggleButton = m.findItem(R.id.log_viewer_action_toggle) + return super.onCreateOptionsMenu(m) + } + + override fun onSupportNavigateUp(): Boolean { + finish() + return true + } + + override fun onDestroy() { + toggleLogging(false) + super.onDestroy() + } + + fun share(@Suppress("UNUSED_PARAMETER") menuItem: MenuItem) { + synchronized(this) { + val logFragment = log.joinToString(NEW_LINE) + val shareIntent = Intent(Intent.ACTION_SEND) + .setType(MIME_TYPE) + .putExtra(Intent.EXTRA_TEXT, logFragment) + + startActivity(Intent.createChooser(shareIntent, resources.getString(R.string.menu_share_log))) + } + } + + fun toggleLogging(@Suppress("UNUSED_PARAMETER") menuItem: MenuItem) = toggleLogging(logger == null) + + private fun toggleLogging(enable: Boolean) { + if (enable) { + disableUserScroll() + toggleButton?.setIcon(R.drawable.ic_pause_circle_outline_primary_24dp) + startLogging() + } else { + enableUserScroll() + toggleButton?.setIcon(R.drawable.ic_pause_circle_filled_primary_24dp) + stopLogging() + } + } + + private fun startLogging(level: Int = LOG_LEVEL) { + appendLog(resources.getString(R.string.message_log_level_set, level)) + + TincVpnService.getCurrentNetName()?.let { netName -> + Tinc.log(netName, level).let { process -> + logger = process + Executor.runAsyncTask { captureLog(process) } + } + logUpdateTimer = timer(period = UPDATE_INTERVAL, action = { printLog() }) + } ?: run { + appendLog(resources.getString(R.string.message_no_daemon)) + toggleLogging(false) + } + } + + private fun stopLogging() { + logger?.destroy() + logger = null + logUpdateTimer?.cancel() + logUpdateTimer?.purge() + logUpdateTimer = null + appendLog(resources.getString(R.string.message_log_paused)) + printLog() + } + + private fun captureLog(logger: Process) { + logger.inputStream?.use { inputStream -> + inputStream.bufferedReader().useLines { lines -> + lines.forEach { appendLog(it) } + } + } + } + + private fun appendLog(line: String) = synchronized(this) { + if (log.size >= LOG_LINES) log.removeFirst() + log.addLast(line) + } + + private fun printLog() = synchronized(this) { + log.joinToString(SPACED_NEW_LINE).let { + logview_text.post { + logview_text.text = it + logview_frame.post { logview_frame.fullScroll(View.FOCUS_DOWN) } + } + } + } + + private fun enableUserScroll() { + logview_text.setTextIsSelectable(true) + logview_frame.setState(true) + } + + private fun disableUserScroll() { + logview_text.setTextIsSelectable(false) + logview_frame.setState(false) + } + + private fun ScrollView.setState(enabled: Boolean) { + if (enabled) setOnTouchListener(null) else setOnTouchListener { _, _ -> true } + logview_frame.isSmoothScrollingEnabled = enabled + logview_frame.isVerticalScrollBarEnabled = enabled + } +} diff --git a/app/src/main/kotlin/org/pacien/tincapp/commands/Command.kt b/app/src/main/kotlin/org/pacien/tincapp/commands/Command.kt new file mode 100644 index 0000000..132cda9 --- /dev/null +++ b/app/src/main/kotlin/org/pacien/tincapp/commands/Command.kt @@ -0,0 +1,46 @@ +/* + * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon + * Copyright (C) 2017-2018 Pacien TRAN-GIRARD + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.pacien.tincapp.commands + +import java.util.* + +/** + * @author pacien + */ +internal class Command(private val cmd: String) { + private data class Option(val key: String, val value: String?) { + fun toCommandLineOption(): String = if (value != null) "--$key=$value" else "--$key" + } + + private val opts: MutableList