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 | 174 |
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 | |||
19 | package org.pacien.tincapp.service | ||
20 | |||
21 | import android.app.PendingIntent | ||
22 | import android.app.Service | ||
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.FtpServer | ||
29 | import org.apache.ftpserver.FtpServerFactory | ||
30 | import org.apache.ftpserver.ftplet.* | ||
31 | import org.apache.ftpserver.listener.ListenerFactory | ||
32 | import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication | ||
33 | import org.apache.ftpserver.usermanager.impl.WritePermission | ||
34 | import org.pacien.tincapp.R | ||
35 | import org.pacien.tincapp.activities.configure.ConfigureActivity | ||
36 | import org.pacien.tincapp.context.App | ||
37 | import org.pacien.tincapp.context.AppNotificationManager | ||
38 | import org.pacien.tincapp.extensions.Java.defaultMessage | ||
39 | import org.slf4j.LoggerFactory | ||
40 | import 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 | */ | ||
48 | class 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 | } | ||