mirror of
git://f0xx.org/android_cast
synced 2026-07-29 04:18:09 +03:00
some rssh stuff
This commit is contained in:
@@ -119,6 +119,7 @@ android {
|
|||||||
excludes += '/META-INF/LICENSE.txt'
|
excludes += '/META-INF/LICENSE.txt'
|
||||||
excludes += '/META-INF/NOTICE'
|
excludes += '/META-INF/NOTICE'
|
||||||
excludes += '/META-INF/NOTICE.txt'
|
excludes += '/META-INF/NOTICE.txt'
|
||||||
|
excludes += '/META-INF/versions/**'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +147,8 @@ dependencies {
|
|||||||
implementation 'com.github.mwiede:jsch:0.2.21'
|
implementation 'com.github.mwiede:jsch:0.2.21'
|
||||||
|
|
||||||
implementation 'org.apache.sshd:sshd-core:2.14.0'
|
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 'org.slf4j:slf4j-android:1.7.36'
|
||||||
|
|
||||||
implementation project(':tunnel')
|
implementation project(':tunnel')
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
<li><b>Google Play In-App Review</b> — Google Play SDK terms</li>
|
<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>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>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>
|
</ul>
|
||||||
|
|
||||||
<h2>JUnit 4</h2>
|
<h2>JUnit 4</h2>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public final class ReverseSshTunnelBridge {
|
|||||||
Log.w(TAG, "incomplete RSSH credentials");
|
Log.w(TAG, "incomplete RSSH credentials");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!RsshLocalSshServer.start(user, pass, localPort)) {
|
if (!RsshLocalSshServer.start(context, user, pass, localPort)) {
|
||||||
Log.w(TAG, "local SSH on :" + localPort + " failed");
|
Log.w(TAG, "local SSH on :" + localPort + " failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
package com.foxx.androidcast.remoteaccess;
|
package com.foxx.androidcast.remoteaccess;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.apache.sshd.server.SshServer;
|
import org.apache.sshd.server.SshServer;
|
||||||
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
|
import org.apache.sshd.server.auth.password.PasswordAuthenticator;
|
||||||
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
|
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
|
||||||
import org.apache.sshd.server.shell.ProcessShellFactory;
|
import org.apache.sshd.server.shell.ProcessShellFactory;
|
||||||
|
import org.apache.sshd.sftp.server.SftpSubsystemFactory;
|
||||||
|
|
||||||
import java.nio.file.Files;
|
import java.util.Collections;
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
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).
|
* Operator reaches this via bastion forwarded port (same username/password).
|
||||||
*/
|
*/
|
||||||
public final class RsshLocalSshServer {
|
public final class RsshLocalSshServer {
|
||||||
@@ -24,9 +25,10 @@ public final class RsshLocalSshServer {
|
|||||||
|
|
||||||
private 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();
|
stop();
|
||||||
if (username == null || username.isEmpty() || password == null || password.isEmpty()) {
|
if (context == null || username == null || username.isEmpty()
|
||||||
|
|| password == null || password.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (port <= 0 || port > 65535) {
|
if (port <= 0 || port > 65535) {
|
||||||
@@ -36,8 +38,8 @@ public final class RsshLocalSshServer {
|
|||||||
final String pass = password;
|
final String pass = password;
|
||||||
final int listenPort = port;
|
final int listenPort = port;
|
||||||
try {
|
try {
|
||||||
Path hostKey = Files.createTempFile("rssh_host", ".key");
|
RsshSecuritySetup.ensureReady(context);
|
||||||
hostKey.toFile().deleteOnExit();
|
java.nio.file.Path hostKey = context.getCacheDir().toPath().resolve("rssh_host.key");
|
||||||
SshServer sshd = SshServer.setUpDefaultServer();
|
SshServer sshd = SshServer.setUpDefaultServer();
|
||||||
sshd.setHost("127.0.0.1");
|
sshd.setHost("127.0.0.1");
|
||||||
sshd.setPort(listenPort);
|
sshd.setPort(listenPort);
|
||||||
@@ -45,14 +47,15 @@ public final class RsshLocalSshServer {
|
|||||||
sshd.setPasswordAuthenticator((PasswordAuthenticator) (u, p, session) ->
|
sshd.setPasswordAuthenticator((PasswordAuthenticator) (u, p, session) ->
|
||||||
user.equals(u) && pass.equals(p));
|
user.equals(u) && pass.equals(p));
|
||||||
sshd.setShellFactory(new ProcessShellFactory("/system/bin/sh", "-"));
|
sshd.setShellFactory(new ProcessShellFactory("/system/bin/sh", "-"));
|
||||||
|
sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
|
||||||
sshd.start();
|
sshd.start();
|
||||||
server = sshd;
|
server = sshd;
|
||||||
activeUser = user;
|
activeUser = user;
|
||||||
running.set(true);
|
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;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Throwable e) {
|
||||||
Log.w(TAG, "local SSH start failed: " + e.getMessage());
|
Log.e(TAG, "local SSH start failed: " + e.getMessage(), e);
|
||||||
stop();
|
stop();
|
||||||
return false;
|
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 rowKey = 'ra-' + i;
|
||||||
const briefId = 'ra-brief-' + rowKey;
|
const briefId = 'ra-brief-' + rowKey;
|
||||||
const wl = Number(d.whitelisted) === 1;
|
const wl = Number(d.whitelisted) === 1;
|
||||||
const canOpen = canOperate() && wl && (d.opt_in_mode === 'wireguard' || d.opt_in_mode === 'rssh');
|
const optIn = d.opt_in_mode || 'none';
|
||||||
const openDisabled = canOpen ? '' : ' disabled title="Whitelist device and wait for WireGuard/RSSH opt-in poll"';
|
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 wlDisabled = canAdmin() ? '' : ' disabled';
|
||||||
const isOpen = lastExpanded.has(String(d.device_id || ''));
|
const isOpen = lastExpanded.has(String(d.device_id || ''));
|
||||||
const rowClass =
|
const rowClass =
|
||||||
@@ -545,6 +553,8 @@
|
|||||||
openDisabled +
|
openDisabled +
|
||||||
' data-open-session="' +
|
' data-open-session="' +
|
||||||
esc(d.device_id) +
|
esc(d.device_id) +
|
||||||
|
'" title="' +
|
||||||
|
esc(openHint) +
|
||||||
'">Open session</button> ' +
|
'">Open session</button> ' +
|
||||||
'<button type="button" class="btn btn-sm"' +
|
'<button type="button" class="btn btn-sm"' +
|
||||||
wlDisabled +
|
wlDisabled +
|
||||||
@@ -691,6 +701,22 @@
|
|||||||
const openId = t.getAttribute('data-open-session');
|
const openId = t.getAttribute('data-open-session');
|
||||||
if (openId) {
|
if (openId) {
|
||||||
if (!canOperate()) return;
|
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 {
|
try {
|
||||||
setStatus('Opening session for ' + openId + '…');
|
setStatus('Opening session for ' + openId + '…');
|
||||||
await fetchJson(apiUrl('open_session'), {
|
await fetchJson(apiUrl('open_session'), {
|
||||||
|
|||||||
Reference in New Issue
Block a user