aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/org/pacien/tincapp/service/ConfigurationAccessService.kt
diff options
context:
space:
mode:
authorpacien2020-12-08 18:03:20 +0100
committerpacien2020-12-08 18:03:20 +0100
commit355251694d63640f028f3e2c17235d12a8573df6 (patch)
tree966ffd45ec29797afb440ead00586f4fcdf15589 /app/src/main/java/org/pacien/tincapp/service/ConfigurationAccessService.kt
parent2760703484f9b12f8c21c395915f9780b1ae7e9e (diff)
downloadtincapp-355251694d63640f028f3e2c17235d12a8573df6.tar.gz
ConfigurationAccessService: prevent service from being stopped when app loses focus
This makes the ConfigurationAccessService (formerly ConfigurationFtpService) start in foreground through the use of a persistent notification so that it isn't stopped by the system after the app loses the focus on the user's screen, which happens when the user switches to an FTP client application on the same device.
Diffstat (limited to 'app/src/main/java/org/pacien/tincapp/service/ConfigurationAccessService.kt')
-rw-r--r--app/src/main/java/org/pacien/tincapp/service/ConfigurationAccessService.kt174
1 files changed, 174 insertions, 0 deletions
diff --git a/app/src/main/java/org/pacien/tincapp/service/ConfigurationAccessService.kt b/app/src/main/java/org/pacien/tincapp/service/ConfigurationAccessService.kt
new file mode 100644
index 0000000..b083a83
--- /dev/null
+++ b/app/src/main/java/org/pacien/tincapp/service/ConfigurationAccessService.kt
@@ -0,0 +1,174 @@
1/*
2 * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon
3 * Copyright (C) 2017-2020 Pacien TRAN-GIRARD
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19package org.pacien.tincapp.service
20
21import android.app.PendingIntent
22import android.app.Service
23import android.content.Intent
24import android.os.IBinder
25import androidx.databinding.ObservableBoolean
26import ch.qos.logback.classic.Level
27import ch.qos.logback.classic.Logger
28import org.apache.ftpserver.FtpServer
29import org.apache.ftpserver.FtpServerFactory
30import org.apache.ftpserver.ftplet.*
31import org.apache.ftpserver.listener.ListenerFactory
32import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication
33import org.apache.ftpserver.usermanager.impl.WritePermission
34import org.pacien.tincapp.R
35import org.pacien.tincapp.activities.configure.ConfigureActivity
36import org.pacien.tincapp.context.App
37import org.pacien.tincapp.context.AppNotificationManager
38import org.pacien.tincapp.extensions.Java.defaultMessage
39import org.slf4j.LoggerFactory
40import java.io.IOException
41
42/**
43 * FTP server service allowing a remote and local user to access and modify configuration files in
44 * the application's context.
45 *
46 * @author pacien
47 */
48class ConfigurationAccessService : Service() {
49 companion object {
50 // Apache Mina FtpServer's INFO log level is actually VERBOSE.
51 // The object holds static references to those loggers so that they stay around.
52 @Suppress("unused")
53 private val MINA_FTP_LOGGER_OVERRIDER = MinaLoggerOverrider(Level.WARN)
54
55 const val FTP_PORT = 65521 // tinc port `concat` FTP port
56 const val FTP_USERNAME = "tincapp"
57 val FTP_HOME_DIR = App.getContext().applicationInfo.dataDir!!
58 val FTP_PASSWORD = generateRandomString(8)
59
60 val runningState = ObservableBoolean(false)
61
62 private fun generateRandomString(length: Int): String {
63 val alphabet = ('a'..'z') + ('A'..'Z') + ('0'..'9')
64 return List(length) { alphabet.random() }.joinToString("")
65 }
66 }
67
68 private val log by lazy { LoggerFactory.getLogger(this.javaClass)!! }
69 private val notificationManager by lazy { App.notificationManager }
70 private var sftpServer: FtpServer? = null
71
72 override fun onBind(intent: Intent): IBinder? = null // non-bindable service
73
74 override fun onDestroy() {
75 sftpServer?.stop()
76 sftpServer = null
77 runningState.set(false)
78 log.info("Stopped FTP server")
79 super.onDestroy()
80 }
81
82 override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
83 val ftpUser = StaticFtpUser(FTP_USERNAME, FTP_PASSWORD, FTP_HOME_DIR, listOf(WritePermission()))
84 sftpServer = setupSingleUserServer(ftpUser).also {
85 try {
86 it.start()
87 runningState.set(true)
88 log.info("Started FTP server on port {}", FTP_PORT)
89 pinInForeground()
90 } catch (e: IOException) {
91 log.error("Could not start FTP server", e)
92 App.alert(R.string.notification_error_title_unable_to_start_ftp_server, e.defaultMessage())
93 }
94 }
95
96 return START_NOT_STICKY
97 }
98
99 /**
100 * Pins the service in the foreground so that it doesn't get stopped by the system when the
101 * application's activities are put in the background, which is the case when the user sets the
102 * focus on an FTP client app for example.
103 */
104 private fun pinInForeground() {
105 startForeground(
106 AppNotificationManager.CONFIG_ACCESS_NOTIFICATION_ID,
107 notificationManager.newConfigurationAccessNotificationBuilder()
108 .setSmallIcon(R.drawable.ic_baseline_folder_open_primary_24dp)
109 .setContentTitle(resources.getString(R.string.notification_config_access_server_running_title))
110 .setContentText(resources.getString(R.string.notification_config_access_server_running_message))
111 .setContentIntent(Intent(this, ConfigureActivity::class.java).let {
112 PendingIntent.getActivity(this, 0, it, 0)
113 })
114 .build()
115 )
116 }
117
118 private fun setupSingleUserServer(ftpUser: User): FtpServer {
119 return FtpServerFactory()
120 .apply { addListener("default", ListenerFactory().apply { port = FTP_PORT }.createListener()) }
121 .apply { userManager = StaticFtpUserManager(listOf(ftpUser)) }
122 .createServer()
123 }
124
125 private class StaticFtpUserManager(users: List<User>) : UserManager {
126 private val userMap: Map<String, User> = users.map { it.name to it }.toMap()
127 override fun getUserByName(username: String?): User? = userMap[username]
128 override fun getAllUserNames(): Array<String> = userMap.keys.toTypedArray()
129 override fun doesExist(username: String?): Boolean = username in userMap
130 override fun delete(username: String?) = throw UnsupportedOperationException()
131 override fun save(user: User?) = throw UnsupportedOperationException()
132 override fun getAdminName(): String = throw UnsupportedOperationException()
133 override fun isAdmin(username: String?): Boolean = throw UnsupportedOperationException()
134 override fun authenticate(authentication: Authentication?): User = when (authentication) {
135 is UsernamePasswordAuthentication -> getUserByName(authentication.username).let {
136 if (it != null && authentication.password == it.password) it
137 else throw AuthenticationFailedException()
138 }
139 else -> throw IllegalArgumentException()
140 }
141 }
142
143 private data class StaticFtpUser(
144 private val name: String,
145 private val password: String,
146 private val homeDirectory: String,
147 private val authorities: List<Authority>
148 ) : User {
149 override fun getName(): String = name
150 override fun getPassword(): String = password
151 override fun getAuthorities(): List<Authority> = authorities
152 override fun getAuthorities(clazz: Class<out Authority>): List<Authority> = authorities.filter(clazz::isInstance)
153 override fun getMaxIdleTime(): Int = 0 // unlimited
154 override fun getEnabled(): Boolean = true
155 override fun getHomeDirectory(): String = homeDirectory
156 override fun authorize(request: AuthorizationRequest?): AuthorizationRequest? =
157 authorities.filter { it.canAuthorize(request) }.fold(request) { req, auth -> auth.authorize(req) }
158 }
159
160 /**
161 * This registers package loggers filtering the output of the Mina FtpServer.
162 * The object holds static references to those loggers so that they stay around.
163 */
164 private class MinaLoggerOverrider(logLevel: Level) {
165 @Suppress("unused")
166 private val ftpServerLogger = forceLogLevel("org.apache.ftpserver", logLevel)
167
168 @Suppress("unused")
169 private val minaLogger = forceLogLevel("org.apache.mina", logLevel)
170
171 private fun forceLogLevel(pkgName: String, logLevel: Level) =
172 (LoggerFactory.getLogger(pkgName) as Logger).apply { level = logLevel }
173 }
174}