File size: 6,305 Bytes
5bbfb59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
175
/*
 * Copyright (C) 2014-2020 Arpit Khurana <arpitkh96@gmail.com>, Vishal Nehra <vishalmeham2@gmail.com>,
 * Emmanuel Messulam<emmanuelbendavid@gmail.com>, Raymond Lai <airwave209gt at gmail.com> and Contributors.
 *
 * This file is part of Amaze File Manager.
 *
 * Amaze File Manager 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 <http://www.gnu.org/licenses/>.
 */

package com.amaze.filemanager.filesystem.ssh

import android.os.Build.VERSION_CODES
import android.os.Build.VERSION_CODES.LOLLIPOP
import android.os.Build.VERSION_CODES.P
import android.os.Environment
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.amaze.filemanager.application.AppConfig
import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX
import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.getConnection
import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.shutdown
import com.amaze.filemanager.filesystem.ssh.test.TestKeyProvider
import com.amaze.filemanager.filesystem.ssh.test.TestUtils
import com.amaze.filemanager.shadows.ShadowMultiDex
import com.amaze.filemanager.test.ShadowPasswordUtil
import com.amaze.filemanager.utils.PasswordUtil
import io.reactivex.android.plugins.RxAndroidPlugins
import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.schedulers.Schedulers
import org.apache.sshd.common.NamedFactory
import org.apache.sshd.common.config.keys.KeyUtils
import org.apache.sshd.common.file.FileSystemFactory
import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory
import org.apache.sshd.server.Command
import org.apache.sshd.server.SshServer
import org.apache.sshd.server.auth.password.PasswordAuthenticator
import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator
import org.apache.sshd.server.scp.ScpCommandFactory
import org.apache.sshd.server.session.ServerSession
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import java.io.IOException
import java.net.BindException
import java.net.URLEncoder.encode
import java.nio.file.Paths
import kotlin.text.Charsets.UTF_8

/**
 * Base class for all SSH server related tests.
 */
@RunWith(AndroidJUnit4::class)
@Config(
    shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class],
    sdk = [LOLLIPOP, P, VERSION_CODES.R],
)
abstract class AbstractSftpServerTest {
    protected var encryptedPassword: String? =
        PasswordUtil.encryptPassword(AppConfig.getInstance(), PASSWORD)?.replace("\n", "")
    protected var serverPort = 0
    private lateinit var server: SshServer

    /**
     * Setup SSH server with device directory as root.
     */
    @Before
    @Throws(IOException::class)
    open fun setUp() {
        serverPort =
            createSshServer(
                VirtualFileSystemFactory(
                    Paths.get(Environment.getExternalStorageDirectory().absolutePath),
                ),
                64000,
            )
        prepareSshConnection()
        TestUtils.saveSshConnectionSettings(
            hostKeyPair = hostKeyProvider.keyPair,
            validUsername = encode(USERNAME, UTF_8.name()),
            validPassword = encryptedPassword,
            privateKey = null,
            port = 64000,
        )
    }

    /**
     * Shutdown SSH server after test.
     */
    @After
    @Throws(IOException::class)
    fun tearDown() {
        shutdown()
        if (server.isOpen) {
            server.stop(true)
        }
    }

    protected fun prepareSshConnection() {
        val hostFingerprint = KeyUtils.getFingerPrint(hostKeyProvider.keyPair.public)
        getConnection(
            SSH_URI_PREFIX,
            HOST,
            serverPort,
            hostFingerprint,
            USERNAME,
            PasswordUtil.encryptPassword(AppConfig.getInstance(), PASSWORD)?.replace("\n", ""),
            null,
        )
    }

    @Throws(IOException::class)
    protected fun createSshServer(
        fileSystemFactory: FileSystemFactory,
        startPort: Int,
    ): Int {
        server = SshServer.setUpDefaultServer()
        server.fileSystemFactory = fileSystemFactory
        server.publickeyAuthenticator = AcceptAllPublickeyAuthenticator.INSTANCE
        server.host = HOST
        server.keyPairProvider = hostKeyProvider
        server.commandFactory = ScpCommandFactory()
        server.subsystemFactories = listOf<NamedFactory<Command>>(SftpSubsystemFactory())
        server.passwordAuthenticator =
            PasswordAuthenticator { username: String, password: String, _: ServerSession? ->
                username == USERNAME && password == PASSWORD
            }
        return try {
            server.port = startPort
            server.start()
            startPort
        } catch (ifPortIsUnavailable: BindException) {
            createSshServer(fileSystemFactory, startPort + 1)
        }
    }

    companion object {
        @JvmStatic
        protected val HOST = "127.0.0.1"

        @JvmStatic
        protected val USERNAME = "testuser"

        @JvmStatic
        protected val PASSWORD = "testpassword"

        protected lateinit var hostKeyProvider: TestKeyProvider

        /**
         * Prepare SSH host key provider and RxJava scheduler handlers.
         */
        @BeforeClass
        @JvmStatic
        fun bootstrap() {
            hostKeyProvider = TestKeyProvider()
            RxJavaPlugins.reset()
            RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
            RxAndroidPlugins.reset()
            RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
        }
    }
}