mirror of
git://f0xx.org/android_cast
synced 2026-07-29 02:18:40 +03:00
some rssh stuff
This commit is contained in:
@@ -119,6 +119,7 @@ android {
|
||||
excludes += '/META-INF/LICENSE.txt'
|
||||
excludes += '/META-INF/NOTICE'
|
||||
excludes += '/META-INF/NOTICE.txt'
|
||||
excludes += '/META-INF/versions/**'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +147,8 @@ dependencies {
|
||||
implementation 'com.github.mwiede:jsch:0.2.21'
|
||||
|
||||
implementation 'org.apache.sshd:sshd-core:2.14.0'
|
||||
implementation 'org.apache.sshd:sshd-sftp:2.14.0'
|
||||
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
|
||||
implementation 'org.slf4j:slf4j-android:1.7.36'
|
||||
|
||||
implementation project(':tunnel')
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
<li><b>Google Play In-App Review</b> — Google Play SDK terms</li>
|
||||
<li><b>Eclipse Paho MQTT client</b> — Eclipse Public License 1.0 and Eclipse Distribution License 1.0 (dual-licensed)</li>
|
||||
<li><b>JSch (mwiede fork)</b> — BSD 3-Clause License (reverse SSH remote access)</li>
|
||||
<li><b>Apache MINA SSHD</b> — Apache License 2.0 (embedded local SSH on device for RSSH alpha)</li>
|
||||
<li><b>Apache MINA SSHD</b> — Apache License 2.0 (embedded local SSH/SFTP for RSSH alpha)</li>
|
||||
<li><b>Bouncy Castle</b> — MIT-style license (SSHD crypto on Android)</li>
|
||||
</ul>
|
||||
|
||||
<h2>JUnit 4</h2>
|
||||
|
||||
@@ -42,7 +42,7 @@ public final class ReverseSshTunnelBridge {
|
||||
Log.w(TAG, "incomplete RSSH credentials");
|
||||
return false;
|
||||
}
|
||||
if (!RsshLocalSshServer.start(user, pass, localPort)) {
|
||||
if (!RsshLocalSshServer.start(context, user, pass, localPort)) {
|
||||
Log.w(TAG, "local SSH on :" + localPort + " failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
package com.foxx.androidcast.remoteaccess;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.sshd.server.SshServer;
|
||||
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
|
||||
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
|
||||
import org.apache.sshd.server.shell.ProcessShellFactory;
|
||||
import org.apache.sshd.sftp.server.SftpSubsystemFactory;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Embedded SSH on {@code 127.0.0.1:8022} for RSSH reverse forward target.
|
||||
* Embedded SSH/SFTP on {@code 127.0.0.1:8022} for RSSH reverse forward target.
|
||||
* Operator reaches this via bastion forwarded port (same username/password).
|
||||
*/
|
||||
public final class RsshLocalSshServer {
|
||||
@@ -24,9 +25,10 @@ public final class RsshLocalSshServer {
|
||||
|
||||
private RsshLocalSshServer() {}
|
||||
|
||||
public static boolean start(String username, String password, int port) {
|
||||
public static boolean start(Context context, String username, String password, int port) {
|
||||
stop();
|
||||
if (username == null || username.isEmpty() || password == null || password.isEmpty()) {
|
||||
if (context == null || username == null || username.isEmpty()
|
||||
|| password == null || password.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (port <= 0 || port > 65535) {
|
||||
@@ -36,8 +38,8 @@ public final class RsshLocalSshServer {
|
||||
final String pass = password;
|
||||
final int listenPort = port;
|
||||
try {
|
||||
Path hostKey = Files.createTempFile("rssh_host", ".key");
|
||||
hostKey.toFile().deleteOnExit();
|
||||
RsshSecuritySetup.ensureReady(context);
|
||||
java.nio.file.Path hostKey = context.getCacheDir().toPath().resolve("rssh_host.key");
|
||||
SshServer sshd = SshServer.setUpDefaultServer();
|
||||
sshd.setHost("127.0.0.1");
|
||||
sshd.setPort(listenPort);
|
||||
@@ -45,14 +47,15 @@ public final class RsshLocalSshServer {
|
||||
sshd.setPasswordAuthenticator((PasswordAuthenticator) (u, p, session) ->
|
||||
user.equals(u) && pass.equals(p));
|
||||
sshd.setShellFactory(new ProcessShellFactory("/system/bin/sh", "-"));
|
||||
sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
|
||||
sshd.start();
|
||||
server = sshd;
|
||||
activeUser = user;
|
||||
running.set(true);
|
||||
Log.i(TAG, "local SSH listening 127.0.0.1:" + listenPort + " user=" + user);
|
||||
Log.i(TAG, "local SSH/SFTP listening 127.0.0.1:" + listenPort + " user=" + user);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "local SSH start failed: " + e.getMessage());
|
||||
} catch (Throwable e) {
|
||||
Log.e(TAG, "local SSH start failed: " + e.getMessage(), e);
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.foxx.androidcast.remoteaccess;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.sshd.common.util.OsUtils;
|
||||
import org.apache.sshd.common.util.io.PathUtils;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.security.Security;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/** One-time MINA SSHD + BouncyCastle setup for Android (see mina-sshd docs/android.md). */
|
||||
final class RsshSecuritySetup {
|
||||
private static final String TAG = "RsshSecurity";
|
||||
private static final AtomicBoolean READY = new AtomicBoolean(false);
|
||||
|
||||
private RsshSecuritySetup() {}
|
||||
|
||||
static void ensureReady(Context context) {
|
||||
if (!READY.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
Context app = context.getApplicationContext();
|
||||
try {
|
||||
try {
|
||||
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
Path home = app.getFilesDir().toPath();
|
||||
System.setProperty("user.home", home.toString());
|
||||
PathUtils.setUserHomeFolderResolver(() -> home);
|
||||
OsUtils.setCurrentUser(app.getPackageName());
|
||||
Log.i(TAG, "SSHD security providers ready; user.home=" + home);
|
||||
} catch (Exception e) {
|
||||
READY.set(false);
|
||||
Log.e(TAG, "SSHD security setup failed: " + e.getMessage(), e);
|
||||
throw new IllegalStateException("RSSH security setup failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -518,8 +518,16 @@
|
||||
const rowKey = 'ra-' + i;
|
||||
const briefId = 'ra-brief-' + rowKey;
|
||||
const wl = Number(d.whitelisted) === 1;
|
||||
const canOpen = canOperate() && wl && (d.opt_in_mode === 'wireguard' || d.opt_in_mode === 'rssh');
|
||||
const openDisabled = canOpen ? '' : ' disabled title="Whitelist device and wait for WireGuard/RSSH opt-in poll"';
|
||||
const optIn = d.opt_in_mode || 'none';
|
||||
const openReady = wl && (optIn === 'wireguard' || optIn === 'rssh');
|
||||
const openHint = !canOperate()
|
||||
? 'Need remote_access_operate permission'
|
||||
: !wl
|
||||
? 'Whitelist device first'
|
||||
: openReady
|
||||
? 'Open session — device connects on next poll (≤7 min)'
|
||||
: 'Phone must poll with RSSH/WG enabled (dev settings on device; wait ≤7 min)';
|
||||
const openDisabled = canOperate() ? '' : ' disabled';
|
||||
const wlDisabled = canAdmin() ? '' : ' disabled';
|
||||
const isOpen = lastExpanded.has(String(d.device_id || ''));
|
||||
const rowClass =
|
||||
@@ -545,6 +553,8 @@
|
||||
openDisabled +
|
||||
' data-open-session="' +
|
||||
esc(d.device_id) +
|
||||
'" title="' +
|
||||
esc(openHint) +
|
||||
'">Open session</button> ' +
|
||||
'<button type="button" class="btn btn-sm"' +
|
||||
wlDisabled +
|
||||
@@ -691,6 +701,22 @@
|
||||
const openId = t.getAttribute('data-open-session');
|
||||
if (openId) {
|
||||
if (!canOperate()) return;
|
||||
const dev = (lastDevices || []).find((d) => String(d.device_id) === openId);
|
||||
const wl = dev && Number(dev.whitelisted) === 1;
|
||||
const optIn = dev ? dev.opt_in_mode || 'none' : 'none';
|
||||
if (!wl) {
|
||||
setStatus('Whitelist device ' + openId + ' first', true);
|
||||
return;
|
||||
}
|
||||
if (optIn !== 'wireguard' && optIn !== 'rssh') {
|
||||
setStatus(
|
||||
'Device opt-in is "' +
|
||||
optIn +
|
||||
'". On phone: dev settings → Remote access → RSSH, then wait for poll (≤7 min).',
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setStatus('Opening session for ' + openId + '…');
|
||||
await fetchJson(apiUrl('open_session'), {
|
||||
|
||||
Reference in New Issue
Block a user