aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/org/pacien/tincapp/service/ConfigurationAccessService.kt
diff options
context:
space:
mode:
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}