diff options
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.kt | 204 |
1 files changed, 0 insertions, 204 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 deleted file mode 100644 index 916f19d..0000000 --- a/app/src/main/java/org/pacien/tincapp/service/ConfigurationAccessService.kt +++ /dev/null | |||
@@ -1,204 +0,0 @@ | |||
1 | /* | ||
2 | * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon | ||
3 | * Copyright (C) 2017-2023 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 | |||
19 | package org.pacien.tincapp.service | ||
20 | |||
21 | import android.app.Service | ||
22 | import android.content.Context | ||
23 | import android.content.Intent | ||
24 | import android.os.IBinder | ||
25 | import androidx.databinding.ObservableBoolean | ||
26 | import ch.qos.logback.classic.Level | ||
27 | import ch.qos.logback.classic.Logger | ||
28 | import org.apache.ftpserver.ConnectionConfigFactory | ||
29 | import org.apache.ftpserver.DataConnectionConfigurationFactory | ||
30 | import org.apache.ftpserver.FtpServer | ||
31 | import org.apache.ftpserver.FtpServerFactory | ||
32 | import org.apache.ftpserver.ftplet.* | ||
33 | import org.apache.ftpserver.listener.ListenerFactory | ||
34 | import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication | ||
35 | import org.apache.ftpserver.usermanager.impl.WritePermission | ||
36 | import org.pacien.tincapp.R | ||
37 | import org.pacien.tincapp.activities.configure.ConfigureActivity | ||
38 | import org.pacien.tincapp.context.App | ||
39 | import org.pacien.tincapp.context.AppNotificationManager | ||
40 | import org.pacien.tincapp.extensions.Java.defaultMessage | ||
41 | import org.pacien.tincapp.utils.PendingIntentUtils | ||
42 | import org.slf4j.LoggerFactory | ||
43 | import java.io.IOException | ||
44 | |||
45 | /** | ||
46 | * FTP server service allowing a remote and local user to access and modify configuration files in | ||
47 | * the application's context. | ||
48 | * | ||
49 | * @author pacien | ||
50 | */ | ||
51 | class ConfigurationAccessService : Service() { | ||
52 | companion object { | ||
53 | // Apache Mina FtpServer's INFO log level is actually VERBOSE. | ||
54 | // The object holds static references to those loggers so that they stay around. | ||
55 | @Suppress("unused") | ||
56 | private val MINA_FTP_LOGGER_OVERRIDER = MinaLoggerOverrider(Level.WARN) | ||
57 | |||
58 | private val context by lazy { App.getContext() } | ||
59 | private val store by lazy { context.getSharedPreferences("${this::class.java.`package`!!.name}.ftp", Context.MODE_PRIVATE)!! } | ||
60 | val runningState = ObservableBoolean(false) | ||
61 | |||
62 | fun getFtpHomeDir(): String = context.applicationInfo.dataDir!! | ||
63 | fun getFtpUsername() = storeGetOrInsertString("username") { "tincapp" } | ||
64 | fun getFtpPassword() = storeGetOrInsertString("password") { generateRandomString(8) } | ||
65 | fun getFtpPort() = storeGetOrInsertInt("port") { 65521 } // tinc port `concat` FTP port | ||
66 | fun getFtpPassiveDataPorts() = storeGetOrInsertString("passive-range") { "65522-65532" } | ||
67 | |||
68 | private fun storeGetOrInsertString(key: String, defaultGenerator: () -> String): String = synchronized(store) { | ||
69 | if (!store.contains(key)) store.edit().putString(key, defaultGenerator()).apply() | ||
70 | return store.getString(key, null)!! | ||
71 | } | ||
72 | |||
73 | private fun storeGetOrInsertInt(key: String, defaultGenerator: () -> Int): Int = synchronized(store) { | ||
74 | if (!store.contains(key)) store.edit().putInt(key, defaultGenerator()).apply() | ||
75 | return store.getInt(key, 0) | ||
76 | } | ||
77 | |||
78 | private fun generateRandomString(length: Int): String { | ||
79 | val alphabet = ('a'..'z') + ('A'..'Z') + ('0'..'9') | ||
80 | return List(length) { alphabet.random() }.joinToString("") | ||
81 | } | ||
82 | } | ||
83 | |||
84 | private val log by lazy { LoggerFactory.getLogger(this.javaClass)!! } | ||
85 | private val notificationManager by lazy { App.notificationManager } | ||
86 | private var sftpServer: FtpServer? = null | ||
87 | |||
88 | override fun onBind(intent: Intent): IBinder? = null // non-bindable service | ||
89 | |||
90 | override fun onDestroy() { | ||
91 | sftpServer?.stop() | ||
92 | sftpServer = null | ||
93 | runningState.set(false) | ||
94 | log.info("Stopped FTP server") | ||
95 | super.onDestroy() | ||
96 | } | ||
97 | |||
98 | override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { | ||
99 | val ftpUser = StaticFtpUser(getFtpUsername(), getFtpPassword(), getFtpHomeDir(), listOf(WritePermission())) | ||
100 | sftpServer = setupSingleUserServer(ftpUser, getFtpPort(), getFtpPassiveDataPorts()).also { | ||
101 | try { | ||
102 | it.start() | ||
103 | runningState.set(true) | ||
104 | log.info("Started FTP server on port {}", getFtpPort()) | ||
105 | pinInForeground() | ||
106 | } catch (e: IOException) { | ||
107 | log.error("Could not start FTP server", e) | ||
108 | App.alert(R.string.notification_error_title_unable_to_start_ftp_server, e.defaultMessage()) | ||
109 | } | ||
110 | } | ||
111 | |||
112 | return START_NOT_STICKY | ||
113 | } | ||
114 | |||
115 | /** | ||
116 | * Pins the service in the foreground so that it doesn't get stopped by the system when the | ||
117 | * application's activities are put in the background, which is the case when the user sets the | ||
118 | * focus on an FTP client app for example. | ||
119 | */ | ||
120 | private fun pinInForeground() { | ||
121 | startForeground( | ||
122 | AppNotificationManager.CONFIG_ACCESS_NOTIFICATION_ID, | ||
123 | notificationManager.newConfigurationAccessNotificationBuilder() | ||
124 | .setSmallIcon(R.drawable.ic_baseline_folder_open_primary_24dp) | ||
125 | .setContentTitle(resources.getString(R.string.notification_config_access_server_running_title)) | ||
126 | .setContentText(resources.getString(R.string.notification_config_access_server_running_message)) | ||
127 | .setContentIntent(Intent(this, ConfigureActivity::class.java).let { | ||
128 | PendingIntentUtils.getActivity(this, 0, it, 0) | ||
129 | }) | ||
130 | .build() | ||
131 | ) | ||
132 | } | ||
133 | |||
134 | private fun setupSingleUserServer(ftpUser: User, ftpPort: Int, ftpPassivePorts: String): FtpServer = | ||
135 | FtpServerFactory() | ||
136 | .apply { | ||
137 | addListener("default", ListenerFactory() | ||
138 | .apply { | ||
139 | connectionConfig = ConnectionConfigFactory() | ||
140 | .apply { maxThreads = 1 } // library has issues with multiple threads | ||
141 | .createConnectionConfig() | ||
142 | } | ||
143 | .apply { port = ftpPort } | ||
144 | .apply { | ||
145 | dataConnectionConfiguration = DataConnectionConfigurationFactory() | ||
146 | .apply { passivePorts = ftpPassivePorts } | ||
147 | .createDataConnectionConfiguration() | ||
148 | } | ||
149 | .createListener() | ||
150 | ) | ||
151 | } | ||
152 | .apply { userManager = StaticFtpUserManager(listOf(ftpUser)) } | ||
153 | .createServer() | ||
154 | |||
155 | private class StaticFtpUserManager(users: List<User>) : UserManager { | ||
156 | private val userMap: Map<String, User> = users.map { it.name to it }.toMap() | ||
157 | override fun getUserByName(username: String?): User? = userMap[username] | ||
158 | override fun getAllUserNames(): Array<String> = userMap.keys.toTypedArray() | ||
159 | override fun doesExist(username: String?): Boolean = username in userMap | ||
160 | override fun delete(username: String?) = throw UnsupportedOperationException() | ||
161 | override fun save(user: User?) = throw UnsupportedOperationException() | ||
162 | override fun getAdminName(): String = throw UnsupportedOperationException() | ||
163 | override fun isAdmin(username: String?): Boolean = throw UnsupportedOperationException() | ||
164 | override fun authenticate(authentication: Authentication?): User = when (authentication) { | ||
165 | is UsernamePasswordAuthentication -> getUserByName(authentication.username).let { | ||
166 | if (it != null && authentication.password == it.password) it | ||
167 | else throw AuthenticationFailedException() | ||
168 | } | ||
169 | else -> throw IllegalArgumentException() | ||
170 | } | ||
171 | } | ||
172 | |||
173 | private data class StaticFtpUser( | ||
174 | private val name: String, | ||
175 | private val password: String, | ||
176 | private val homeDirectory: String, | ||
177 | private val authorities: List<Authority> | ||
178 | ) : User { | ||
179 | override fun getName(): String = name | ||
180 | override fun getPassword(): String = password | ||
181 | override fun getAuthorities(): List<Authority> = authorities | ||
182 | override fun getAuthorities(clazz: Class<out Authority>): List<Authority> = authorities.filter(clazz::isInstance) | ||
183 | override fun getMaxIdleTime(): Int = 0 // unlimited | ||
184 | override fun getEnabled(): Boolean = true | ||
185 | override fun getHomeDirectory(): String = homeDirectory | ||
186 | override fun authorize(request: AuthorizationRequest?): AuthorizationRequest? = | ||
187 | authorities.filter { it.canAuthorize(request) }.fold(request) { req, auth -> auth.authorize(req) } | ||
188 | } | ||
189 | |||
190 | /** | ||
191 | * This registers package loggers filtering the output of the Mina FtpServer. | ||
192 | * The object holds static references to those loggers so that they stay around. | ||
193 | */ | ||
194 | private class MinaLoggerOverrider(logLevel: Level) { | ||