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.kt204
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 1708243..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-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.Context
24import android.content.Intent
25import android.os.IBinder
26import androidx.databinding.ObservableBoolean
27import ch.qos.logback.classic.Level
28import ch.qos.logback.classic.Logger
29import org.apache.ftpserver.ConnectionConfigFactory
30import org.apache.ftpserver.DataConnectionConfigurationFactory
31import org.apache.ftpserver.FtpServer
32import org.apache.ftpserver.FtpServerFactory
33import org.apache.ftpserver.ftplet.*
34import org.apache.ftpserver.listener.ListenerFactory
35import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication
36import org.apache.ftpserver.usermanager.impl.WritePermission
37import org.pacien.tincapp.R
38import org.pacien.tincapp.activities.configure.ConfigureActivity
39import org.pacien.tincapp.context.App
40import org.pacien.tincapp.context.AppNotificationManager
41import org.pacien.tincapp.extensions.Java.defaultMessage
42import org.slf4j.LoggerFactory
43import 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 */
51class 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 PendingIntent.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) {