mirror of
git://f0xx.org/android_cast
synced 2026-07-29 04:18:09 +03:00
sync
This commit is contained in:
@@ -21,14 +21,18 @@ import com.foxx.androidcast.DeviceInfo;
|
|||||||
import com.foxx.androidcast.R;
|
import com.foxx.androidcast.R;
|
||||||
import com.foxx.androidcast.vpn.VpnServiceClient;
|
import com.foxx.androidcast.vpn.VpnServiceClient;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import java.net.Inet4Address;
|
||||||
import org.json.JSONObject;
|
import java.net.InetAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
/** Jittered heartbeat type:ra polling while remote access mode is active. */
|
/** Jittered heartbeat type:ra polling while remote access mode is active. */
|
||||||
public final class RemoteAccessService extends Service {
|
public final class RemoteAccessService extends Service {
|
||||||
private static final String TAG = "RemoteAccessSvc";
|
private static final String TAG = "RemoteAccessSvc";
|
||||||
@@ -369,9 +373,34 @@ public final class RemoteAccessService extends Service {
|
|||||||
abis.put(abi);
|
abis.put(abi);
|
||||||
}
|
}
|
||||||
device.put("abis", abis);
|
device.put("abis", abis);
|
||||||
|
String lanIp = localLanIpV4();
|
||||||
|
if (!lanIp.isEmpty()) {
|
||||||
|
device.put("lan_ip", lanIp);
|
||||||
|
}
|
||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String localLanIpV4() {
|
||||||
|
try {
|
||||||
|
for (NetworkInterface intf : Collections.list(NetworkInterface.getNetworkInterfaces())) {
|
||||||
|
if (intf == null || !intf.isUp() || intf.isLoopback()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (InetAddress addr : Collections.list(intf.getInetAddresses())) {
|
||||||
|
if (addr == null || addr.isLoopbackAddress() || !(addr instanceof Inet4Address)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String host = addr.getHostAddress();
|
||||||
|
if (host != null && !host.isEmpty()) {
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
public IBinder onBind(Intent intent) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -393,6 +393,10 @@
|
|||||||
if (d.sdk_int != null) lines.push('Android API: ' + d.sdk_int);
|
if (d.sdk_int != null) lines.push('Android API: ' + d.sdk_int);
|
||||||
if (d.os_release) lines.push('Android version: ' + d.os_release);
|
if (d.os_release) lines.push('Android version: ' + d.os_release);
|
||||||
if (d.abis && d.abis.length) lines.push('ABIs: ' + formatAbis(d.abis));
|
if (d.abis && d.abis.length) lines.push('ABIs: ' + formatAbis(d.abis));
|
||||||
|
if (d.wan_ip) lines.push('WAN IP (last poll): ' + d.wan_ip);
|
||||||
|
if (d.lan_ip) lines.push('LAN IP: ' + d.lan_ip);
|
||||||
|
if (d.vpn_ip) lines.push('VPN IP: ' + d.vpn_ip);
|
||||||
|
if (d.vpn_public_key) lines.push('VPN public key: ' + d.vpn_public_key);
|
||||||
lines.push('App version: ' + (d.app_version || '—'));
|
lines.push('App version: ' + (d.app_version || '—'));
|
||||||
lines.push('Opt-in mode: ' + (d.opt_in_mode || 'none'));
|
lines.push('Opt-in mode: ' + (d.opt_in_mode || 'none'));
|
||||||
lines.push('Last seen: ' + (d.last_seen_at || '—'));
|
lines.push('Last seen: ' + (d.last_seen_at || '—'));
|
||||||
|
|||||||
@@ -25,6 +25,22 @@ final class RemoteAccessRepository {
|
|||||||
self::ensureSessionColumns($pdo);
|
self::ensureSessionColumns($pdo);
|
||||||
self::ensureRsshColumns($pdo);
|
self::ensureRsshColumns($pdo);
|
||||||
self::ensureDeviceMetaColumn($pdo);
|
self::ensureDeviceMetaColumn($pdo);
|
||||||
|
self::ensureLastClientIpColumn($pdo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function ensureLastClientIpColumn(PDO $pdo): void {
|
||||||
|
if (!Database::tableExists($pdo, 'remote_access_devices')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$col = Database::isMysql() ? 'VARCHAR(64) NULL' : 'TEXT NULL';
|
||||||
|
try {
|
||||||
|
$pdo->exec('ALTER TABLE remote_access_devices ADD COLUMN last_client_ip ' . $col);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$msg = strtolower($e->getMessage());
|
||||||
|
if (!str_contains($msg, 'duplicate') && !str_contains($msg, 'already exists')) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function ensureDeviceMetaColumn(PDO $pdo): void {
|
private static function ensureDeviceMetaColumn(PDO $pdo): void {
|
||||||
@@ -275,20 +291,20 @@ final class RemoteAccessRepository {
|
|||||||
if ($metaJson !== null) {
|
if ($metaJson !== null) {
|
||||||
$pdo->prepare(
|
$pdo->prepare(
|
||||||
'UPDATE remote_access_devices SET opt_in_mode = ?, last_random = ?, app_version = ?,
|
'UPDATE remote_access_devices SET opt_in_mode = ?, last_random = ?, app_version = ?,
|
||||||
device_meta_json = ?, last_seen_at = ?, updated_at = ? WHERE device_id = ?'
|
device_meta_json = ?, last_client_ip = ?, last_seen_at = ?, updated_at = ? WHERE device_id = ?'
|
||||||
)->execute([$tunnelMode, $random, $appVersion, $metaJson, $now, $now, $deviceId]);
|
)->execute([$tunnelMode, $random, $appVersion, $metaJson, $clientIp, $now, $now, $deviceId]);
|
||||||
} else {
|
} else {
|
||||||
$pdo->prepare(
|
$pdo->prepare(
|
||||||
'UPDATE remote_access_devices SET opt_in_mode = ?, last_random = ?, app_version = ?,
|
'UPDATE remote_access_devices SET opt_in_mode = ?, last_random = ?, app_version = ?,
|
||||||
last_seen_at = ?, updated_at = ? WHERE device_id = ?'
|
last_client_ip = ?, last_seen_at = ?, updated_at = ? WHERE device_id = ?'
|
||||||
)->execute([$tunnelMode, $random, $appVersion, $now, $now, $deviceId]);
|
)->execute([$tunnelMode, $random, $appVersion, $clientIp, $now, $now, $deviceId]);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$pdo->prepare(
|
$pdo->prepare(
|
||||||
'INSERT INTO remote_access_devices (device_id, company_id, whitelisted, opt_in_mode, last_random,
|
'INSERT INTO remote_access_devices (device_id, company_id, whitelisted, opt_in_mode, last_random,
|
||||||
app_version, device_meta_json, last_seen_at, created_at, updated_at)
|
app_version, device_meta_json, last_client_ip, last_seen_at, created_at, updated_at)
|
||||||
VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?, ?)'
|
VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||||
)->execute([
|
)->execute([
|
||||||
$deviceId,
|
$deviceId,
|
||||||
Rbac::defaultCompanyId(),
|
Rbac::defaultCompanyId(),
|
||||||
@@ -296,6 +312,7 @@ final class RemoteAccessRepository {
|
|||||||
$random,
|
$random,
|
||||||
$appVersion,
|
$appVersion,
|
||||||
$metaJson,
|
$metaJson,
|
||||||
|
$clientIp !== '' ? $clientIp : null,
|
||||||
$now,
|
$now,
|
||||||
$now,
|
$now,
|
||||||
$now,
|
$now,
|
||||||
@@ -308,7 +325,7 @@ final class RemoteAccessRepository {
|
|||||||
if ($deviceMeta === []) {
|
if ($deviceMeta === []) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$allowed = ['name', 'manufacturer', 'brand', 'model', 'device', 'product', 'sdk_int', 'release', 'abis'];
|
$allowed = ['name', 'manufacturer', 'brand', 'model', 'device', 'product', 'sdk_int', 'release', 'abis', 'lan_ip', 'wan_ip'];
|
||||||
$clean = [];
|
$clean = [];
|
||||||
foreach ($allowed as $key) {
|
foreach ($allowed as $key) {
|
||||||
if (!array_key_exists($key, $deviceMeta)) {
|
if (!array_key_exists($key, $deviceMeta)) {
|
||||||
@@ -828,6 +845,11 @@ final class RemoteAccessRepository {
|
|||||||
$row['os_release'] = $ctx['os_release'] ?? '';
|
$row['os_release'] = $ctx['os_release'] ?? '';
|
||||||
$row['sdk_int'] = $ctx['sdk_int'] ?? null;
|
$row['sdk_int'] = $ctx['sdk_int'] ?? null;
|
||||||
$row['abis'] = $ctx['abis'] ?? [];
|
$row['abis'] = $ctx['abis'] ?? [];
|
||||||
|
$row['wan_ip'] = self::resolveDeviceWanIp($row, $storedMeta);
|
||||||
|
$row['lan_ip'] = self::resolveDeviceLanIp($storedMeta);
|
||||||
|
$net = self::lookupDeviceSessionNetwork($deviceId);
|
||||||
|
$row['vpn_ip'] = $net['vpn_ip'] ?? '';
|
||||||
|
$row['vpn_public_key'] = $net['vpn_public_key'] ?? '';
|
||||||
$row['issue_count'] = $ctx['issue_count'] ?? 0;
|
$row['issue_count'] = $ctx['issue_count'] ?? 0;
|
||||||
$row['graph_session_count'] = $ctx['graph_session_count'] ?? 0;
|
$row['graph_session_count'] = $ctx['graph_session_count'] ?? 0;
|
||||||
$row['latest_issue_id'] = $ctx['latest_issue_id'] ?? null;
|
$row['latest_issue_id'] = $ctx['latest_issue_id'] ?? null;
|
||||||
@@ -889,6 +911,79 @@ final class RemoteAccessRepository {
|
|||||||
return (time() - $t) <= $maxAgeSec;
|
return (time() - $t) <= $maxAgeSec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $storedMeta */
|
||||||
|
private static function resolveDeviceWanIp(array $row, array $storedMeta): string {
|
||||||
|
$fromDevice = trim((string) ($storedMeta['wan_ip'] ?? ''));
|
||||||
|
if ($fromDevice !== '') {
|
||||||
|
return self::normalizeClientIp($fromDevice);
|
||||||
|
}
|
||||||
|
$fromRow = trim((string) ($row['last_client_ip'] ?? ''));
|
||||||
|
if ($fromRow !== '') {
|
||||||
|
return self::normalizeClientIp($fromRow);
|
||||||
|
}
|
||||||
|
$deviceId = (string) ($row['device_id'] ?? '');
|
||||||
|
if ($deviceId === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$st = Database::pdo()->prepare(
|
||||||
|
"SELECT client_ip FROM remote_access_events
|
||||||
|
WHERE device_id = ? AND client_ip IS NOT NULL AND client_ip != ''
|
||||||
|
ORDER BY id DESC LIMIT 1"
|
||||||
|
);
|
||||||
|
$st->execute([$deviceId]);
|
||||||
|
$ip = trim((string) ($st->fetchColumn() ?: ''));
|
||||||
|
|
||||||
|
return self::normalizeClientIp($ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $storedMeta */
|
||||||
|
private static function resolveDeviceLanIp(array $storedMeta): string {
|
||||||
|
return trim((string) ($storedMeta['lan_ip'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizeClientIp(string $raw): string {
|
||||||
|
$raw = trim($raw);
|
||||||
|
if ($raw === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (str_contains($raw, ',')) {
|
||||||
|
$raw = trim(explode(',', $raw)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{vpn_ip: string, vpn_public_key: string} */
|
||||||
|
private static function lookupDeviceSessionNetwork(string $deviceId): array {
|
||||||
|
$empty = ['vpn_ip' => '', 'vpn_public_key' => ''];
|
||||||
|
if ($deviceId === '') {
|
||||||
|
return $empty;
|
||||||
|
}
|
||||||
|
$st = Database::pdo()->prepare(
|
||||||
|
"SELECT tunnel, wg_client_address, wg_client_public_key
|
||||||
|
FROM remote_access_sessions
|
||||||
|
WHERE device_id = ? AND status IN ('pending', 'active')
|
||||||
|
ORDER BY id DESC LIMIT 1"
|
||||||
|
);
|
||||||
|
$st->execute([$deviceId]);
|
||||||
|
$row = $st->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!is_array($row)) {
|
||||||
|
return $empty;
|
||||||
|
}
|
||||||
|
$tunnel = (string) ($row['tunnel'] ?? '');
|
||||||
|
if ($tunnel !== '' && $tunnel !== 'wireguard') {
|
||||||
|
return $empty;
|
||||||
|
}
|
||||||
|
$addr = trim((string) ($row['wg_client_address'] ?? ''));
|
||||||
|
$vpnIp = $addr !== '' ? preg_replace('/\/\d+$/', '', $addr) ?? $addr : '';
|
||||||
|
$pub = trim((string) ($row['wg_client_public_key'] ?? ''));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'vpn_ip' => is_string($vpnIp) ? trim($vpnIp) : '',
|
||||||
|
'vpn_public_key' => $pub,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $storedMeta @return array<string, mixed> */
|
/** @param array<string, mixed> $storedMeta @return array<string, mixed> */
|
||||||
private static function lookupDeviceContext(string $deviceId, int $companyId, array $storedMeta = []): array {
|
private static function lookupDeviceContext(string $deviceId, int $companyId, array $storedMeta = []): array {
|
||||||
if ($deviceId === '') {
|
if ($deviceId === '') {
|
||||||
|
|||||||
@@ -4,23 +4,97 @@ declare(strict_types=1);
|
|||||||
/** PDO to MariaDB `url_shortener` (separate from crashes DB). */
|
/** PDO to MariaDB `url_shortener` (separate from crashes DB). */
|
||||||
final class UrlShortenerDatabase {
|
final class UrlShortenerDatabase {
|
||||||
private static ?PDO $pdo = null;
|
private static ?PDO $pdo = null;
|
||||||
|
/** @var array<string,mixed>|null|false */
|
||||||
|
private static $resolvedConfig = null;
|
||||||
|
|
||||||
|
/** @return array<string,mixed>|null */
|
||||||
|
private static function resolveConfig(): ?array {
|
||||||
|
if (self::$resolvedConfig !== null) {
|
||||||
|
return self::$resolvedConfig === false ? null : self::$resolvedConfig;
|
||||||
|
}
|
||||||
|
$block = cfg('url_shortener', null);
|
||||||
|
if (is_array($block)) {
|
||||||
|
if (!empty($block['enabled']) && is_array($block['db']['mysql'] ?? null)) {
|
||||||
|
self::$resolvedConfig = $block;
|
||||||
|
return $block;
|
||||||
|
}
|
||||||
|
if (is_array($block['db']['mysql'] ?? null) && ($block['db']['mysql']['database'] ?? '') !== '') {
|
||||||
|
$block['enabled'] = true;
|
||||||
|
self::$resolvedConfig = $block;
|
||||||
|
return $block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$crashDb = cfg('db.mysql', []);
|
||||||
|
if (!is_array($crashDb) || ($crashDb['password'] ?? '') === '') {
|
||||||
|
self::$resolvedConfig = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$m = $crashDb;
|
||||||
|
$m['database'] = 'url_shortener';
|
||||||
|
$candidate = [
|
||||||
|
'enabled' => true,
|
||||||
|
'public_base' => 'https://s.f0xx.org',
|
||||||
|
'db' => ['mysql' => $m],
|
||||||
|
];
|
||||||
|
if (!self::pingMysql($m)) {
|
||||||
|
self::$resolvedConfig = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
self::$resolvedConfig = $candidate;
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string,mixed> $m */
|
||||||
|
private static function pingMysql(array $m): bool {
|
||||||
|
try {
|
||||||
|
$db = (string) ($m['database'] ?? '');
|
||||||
|
if ($db === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$charset = (string) ($m['charset'] ?? 'utf8mb4');
|
||||||
|
$socket = trim((string) ($m['socket'] ?? ''));
|
||||||
|
if ($socket !== '') {
|
||||||
|
$dsn = sprintf('mysql:unix_socket=%s;dbname=%s;charset=%s', $socket, $db, $charset);
|
||||||
|
} else {
|
||||||
|
$dsn = sprintf(
|
||||||
|
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
|
||||||
|
$m['host'] ?? '127.0.0.1',
|
||||||
|
(int) ($m['port'] ?? 3306),
|
||||||
|
$db,
|
||||||
|
$charset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$pdo = new PDO($dsn, (string) ($m['username'] ?? ''), (string) ($m['password'] ?? ''), [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
]);
|
||||||
|
$pdo->query('SELECT 1');
|
||||||
|
return true;
|
||||||
|
} catch (Throwable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function enabled(): bool {
|
public static function enabled(): bool {
|
||||||
return (bool) cfg('url_shortener.enabled', false);
|
return self::resolveConfig() !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function publicBase(): string {
|
public static function publicBase(): string {
|
||||||
|
$block = self::resolveConfig();
|
||||||
|
if ($block !== null) {
|
||||||
|
return rtrim((string) ($block['public_base'] ?? 'https://s.f0xx.org'), '/');
|
||||||
|
}
|
||||||
return rtrim((string) cfg('url_shortener.public_base', 'https://s.f0xx.org'), '/');
|
return rtrim((string) cfg('url_shortener.public_base', 'https://s.f0xx.org'), '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function pdo(): PDO {
|
public static function pdo(): PDO {
|
||||||
if (!self::enabled()) {
|
if (!self::enabled()) {
|
||||||
throw new RuntimeException('url_shortener.enabled is false');
|
throw new RuntimeException('url_shortener not configured');
|
||||||
}
|
}
|
||||||
if (self::$pdo !== null) {
|
if (self::$pdo !== null) {
|
||||||
return self::$pdo;
|
return self::$pdo;
|
||||||
}
|
}
|
||||||
$m = cfg('url_shortener.db.mysql', []);
|
$block = self::resolveConfig();
|
||||||
|
$m = is_array($block) ? ($block['db']['mysql'] ?? []) : [];
|
||||||
if (!is_array($m) || ($m['database'] ?? '') === '') {
|
if (!is_array($m) || ($m['database'] ?? '') === '') {
|
||||||
throw new RuntimeException('url_shortener.db.mysql not configured');
|
throw new RuntimeException('url_shortener.db.mysql not configured');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user