From 20ecd9840f1e237dba79674b71e49b43b074902e Mon Sep 17 00:00:00 2001 From: pacien Date: Tue, 8 Dec 2020 16:16:08 +0100 Subject: app: add configuration FTP server This is a ridiculous workaround to make the configuration (and other files) accessible to the user necessary after the new storage access restriction enforced in Android 11 which prevent other applications from accessing the supposedly public application's directory. The app's internal private storage directory is now exposed to the user through an embedded FTP server that the user can turn on and off from the configuration activity. The user can then play with the configuration and retrieve logs through a remote or local FTP client application of their choice. GitHub: closes #103 --- app/build.gradle | 9 ++ app/proguard-rules.pro | 4 +- app/src/main/AndroidManifest.xml | 4 + .../configure/ConfigurationFtpServerFragment.kt | 72 ++++++++++++ .../tincapp/service/ConfigurationFtpService.kt | 128 +++++++++++++++++++++ app/src/main/res/layout/configure_activity.xml | 24 ++-- ...ure_tools_configuration_ftp_server_fragment.xml | 86 ++++++++++++++ app/src/main/res/values-nb-rNO/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values-zh-rHK/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - app/src/main/res/values/strings.xml | 8 +- readme.md | 1 + 13 files changed, 327 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/org/pacien/tincapp/activities/configure/ConfigurationFtpServerFragment.kt create mode 100644 app/src/main/java/org/pacien/tincapp/service/ConfigurationFtpService.kt create mode 100644 app/src/main/res/layout/configure_tools_configuration_ftp_server_fragment.xml diff --git a/app/build.gradle b/app/build.gradle index 99e9226..41c75ef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -70,6 +70,14 @@ android { buildFeatures { dataBinding = true } + + packagingOptions { + // clashing in dependencies (Apache Mina FtpServer) + exclude 'META-INF/DEPENDENCIES' + exclude 'META-INF/spring.schemas' + exclude 'META-INF/spring.handlers' + exclude 'META-INF/license.txt' + } } dependencies { @@ -88,6 +96,7 @@ dependencies { implementation('org.apache.commons:commons-configuration2:2.3') { exclude group: 'commons-logging', module: 'commons-logging' } implementation('commons-beanutils:commons-beanutils:1.9.3') { exclude group: 'commons-logging', module: 'commons-logging' } implementation('commons-io:commons-io:2.6') { exclude group: 'commons-logging', module: 'commons-logging' } + implementation('org.apache.ftpserver:ftpserver:1.1.1') { exclude group: 'org.slf4j', module: 'slf4j-log4j12' } } repositories { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 5100915..ffbb3e3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,5 +1,5 @@ # Tinc App, an Android binding and user interface for the tinc mesh VPN daemon -# Copyright (C) 2017-2019 Pacien TRAN-GIRARD +# Copyright (C) 2017-2020 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 @@ -15,6 +15,8 @@ # along with this program. If not, see . -keep class org.apache.commons.** { *; } +-keep class org.apache.mina.** { *; } +-keep class org.apache.ftpserver.** { *; } -keep class org.bouncycastle.** -keep class ch.qos.** { *; } -keep class org.slf4j.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c86ec8f..020d62d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -81,6 +81,10 @@ + + + diff --git a/app/src/main/java/org/pacien/tincapp/activities/configure/ConfigurationFtpServerFragment.kt b/app/src/main/java/org/pacien/tincapp/activities/configure/ConfigurationFtpServerFragment.kt new file mode 100644 index 0000000..b97a15e --- /dev/null +++ b/app/src/main/java/org/pacien/tincapp/activities/configure/ConfigurationFtpServerFragment.kt @@ -0,0 +1,72 @@ +/* + * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon + * Copyright (C) 2017-2020 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.configure + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.Observable +import androidx.databinding.ObservableBoolean +import org.pacien.tincapp.activities.BaseFragment +import org.pacien.tincapp.databinding.ConfigureToolsConfigurationFtpServerFragmentBinding +import org.pacien.tincapp.service.ConfigurationFtpService + +/** + * @author pacien + */ +class ConfigurationFtpServerFragment : BaseFragment() { + private val ftpServerStartListener = object : Observable.OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable, propertyId: Int) { + binding.ftpEnabled = (sender as ObservableBoolean).get() + } + } + + private lateinit var binding: ConfigureToolsConfigurationFtpServerFragmentBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = ConfigureToolsConfigurationFtpServerFragmentBinding.inflate(inflater, container, false) + binding.ftpUsername = ConfigurationFtpService.FTP_USERNAME + binding.ftpPassword = ConfigurationFtpService.FTP_PASSWORD + binding.ftpPort = ConfigurationFtpService.FTP_PORT + binding.toggleFtpState = { toggleServer() } + return binding.root + } + + override fun onResume() { + super.onResume() + ConfigurationFtpService.runningState.addOnPropertyChangedCallback(ftpServerStartListener) + binding.ftpEnabled = ConfigurationFtpService.runningState.get() + } + + override fun onPause() { + ConfigurationFtpService.runningState.removeOnPropertyChangedCallback(ftpServerStartListener) + super.onPause() + } + + private fun toggleServer() { + val targetServiceIntent = Intent(requireContext(), ConfigurationFtpService::class.java) + + if (binding.ftpEnabled) + requireContext().stopService(targetServiceIntent) + else + requireContext().startService(targetServiceIntent) + } +} diff --git a/app/src/main/java/org/pacien/tincapp/service/ConfigurationFtpService.kt b/app/src/main/java/org/pacien/tincapp/service/ConfigurationFtpService.kt new file mode 100644 index 0000000..c562768 --- /dev/null +++ b/app/src/main/java/org/pacien/tincapp/service/ConfigurationFtpService.kt @@ -0,0 +1,128 @@ +/* + * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon + * Copyright (C) 2017-2020 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.service + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import androidx.databinding.ObservableBoolean +import org.apache.ftpserver.FtpServer +import org.apache.ftpserver.FtpServerFactory +import org.apache.ftpserver.ftplet.* +import org.apache.ftpserver.listener.ListenerFactory +import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication +import org.apache.ftpserver.usermanager.impl.WritePermission +import org.pacien.tincapp.R +import org.pacien.tincapp.context.App +import org.pacien.tincapp.extensions.Java.defaultMessage +import org.slf4j.LoggerFactory +import java.io.IOException + +/** + * FTP server service allowing a remote and local user to access and modify configuration files in + * the application's context. + * + * @author pacien + */ +class ConfigurationFtpService : Service() { + companion object { + const val FTP_PORT = 65521 // tinc port `concat` FTP port + const val FTP_USERNAME = "tincapp" + val FTP_HOME_DIR = App.getContext().applicationInfo.dataDir!! + val FTP_PASSWORD = generateRandomString(8) + + val runningState = ObservableBoolean(false) + + private fun generateRandomString(length: Int): String { + val alphabet = ('a'..'z') + ('A'..'Z') + ('0'..'9') + return List(length) { alphabet.random() }.joinToString("") + } + } + + private val log by lazy { LoggerFactory.getLogger(this.javaClass)!! } + private var sftpServer: FtpServer? = null + + override fun onBind(intent: Intent): IBinder? = null // non-bindable service + + override fun onDestroy() { + sftpServer?.stop() + sftpServer = null + runningState.set(false) + log.info("Stopped FTP server") + super.onDestroy() + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + val ftpUser = StaticFtpUser(FTP_USERNAME, FTP_PASSWORD, FTP_HOME_DIR, listOf(WritePermission())) + sftpServer = setupSingleUserServer(ftpUser).also { + try { + it.start() + runningState.set(true) + log.info("Started FTP server on port {}", FTP_PORT) + } catch (e: IOException) { + log.error("Could not start FTP server", e) + App.alert(R.string.notification_error_title_unable_to_start_ftp_server, e.defaultMessage()) + } + } + + return START_NOT_STICKY + } + + private fun setupSingleUserServer(ftpUser: User): FtpServer { + return FtpServerFactory() + .apply { addListener("default", ListenerFactory().apply { port = FTP_PORT }.createListener()) } + .apply { userManager = StaticFtpUserManager(listOf(ftpUser)) } + .createServer() + } + + private class StaticFtpUserManager(users: List) : UserManager { + private val userMap: Map = users.map { it.name to it }.toMap() + override fun getUserByName(username: String?): User? = userMap[username] + override fun getAllUserNames(): Array = userMap.keys.toTypedArray() + override fun doesExist(username: String?): Boolean = username in userMap + override fun delete(username: String?) = throw UnsupportedOperationException() + override fun save(user: User?) = throw UnsupportedOperationException() + override fun getAdminName(): String = throw UnsupportedOperationException() + override fun isAdmin(username: String?): Boolean = throw UnsupportedOperationException() + override fun authenticate(authentication: Authentication?): User = when (authentication) { + is UsernamePasswordAuthentication -> getUserByName(authentication.username).let { + if (it != null && authentication.password == it.password) it + else throw AuthenticationFailedException() + } + else -> throw IllegalArgumentException() + } + } + + private data class StaticFtpUser( + private val name: String, + private val password: String, + private val homeDirectory: String, + private val authorities: List + ) : User { + override fun getName(): String = name + override fun getPassword(): String = password + override fun getAuthorities(): List = authorities + override fun getAuthorities(clazz: Class): List = authorities.filter(clazz::isInstance) + override fun getMaxIdleTime(): Int = 0 // unlimited + override fun getEnabled(): Boolean = true + override fun getHomeDirectory(): String = homeDirectory + override fun authorize(request: AuthorizationRequest?): AuthorizationRequest? = + authorities.filter { it.canAuthorize(request) }.fold(request) { req, auth -> auth.authorize(req) } + } +} diff --git a/app/src/main/res/layout/configure_activity.xml b/app/src/main/res/layout/configure_activity.xml index 6097384..2fde4a2 100644 --- a/app/src/main/res/layout/configure_activity.xml +++ b/app/src/main/res/layout/configure_activity.xml @@ -2,7 +2,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 9c330a7..346a2d1 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -80,7 +80,6 @@ Ugyldig nettverksnavn. Nettverksoppsett skrevet. - Sti-info Oppsettsmappe Loggingsmappe tinc-binærfil diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b5d12b1..536403e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -80,7 +80,6 @@ 网络名称无效。 网络配置写入成功。 - 路径信息 配置目录 日志目录 Tinc 二进制文件 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index bddb004..e9c48d2 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -80,7 +80,6 @@ 網絡名稱無效。 網絡配置寫入成功。 - 路徑信息 配置目錄 日誌目錄 Tinc 二進制文件 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index c423b15..82eec1d 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -80,7 +80,6 @@ 網路名稱無效。 網路配置寫入成功。 - 路徑資訊 配置目錄 日誌目錄 Tinc 二進位制檔案 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 770f61c..cae7fe5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -62,6 +62,7 @@ Errors Open manual Could not start tinc + Could not start FTP server Could not read private tinc keys:\n%1$s Could not read network interface configuration:\n%1$s Could not bind network interface. Is another VPN running? @@ -84,7 +85,12 @@ Invalid network name. Network configuration written. - Path info + Configuration server + FTP access + User: %1$s, password: %2$s, port: %3$d + Not active + + Internal paths info Configuration directory Log directory tinc binary diff --git a/readme.md b/readme.md index 070023f..5df80d0 100644 --- a/readme.md +++ b/readme.md @@ -57,6 +57,7 @@ Builds of this software embed and make use of the following libraries: * logback-android, licensed under the GNU Lesser General Public License v2.1 * Apache Commons Configuration, licensed under the Apache v2.0 License * Apache Commons BeanUtils, licensed under the Apache v2.0 License +* Apache Mina FtpServer, licensed under the Apache v2.0 License * LZO, licensed under the GNU General Public License v2.0 * LibreSSL libcrypto, licensed under the OpenSSL License, ISC License, public domain * tinc, licensed under the GNU General Public License v2.0 -- cgit v1.2.3