aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/org/pacien/tincapp/service/ConfigurationAccessService.kt
blob: b083a83a367a98046f77bbdce6dca03374ef3978 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
/*
 * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon
 * Copyright (C) 2017-2020 Pacien TRAN-GIRARD
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package org.pacien.tincapp.service

import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.os.IBinder
import androidx.databinding.ObservableBoolean
import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger
import org.apache.ftpserver.FtpServer
import org.apache.ftpserver.FtpServerFactory
import org.apache.ftpserver.ftplet.*
import org.apache.ftpserver.listener.ListenerFactory
import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication
import org.apache.ftpserver.usermanager.impl.WritePermission
import org.pacien.tincapp.R
import org.pacien.tincapp.activities.configure.ConfigureActivity
import org.pacien.tincapp.context.App
import org.pacien.tincapp.context.AppNotificationManager
import org.pacien.tincapp.extensions.Java.defaultMessage
import org.slf4j.LoggerFactory
import java.io.IOException

/**
 * FTP server service allowing a remote and local user to access and modify configuration files in
 * the application's context.
 *
 * @author pacien
 */
class ConfigurationAccessService : Service() {
  companion object {
    // Apache Mina FtpServer's INFO log level is actually VERBOSE.
    // The object holds static references to those loggers so that they stay around.
    @Suppress("unused")
    private val MINA_FTP_LOGGER_OVERRIDER = MinaLoggerOverrider(Level.WARN)

    const val FTP_PORT = 65521 // tinc port `concat` FTP port
    const val FTP_USERNAME = "tincapp"
    val FTP_HOME_DIR = App.getContext().applicationInfo.dataDir!!
    val FTP_PASSWORD = generateRandomString(8)

    val runningState = ObservableBoolean(false)

    private fun generateRandomString(length: Int): String {
      val alphabet = ('a'..'z') + ('A'..'Z') + ('0'..'9')
      return List(length) { alphabet.random() }.joinToString("")
    }
  }

  private val log by lazy { LoggerFactory.getLogger(this.javaClass)!! }
  private val notificationManager by lazy { App.notificationManager }
  private var sftpServer: FtpServer? = null

  override fun onBind(intent: Intent): IBinder? = null // non-bindable service

  override fun onDestroy() {
    sftpServer?.stop()
    sftpServer = null
    runningState.set(false)
    log.info("Stopped FTP server")
    super.onDestroy()
  }

  override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
    val ftpUser = StaticFtpUser(FTP_USERNAME, FTP_PASSWORD, FTP_HOME_DIR, listOf(WritePermission()))
    sftpServer = setupSingleUserServer(ftpUser).also {
      try {
        it.start()
        runningState.set(true)
        log.info("Started FTP server on port {}", FTP_PORT)
        pinInForeground()
      } catch (e: IOException) {
        log.error("Could not start FTP server", e)
        App.alert(R.string.notification_error_title_unable_to_start_ftp_server, e.defaultMessage())
      }
    }

    return START_NOT_STICKY
  }

  /**
   * Pins the service in the foreground so that it doesn't get stopped by the system when the
   * application's activities are put in the background, which is the case when the user sets the
   * focus on an FTP client app for example.
   */
  private fun pinInForeground() {
    startForeground(
      AppNotificationManager.CONFIG_ACCESS_NOTIFICATION_ID,
      notificationManager.newConfigurationAccessNotificationBuilder()
        .setSmallIcon(R.drawable.ic_baseline_folder_open_primary_24dp)
        .setContentTitle(resources.getString(R.string.notification_config_access_server_running_title))
        .setContentText(resources.getString(R.string.notification_config_access_server_running_message))
        .setContentIntent(Intent(this, ConfigureActivity::class.java).let {
          PendingIntent.getActivity(this, 0, it, 0)
        })
        .build()
    )
  }

  private fun setupSingleUserServer(ftpUser: User): FtpServer {
    return FtpServerFactory()
      .apply { addListener("default", ListenerFactory().apply { port = FTP_PORT }.createListener()) }
      .apply { userManager = StaticFtpUserManager(listOf(ftpUser)) }
      .createServer()
  }

  private class StaticFtpUserManager(users: List<User>) : UserManager {
    private val userMap: Map<String, User> = users.map { it.name to it }.toMap()
    override fun getUserByName(username: String?): User? = userMap[username]
    override fun getAllUserNames(): Array<String> = userMap.keys.toTypedArray()
    override fun doesExist(username: String?): Boolean = username in userMap
    override fun delete(username: String?) = throw UnsupportedOperationException()
    override fun save(user: User?) = throw UnsupportedOperationException()
    override fun getAdminName(): String = throw UnsupportedOperationException()
    override fun isAdmin(username: String?): Boolean = throw UnsupportedOperationException()
    override fun authenticate(authentication: Authentication?): User = when (authentication) {
      is UsernamePasswordAuthentication -> getUserByName(authentication.username).let {
        if (it != null && authentication.password == it.password) it
        else throw AuthenticationFailedException()
      }
      else -> throw IllegalArgumentException()
    }
  }

  private data class StaticFtpUser(
    private val name: String,
    private val password: String,
    private val homeDirectory: String,
    private val authorities: List<Authority>
  ) : User {
    override fun getName(): String = name
    override fun getPassword(): String = password
    override fun getAuthorities(): List<Authority> = authorities
    override fun getAuthorities(clazz: Class<out Authority>): List<Authority> = authorities.filter(clazz::isInstance)
    override fun getMaxIdleTime(): Int = 0 // unlimited
    override fun getEnabled(): Boolean = true
    override fun getHomeDirectory(): String = homeDirectory
    override fun authorize(request: AuthorizationRequest?): AuthorizationRequest? =
      authorities.filter { it.canAuthorize(request) }.fold(request) { req, auth -> auth.authorize(req) }
  }

  /**
   * This registers package loggers filtering the output of the Mina FtpServer.
   * The object holds static references to those loggers so that they stay around.
   */
  private class MinaLoggerOverrider(logLevel: Level) {
    @Suppress("unused")
    private val ftpServerLogger = forceLogLevel("org.apache.ftpserver", logLevel)

    @Suppress("unused")
    private val minaLogger = forceLogLevel("org.apache.mina", logLevel)

    private fun forceLogLevel(pkgName: String, logLevel: Level) =
      (LoggerFactory.getLogger(pkgName) as Logger).apply { level = logLevel }
  }
}