android: rip android_legacy (#335)
Updates #cleanup Signed-off-by: kari-ts <kari@tailscale.com>bradfitz/docs
@ -1,60 +0,0 @@
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = "1.9.22"
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:8.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
flatDir {
|
||||
dirs 'libs'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
ndkVersion "23.1.7779620"
|
||||
compileSdk 33
|
||||
defaultConfig {
|
||||
minSdkVersion 22
|
||||
targetSdkVersion 33
|
||||
versionCode 201
|
||||
versionName "1.61.105-t7429e8912-g12210d3f26b"
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
flavorDimensions "version"
|
||||
productFlavors {
|
||||
fdroid {
|
||||
// The fdroid flavor contains only free dependencies and is suitable
|
||||
// for the F-Droid app store.
|
||||
}
|
||||
play {
|
||||
// The play flavor contains all features and is for the Play Store.
|
||||
}
|
||||
}
|
||||
namespace 'com.tailscale.ipn'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.core:core:1.9.0"
|
||||
implementation "androidx.browser:browser:1.5.0"
|
||||
implementation "androidx.security:security-crypto:1.1.0-alpha06"
|
||||
implementation "androidx.work:work-runtime:2.8.1"
|
||||
implementation ':ipn@aar'
|
||||
testImplementation "junit:junit:4.12"
|
||||
|
||||
// Non-free dependencies.
|
||||
playImplementation 'com.google.android.gms:play-services-auth:20.7.0'
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonFinalResIds=false
|
||||
android.nonTransitiveRClass=false
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
|
@ -1,6 +0,0 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=38f66cd6eef217b4c35855bb11ea4e9fbc53594ccccb5fb82dfd317ef8c2c5a3
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
@ -1,183 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx80m" "-Xms80m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
@ -1,103 +0,0 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx80m" "-Xms80m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
@ -1,86 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<!-- Disable input emulation on ChromeOS -->
|
||||
<uses-feature android:name="android.hardware.type.pc" android:required="false"/>
|
||||
|
||||
<!-- Signal support for Android TV -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
|
||||
<application android:label="Tailscale" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:banner="@drawable/tv_banner"
|
||||
android:name=".App" android:allowBackup="false">
|
||||
<activity android:name="IPNActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.GioApp"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="application/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="message/*" />
|
||||
<data android:mimeType="multipart/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="message/*" />
|
||||
<data android:mimeType="multipart/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<receiver android:name="IPNReceiver"
|
||||
android:exported="true"
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="com.tailscale.ipn.CONNECT_VPN" />
|
||||
<action android:name="com.tailscale.ipn.DISCONNECT_VPN" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<service android:name=".IPNService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".QuickToggleService"
|
||||
android:icon="@drawable/ic_tile"
|
||||
android:label="@string/tile_name"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
Before Width: | Height: | Size: 17 KiB |
@ -1,427 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.Activity;
|
||||
import android.app.DownloadManager;
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.UiModeManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.Signature;
|
||||
import android.content.res.Configuration;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.Settings;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.LinkProperties;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.NetworkRequest;
|
||||
import android.net.Uri;
|
||||
import android.net.VpnService;
|
||||
import android.view.View;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import android.Manifest;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
import java.lang.StringBuilder;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import androidx.security.crypto.EncryptedSharedPreferences;
|
||||
import androidx.security.crypto.MasterKey;
|
||||
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
|
||||
import org.gioui.Gio;
|
||||
|
||||
public class App extends Application {
|
||||
private static final String PEER_TAG = "peer";
|
||||
|
||||
static final String STATUS_CHANNEL_ID = "tailscale-status";
|
||||
static final int STATUS_NOTIFICATION_ID = 1;
|
||||
|
||||
static final String NOTIFY_CHANNEL_ID = "tailscale-notify";
|
||||
static final int NOTIFY_NOTIFICATION_ID = 2;
|
||||
|
||||
private static final String FILE_CHANNEL_ID = "tailscale-files";
|
||||
private static final int FILE_NOTIFICATION_ID = 3;
|
||||
|
||||
private static final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
private ConnectivityManager connectivityManager;
|
||||
public DnsConfig dns = new DnsConfig();
|
||||
public DnsConfig getDnsConfigObj() { return this.dns; }
|
||||
|
||||
@Override public void onCreate() {
|
||||
super.onCreate();
|
||||
// Load and initialize the Go library.
|
||||
Gio.init(this);
|
||||
|
||||
this.connectivityManager = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
setAndRegisterNetworkCallbacks();
|
||||
|
||||
createNotificationChannel(NOTIFY_CHANNEL_ID, "Notifications", NotificationManagerCompat.IMPORTANCE_DEFAULT);
|
||||
createNotificationChannel(STATUS_CHANNEL_ID, "VPN Status", NotificationManagerCompat.IMPORTANCE_LOW);
|
||||
createNotificationChannel(FILE_CHANNEL_ID, "File transfers", NotificationManagerCompat.IMPORTANCE_DEFAULT);
|
||||
}
|
||||
|
||||
// requestNetwork attempts to find the best network that matches the passed NetworkRequest. It is possible that
|
||||
// this might return an unusuable network, eg a captive portal.
|
||||
private void setAndRegisterNetworkCallbacks() {
|
||||
connectivityManager.requestNetwork(dns.getDNSConfigNetworkRequest(), new ConnectivityManager.NetworkCallback(){
|
||||
@Override
|
||||
public void onAvailable(Network network){
|
||||
super.onAvailable(network);
|
||||
StringBuilder sb = new StringBuilder("");
|
||||
LinkProperties linkProperties = connectivityManager.getLinkProperties(network);
|
||||
List<InetAddress> dnsList = linkProperties.getDnsServers();
|
||||
for (InetAddress ip : dnsList) {
|
||||
sb.append(ip.getHostAddress()).append(" ");
|
||||
}
|
||||
String searchDomains = linkProperties.getDomains();
|
||||
if (searchDomains != null) {
|
||||
sb.append("\n");
|
||||
sb.append(searchDomains);
|
||||
}
|
||||
|
||||
dns.updateDNSFromNetwork(sb.toString());
|
||||
onDnsConfigChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLost(Network network) {
|
||||
super.onLost(network);
|
||||
onDnsConfigChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void startVPN() {
|
||||
Intent intent = new Intent(this, IPNService.class);
|
||||
intent.setAction(IPNService.ACTION_REQUEST_VPN);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
public void stopVPN() {
|
||||
Intent intent = new Intent(this, IPNService.class);
|
||||
intent.setAction(IPNService.ACTION_STOP_VPN);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
// encryptToPref a byte array of data using the Jetpack Security
|
||||
// library and writes it to a global encrypted preference store.
|
||||
public void encryptToPref(String prefKey, String plaintext) throws IOException, GeneralSecurityException {
|
||||
getEncryptedPrefs().edit().putString(prefKey, plaintext).commit();
|
||||
}
|
||||
|
||||
// decryptFromPref decrypts a encrypted preference using the Jetpack Security
|
||||
// library and returns the plaintext.
|
||||
public String decryptFromPref(String prefKey) throws IOException, GeneralSecurityException {
|
||||
return getEncryptedPrefs().getString(prefKey, null);
|
||||
}
|
||||
|
||||
private SharedPreferences getEncryptedPrefs() throws IOException, GeneralSecurityException {
|
||||
MasterKey key = new MasterKey.Builder(this)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build();
|
||||
|
||||
return EncryptedSharedPreferences.create(
|
||||
this,
|
||||
"secret_shared_prefs",
|
||||
key,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
);
|
||||
}
|
||||
|
||||
public boolean autoConnect = false;
|
||||
public boolean vpnReady = false;
|
||||
|
||||
void setTileReady(boolean ready) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
return;
|
||||
}
|
||||
QuickToggleService.setReady(this, ready);
|
||||
android.util.Log.d("App", "Set Tile Ready: " + ready + " " + autoConnect);
|
||||
|
||||
vpnReady = ready;
|
||||
if (ready && autoConnect) {
|
||||
startVPN();
|
||||
}
|
||||
}
|
||||
|
||||
void setTileStatus(boolean status) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
return;
|
||||
}
|
||||
QuickToggleService.setStatus(this, status);
|
||||
}
|
||||
|
||||
String getHostname() {
|
||||
String userConfiguredDeviceName = getUserConfiguredDeviceName();
|
||||
if (!isEmpty(userConfiguredDeviceName)) return userConfiguredDeviceName;
|
||||
|
||||
return getModelName();
|
||||
}
|
||||
|
||||
String getModelName() {
|
||||
String manu = Build.MANUFACTURER;
|
||||
String model = Build.MODEL;
|
||||
// Strip manufacturer from model.
|
||||
int idx = model.toLowerCase().indexOf(manu.toLowerCase());
|
||||
if (idx != -1) {
|
||||
model = model.substring(idx + manu.length());
|
||||
model = model.trim();
|
||||
}
|
||||
return manu + " " + model;
|
||||
}
|
||||
|
||||
String getOSVersion() {
|
||||
return Build.VERSION.RELEASE;
|
||||
}
|
||||
|
||||
// get user defined nickname from Settings
|
||||
// returns null if not available
|
||||
private String getUserConfiguredDeviceName() {
|
||||
String nameFromSystemDevice = Settings.Secure.getString(getContentResolver(), "device_name");
|
||||
if (!isEmpty(nameFromSystemDevice)) return nameFromSystemDevice;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isEmpty(String str) {
|
||||
return str == null || str.length() == 0;
|
||||
}
|
||||
|
||||
// attachPeer adds a Peer fragment for tracking the Activity
|
||||
// lifecycle.
|
||||
void attachPeer(Activity act) {
|
||||
act.runOnUiThread(new Runnable() {
|
||||
@Override public void run() {
|
||||
FragmentTransaction ft = act.getFragmentManager().beginTransaction();
|
||||
ft.add(new Peer(), PEER_TAG);
|
||||
ft.commit();
|
||||
act.getFragmentManager().executePendingTransactions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
boolean isChromeOS() {
|
||||
return getPackageManager().hasSystemFeature("android.hardware.type.pc");
|
||||
}
|
||||
|
||||
void prepareVPN(Activity act, int reqCode) {
|
||||
act.runOnUiThread(new Runnable() {
|
||||
@Override public void run() {
|
||||
Intent intent = VpnService.prepare(act);
|
||||
if (intent == null) {
|
||||
onVPNPrepared();
|
||||
} else {
|
||||
startActivityForResult(act, intent, reqCode);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void startActivityForResult(Activity act, Intent intent, int request) {
|
||||
Fragment f = act.getFragmentManager().findFragmentByTag(PEER_TAG);
|
||||
f.startActivityForResult(intent, request);
|
||||
}
|
||||
|
||||
void showURL(Activity act, String url) {
|
||||
act.runOnUiThread(new Runnable() {
|
||||
@Override public void run() {
|
||||
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
|
||||
int headerColor = 0xff496495;
|
||||
builder.setToolbarColor(headerColor);
|
||||
CustomTabsIntent intent = builder.build();
|
||||
intent.launchUrl(act, Uri.parse(url));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// getPackageSignatureFingerprint returns the first package signing certificate, if any.
|
||||
byte[] getPackageCertificate() throws Exception {
|
||||
PackageInfo info;
|
||||
info = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
|
||||
for (Signature signature : info.signatures) {
|
||||
return signature.toByteArray();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void requestNotificationPermission(Activity act) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
// We can send notifications without explicit notifications permission.
|
||||
return;
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(act, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
|
||||
return;
|
||||
}
|
||||
act.requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, IPNActivity.NOTIFICATIONS_PERMISSION_RESULT);
|
||||
}
|
||||
|
||||
void requestWriteStoragePermission(Activity act) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
// We can write files without permission.
|
||||
return;
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(act, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
||||
return;
|
||||
}
|
||||
act.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, IPNActivity.WRITE_STORAGE_RESULT);
|
||||
}
|
||||
|
||||
String insertMedia(String name, String mimeType) throws IOException {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ContentResolver resolver = getContentResolver();
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
|
||||
if (!"".equals(mimeType)) {
|
||||
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
|
||||
}
|
||||
Uri root = MediaStore.Files.getContentUri("external");
|
||||
return resolver.insert(root, contentValues).toString();
|
||||
} else {
|
||||
File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
dir.mkdirs();
|
||||
File f = new File(dir, name);
|
||||
return Uri.fromFile(f).toString();
|
||||
}
|
||||
}
|
||||
|
||||
int openUri(String uri, String mode) throws IOException {
|
||||
ContentResolver resolver = getContentResolver();
|
||||
return resolver.openFileDescriptor(Uri.parse(uri), mode).detachFd();
|
||||
}
|
||||
|
||||
void deleteUri(String uri) {
|
||||
ContentResolver resolver = getContentResolver();
|
||||
resolver.delete(Uri.parse(uri), null, null);
|
||||
}
|
||||
|
||||
public void notifyFile(String uri, String msg) {
|
||||
Intent viewIntent;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
|
||||
} else {
|
||||
// uri is a file:// which is not allowed to be shared outside the app.
|
||||
viewIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
|
||||
}
|
||||
PendingIntent pending = PendingIntent.getActivity(this, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, FILE_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle("File received")
|
||||
.setContentText(msg)
|
||||
.setContentIntent(pending)
|
||||
.setAutoCancel(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
|
||||
|
||||
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
|
||||
nm.notify(FILE_NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
public void createNotificationChannel(String id, String name, int importance) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
NotificationChannel channel = new NotificationChannel(id, name, importance);
|
||||
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
|
||||
nm.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
static native void onVPNPrepared();
|
||||
private static native void onDnsConfigChanged();
|
||||
static native void onShareIntent(int nfiles, int[] types, String[] mimes, String[] items, String[] names, long[] sizes);
|
||||
static native void onWriteStorageGranted();
|
||||
|
||||
// Returns details of the interfaces in the system, encoded as a single string for ease
|
||||
// of JNI transfer over to the Go environment.
|
||||
//
|
||||
// Example:
|
||||
// rmnet_data0 10 2000 true false false false false | fe80::4059:dc16:7ed3:9c6e%rmnet_data0/64
|
||||
// dummy0 3 1500 true false false false false | fe80::1450:5cff:fe13:f891%dummy0/64
|
||||
// wlan0 30 1500 true true false false true | fe80::2f60:2c82:4163:8389%wlan0/64 10.1.10.131/24
|
||||
// r_rmnet_data0 21 1500 true false false false false | fe80::9318:6093:d1ad:ba7f%r_rmnet_data0/64
|
||||
// rmnet_data2 12 1500 true false false false false | fe80::3c8c:44dc:46a9:9907%rmnet_data2/64
|
||||
// r_rmnet_data1 22 1500 true false false false false | fe80::b6cd:5cb0:8ae6:fe92%r_rmnet_data1/64
|
||||
// rmnet_data1 11 1500 true false false false false | fe80::51f2:ee00:edce:d68b%rmnet_data1/64
|
||||
// lo 1 65536 true false true false false | ::1/128 127.0.0.1/8
|
||||
// v4-rmnet_data2 68 1472 true true false true true | 192.0.0.4/32
|
||||
//
|
||||
// Where the fields are:
|
||||
// name ifindex mtu isUp hasBroadcast isLoopback isPointToPoint hasMulticast | ip1/N ip2/N ip3/N;
|
||||
String getInterfacesAsString() {
|
||||
List<NetworkInterface> interfaces;
|
||||
try {
|
||||
interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder("");
|
||||
for (NetworkInterface nif : interfaces) {
|
||||
try {
|
||||
// Android doesn't have a supportsBroadcast() but the Go net.Interface wants
|
||||
// one, so we say the interface has broadcast if it has multicast.
|
||||
sb.append(String.format(java.util.Locale.ROOT, "%s %d %d %b %b %b %b %b |", nif.getName(),
|
||||
nif.getIndex(), nif.getMTU(), nif.isUp(), nif.supportsMulticast(),
|
||||
nif.isLoopback(), nif.isPointToPoint(), nif.supportsMulticast()));
|
||||
|
||||
for (InterfaceAddress ia : nif.getInterfaceAddresses()) {
|
||||
// InterfaceAddress == hostname + "/" + IP
|
||||
String[] parts = ia.toString().split("/", 0);
|
||||
if (parts.length > 1) {
|
||||
sb.append(String.format(java.util.Locale.ROOT, "%s/%d ", parts[1], ia.getNetworkPrefixLength()));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// TODO(dgentry) should log the exception not silently suppress it.
|
||||
continue;
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
boolean isTV() {
|
||||
UiModeManager mm = (UiModeManager)getSystemService(UI_MODE_SERVICE);
|
||||
return mm.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn;
|
||||
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkRequest;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
// Tailscale DNS Config retrieval
|
||||
//
|
||||
// Tailscale's DNS support can either override the local DNS servers with a set of servers
|
||||
// configured in the admin panel, or supplement the local DNS servers with additional
|
||||
// servers for specific domains like example.com.beta.tailscale.net. In the non-override mode,
|
||||
// we need to retrieve the current set of DNS servers from the platform. These will typically
|
||||
// be the DNS servers received from DHCP.
|
||||
//
|
||||
// Importantly, after the Tailscale VPN comes up it will set a DNS server of 100.100.100.100
|
||||
// but we still want to retrieve the underlying DNS servers received from DHCP. If we roam
|
||||
// from Wi-Fi to LTE, we want the DNS servers received from LTE.
|
||||
|
||||
public class DnsConfig {
|
||||
private String dnsConfigs;
|
||||
|
||||
// getDnsConfigAsString returns the current DNS configuration as a multiline string:
|
||||
// line[0] DNS server addresses separated by spaces
|
||||
// line[1] search domains separated by spaces
|
||||
//
|
||||
// For example:
|
||||
// 8.8.8.8 8.8.4.4
|
||||
// example.com
|
||||
//
|
||||
// an empty string means the current DNS configuration could not be retrieved.
|
||||
String getDnsConfigAsString() {
|
||||
return getDnsConfigs().trim();
|
||||
}
|
||||
|
||||
private String getDnsConfigs(){
|
||||
synchronized(this) {
|
||||
return this.dnsConfigs;
|
||||
}
|
||||
}
|
||||
|
||||
void updateDNSFromNetwork(String dnsConfigs){
|
||||
synchronized(this) {
|
||||
this.dnsConfigs = dnsConfigs;
|
||||
}
|
||||
}
|
||||
|
||||
NetworkRequest getDNSConfigNetworkRequest(){
|
||||
// Request networks that are able to reach the Internet.
|
||||
return new NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build();
|
||||
}
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.net.Uri;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.gioui.GioView;
|
||||
|
||||
public final class IPNActivity extends Activity {
|
||||
final static int WRITE_STORAGE_RESULT = 1000;
|
||||
final static int NOTIFICATIONS_PERMISSION_RESULT = 1001;
|
||||
|
||||
private GioView view;
|
||||
|
||||
@Override public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
view = new GioView(this);
|
||||
setContentView(view);
|
||||
handleIntent();
|
||||
}
|
||||
|
||||
@Override public void onNewIntent(Intent i) {
|
||||
setIntent(i);
|
||||
handleIntent();
|
||||
}
|
||||
|
||||
private void handleIntent() {
|
||||
Intent it = getIntent();
|
||||
String act = it.getAction();
|
||||
String[] texts;
|
||||
Uri[] uris;
|
||||
if (Intent.ACTION_SEND.equals(act)) {
|
||||
uris = new Uri[]{it.getParcelableExtra(Intent.EXTRA_STREAM)};
|
||||
texts = new String[]{it.getStringExtra(Intent.EXTRA_TEXT)};
|
||||
} else if (Intent.ACTION_SEND_MULTIPLE.equals(act)) {
|
||||
List<Uri> extraUris = it.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||
uris = extraUris.toArray(new Uri[0]);
|
||||
texts = new String[uris.length];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
String mime = it.getType();
|
||||
int nitems = uris.length;
|
||||
String[] items = new String[nitems];
|
||||
String[] mimes = new String[nitems];
|
||||
int[] types = new int[nitems];
|
||||
String[] names = new String[nitems];
|
||||
long[] sizes = new long[nitems];
|
||||
int nfiles = 0;
|
||||
for (int i = 0; i < uris.length; i++) {
|
||||
String text = texts[i];
|
||||
Uri uri = uris[i];
|
||||
if (text != null) {
|
||||
types[nfiles] = 1; // FileTypeText
|
||||
names[nfiles] = "file.txt";
|
||||
mimes[nfiles] = mime;
|
||||
items[nfiles] = text;
|
||||
// Determined by len(text) in Go to eliminate UTF-8 encoding differences.
|
||||
sizes[nfiles] = 0;
|
||||
nfiles++;
|
||||
} else if (uri != null) {
|
||||
Cursor c = getContentResolver().query(uri, null, null, null, null);
|
||||
if (c == null) {
|
||||
// Ignore files we have no permission to access.
|
||||
continue;
|
||||
}
|
||||
int nameCol = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
int sizeCol = c.getColumnIndex(OpenableColumns.SIZE);
|
||||
c.moveToFirst();
|
||||
String name = c.getString(nameCol);
|
||||
long size = c.getLong(sizeCol);
|
||||
types[nfiles] = 2; // FileTypeURI
|
||||
mimes[nfiles] = mime;
|
||||
items[nfiles] = uri.toString();
|
||||
names[nfiles] = name;
|
||||
sizes[nfiles] = size;
|
||||
nfiles++;
|
||||
}
|
||||
}
|
||||
App.onShareIntent(nfiles, types, mimes, items, names, sizes);
|
||||
}
|
||||
|
||||
@Override public void onRequestPermissionsResult(int reqCode, String[] perms, int[] grants) {
|
||||
switch (reqCode) {
|
||||
case WRITE_STORAGE_RESULT:
|
||||
if (grants.length > 0 && grants[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
App.onWriteStorageGranted();
|
||||
}
|
||||
break;
|
||||
case NOTIFICATIONS_PERMISSION_RESULT:
|
||||
// Start the VPN regardless of the notifications permission being granted.
|
||||
// It's not a blocker for running the VPN.
|
||||
App app = ((App)getApplicationContext());
|
||||
app.startVPN();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onDestroy() {
|
||||
view.destroy();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override public void onStart() {
|
||||
super.onStart();
|
||||
view.start();
|
||||
}
|
||||
|
||||
@Override public void onStop() {
|
||||
view.stop();
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override public void onConfigurationChanged(Configuration c) {
|
||||
super.onConfigurationChanged(c);
|
||||
view.configurationChanged();
|
||||
}
|
||||
|
||||
@Override public void onLowMemory() {
|
||||
super.onLowMemory();
|
||||
view.onLowMemory();
|
||||
}
|
||||
|
||||
@Override public void onBackPressed() {
|
||||
if (!view.backPressed())
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
|
||||
public class IPNReceiver extends BroadcastReceiver {
|
||||
|
||||
public static final String INTENT_CONNECT_VPN = "com.tailscale.ipn.CONNECT_VPN";
|
||||
public static final String INTENT_DISCONNECT_VPN = "com.tailscale.ipn.DISCONNECT_VPN";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
WorkManager workManager = WorkManager.getInstance(context);
|
||||
|
||||
// On the relevant action, start the relevant worker, which can stay active for longer than this receiver can.
|
||||
if (intent.getAction() == INTENT_CONNECT_VPN) {
|
||||
workManager.enqueue(new OneTimeWorkRequest.Builder(StartVPNWorker.class).build());
|
||||
} else if (intent.getAction() == INTENT_DISCONNECT_VPN) {
|
||||
workManager.enqueue(new OneTimeWorkRequest.Builder(StopVPNWorker.class).build());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn;
|
||||
|
||||
import android.os.Build;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.VpnService;
|
||||
import android.system.OsConstants;
|
||||
import androidx.work.WorkManager;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
|
||||
import org.gioui.GioActivity;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
public class IPNService extends VpnService {
|
||||
public static final String ACTION_REQUEST_VPN = "com.tailscale.ipn.REQUEST_VPN";
|
||||
public static final String ACTION_STOP_VPN = "com.tailscale.ipn.STOP_VPN";
|
||||
|
||||
@Override public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent != null && ACTION_STOP_VPN.equals(intent.getAction())) {
|
||||
((App)getApplicationContext()).autoConnect = false;
|
||||
close();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
if (intent != null && "android.net.VpnService".equals(intent.getAction())) {
|
||||
// Start VPN and connect to it due to Always-on VPN
|
||||
Intent i = new Intent(IPNReceiver.INTENT_CONNECT_VPN);
|
||||
i.setPackage(getPackageName());
|
||||
i.setClass(getApplicationContext(), com.tailscale.ipn.IPNReceiver.class);
|
||||
sendBroadcast(i);
|
||||
requestVPN();
|
||||
connect();
|
||||
return START_STICKY;
|
||||
}
|
||||
requestVPN();
|
||||
App app = ((App)getApplicationContext());
|
||||
if (app.vpnReady && app.autoConnect) {
|
||||
connect();
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
private void close() {
|
||||
stopForeground(true);
|
||||
disconnect();
|
||||
}
|
||||
|
||||
@Override public void onDestroy() {
|
||||
close();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override public void onRevoke() {
|
||||
close();
|
||||
super.onRevoke();
|
||||
}
|
||||
|
||||
private PendingIntent configIntent() {
|
||||
return PendingIntent.getActivity(this, 0, new Intent(this, IPNActivity.class),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
}
|
||||
|
||||
private void disallowApp(VpnService.Builder b, String name) {
|
||||
try {
|
||||
b.addDisallowedApplication(name);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected VpnService.Builder newBuilder() {
|
||||
VpnService.Builder b = new VpnService.Builder()
|
||||
.setConfigureIntent(configIntent())
|
||||
.allowFamily(OsConstants.AF_INET)
|
||||
.allowFamily(OsConstants.AF_INET6);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
b.setMetered(false); // Inherit the metered status from the underlying networks.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
b.setUnderlyingNetworks(null); // Use all available networks.
|
||||
|
||||
// RCS/Jibe https://github.com/tailscale/tailscale/issues/2322
|
||||
this.disallowApp(b, "com.google.android.apps.messaging");
|
||||
|
||||
// Stadia https://github.com/tailscale/tailscale/issues/3460
|
||||
this.disallowApp(b, "com.google.stadia.android");
|
||||
|
||||
// Android Auto https://github.com/tailscale/tailscale/issues/3828
|
||||
this.disallowApp(b, "com.google.android.projection.gearhead");
|
||||
|
||||
// GoPro https://github.com/tailscale/tailscale/issues/2554
|
||||
this.disallowApp(b, "com.gopro.smarty");
|
||||
|
||||
// Sonos https://github.com/tailscale/tailscale/issues/2548
|
||||
this.disallowApp(b, "com.sonos.acr");
|
||||
this.disallowApp(b, "com.sonos.acr2");
|
||||
|
||||
// Google Chromecast https://github.com/tailscale/tailscale/issues/3636
|
||||
this.disallowApp(b, "com.google.android.apps.chromecast.app");
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
public void notify(String title, String message) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, App.NOTIFY_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setContentIntent(configIntent())
|
||||
.setAutoCancel(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
|
||||
|
||||
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
|
||||
nm.notify(App.NOTIFY_NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
public void updateStatusNotification(String title, String message) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, App.STATUS_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setContentIntent(configIntent())
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW);
|
||||
|
||||
startForeground(App.STATUS_NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
private native void requestVPN();
|
||||
|
||||
private native void disconnect();
|
||||
private native void connect();
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.Intent;
|
||||
|
||||
public class Peer extends Fragment {
|
||||
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
onActivityResult0(getActivity(), requestCode, resultCode);
|
||||
}
|
||||
|
||||
private static native void onActivityResult0(Activity act, int reqCode, int resCode);
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.service.quicksettings.Tile;
|
||||
import android.service.quicksettings.TileService;
|
||||
|
||||
public class QuickToggleService extends TileService {
|
||||
// lock protects the static fields below it.
|
||||
private static Object lock = new Object();
|
||||
// Active tracks whether the VPN is active.
|
||||
private static boolean active;
|
||||
// Ready tracks whether the tailscale backend is
|
||||
// ready to switch on/off.
|
||||
private static boolean ready;
|
||||
// currentTile tracks getQsTile while service is listening.
|
||||
private static Tile currentTile;
|
||||
|
||||
@Override public void onStartListening() {
|
||||
synchronized (lock) {
|
||||
currentTile = getQsTile();
|
||||
}
|
||||
updateTile();
|
||||
}
|
||||
|
||||
@Override public void onStopListening() {
|
||||
synchronized (lock) {
|
||||
currentTile = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onClick() {
|
||||
boolean r;
|
||||
synchronized (lock) {
|
||||
r = ready;
|
||||
}
|
||||
if (r) {
|
||||
onTileClick();
|
||||
} else {
|
||||
// Start main activity.
|
||||
Intent i = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
||||
startActivityAndCollapse(i);
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateTile() {
|
||||
Tile t;
|
||||
boolean act;
|
||||
synchronized (lock) {
|
||||
t = currentTile;
|
||||
act = active && ready;
|
||||
}
|
||||
if (t == null) {
|
||||
return;
|
||||
}
|
||||
t.setState(act ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE);
|
||||
t.updateTile();
|
||||
}
|
||||
|
||||
static void setReady(Context ctx, boolean rdy) {
|
||||
synchronized (lock) {
|
||||
ready = rdy;
|
||||
}
|
||||
updateTile();
|
||||
}
|
||||
|
||||
static void setStatus(Context ctx, boolean act) {
|
||||
synchronized (lock) {
|
||||
active = act;
|
||||
}
|
||||
updateTile();
|
||||
}
|
||||
|
||||
private void onTileClick() {
|
||||
boolean act;
|
||||
synchronized (lock) {
|
||||
act = active && ready;
|
||||
}
|
||||
Intent i = new Intent(act ? IPNReceiver.INTENT_DISCONNECT_VPN : IPNReceiver.INTENT_CONNECT_VPN);
|
||||
i.setPackage(getPackageName());
|
||||
i.setClass(getApplicationContext(), com.tailscale.ipn.IPNReceiver.class);
|
||||
sendBroadcast(i);
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.VpnService;
|
||||
import android.os.Build;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
public final class StartVPNWorker extends Worker {
|
||||
|
||||
public StartVPNWorker(
|
||||
Context appContext,
|
||||
WorkerParameters workerParams) {
|
||||
super(appContext, workerParams);
|
||||
}
|
||||
|
||||
@Override public Result doWork() {
|
||||
App app = ((App)getApplicationContext());
|
||||
|
||||
// We will start the VPN from the background
|
||||
app.autoConnect = true;
|
||||
// We need to make sure we prepare the VPN Service, just in case it isn't prepared.
|
||||
|
||||
Intent intent = VpnService.prepare(app);
|
||||
if (intent == null) {
|
||||
// If null then the VPN is already prepared and/or it's just been prepared because we have permission
|
||||
app.startVPN();
|
||||
return Result.success();
|
||||
} else {
|
||||
// This VPN possibly doesn't have permission, we need to display a notification which when clicked launches the intent provided.
|
||||
android.util.Log.e("StartVPNWorker", "Tailscale doesn't have permission from the system to start VPN. Launching the intent provided.");
|
||||
|
||||
// Send notification
|
||||
NotificationManager notificationManager = (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
String channelId = "start_vpn_channel";
|
||||
|
||||
// Use createNotificationChannel method from App.java
|
||||
app.createNotificationChannel(channelId, "Start VPN Channel", NotificationManager.IMPORTANCE_DEFAULT);
|
||||
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
int pendingIntentFlags = PendingIntent.FLAG_ONE_SHOT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_IMMUTABLE : 0);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(app, 0, intent, pendingIntentFlags);
|
||||
|
||||
Notification notification = new Notification.Builder(app, channelId)
|
||||
.setContentTitle("Tailscale Connection Failed")
|
||||
.setContentText("Tap here to renew permission.")
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.build();
|
||||
|
||||
notificationManager.notify(1, notification);
|
||||
|
||||
return Result.failure();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn;
|
||||
|
||||
import androidx.work.Worker;
|
||||
import android.content.Context;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
public final class StopVPNWorker extends Worker {
|
||||
|
||||
public StopVPNWorker(
|
||||
Context appContext,
|
||||
WorkerParameters workerParams) {
|
||||
super(appContext, workerParams);
|
||||
}
|
||||
|
||||
@Override public Result doWork() {
|
||||
disconnect();
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
private native void disconnect();
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.localapi
|
||||
|
||||
import android.util.Log
|
||||
import com.tailscale.ipn.ui.model.*
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
// A response from the echo endpoint.
|
||||
typealias StatusResponseHandler = (Result<IpnState.Status>) -> Unit
|
||||
|
||||
typealias BugReportIdHandler = (Result<BugReportID>) -> Unit
|
||||
|
||||
typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit
|
||||
|
||||
class LocalApiClient {
|
||||
constructor() {
|
||||
Log.d("LocalApiClient", "LocalApiClient created")
|
||||
}
|
||||
|
||||
// Perform a request to the local API in the go backend. This is
|
||||
// the primary JNI method for servicing a localAPI call. This
|
||||
// is GUARANTEED to call back into onResponse with the response
|
||||
// from the backend with a matching cookie.
|
||||
// @see cmd/localapiclient/localapishim.go
|
||||
//
|
||||
// request: The path to the localAPI endpoint.
|
||||
// method: The HTTP method to use.
|
||||
// body: The body of the request.
|
||||
// cookie: A unique identifier for this request. This is used map responses to
|
||||
// the corresponding request. Cookies must be unique for each request.
|
||||
external fun doRequest(request: String, method: String, body: String, cookie: String)
|
||||
|
||||
fun <T> executeRequest(request: LocalAPIRequest<T>) {
|
||||
Log.d("LocalApiClient", "Executing request:${request.method}:${request.path}")
|
||||
addRequest(request)
|
||||
// The jni handler will treat the empty string in the body as null.
|
||||
val body = request.body ?: ""
|
||||
doRequest(request.path, request.method, body, request.cookie)
|
||||
}
|
||||
|
||||
// This is called from the JNI layer to publish localAPIResponses. This should execute on the
|
||||
// same thread that called doRequest.
|
||||
fun onResponse(response: String, cookie: String) {
|
||||
val request = requests[cookie]
|
||||
if (request != null) {
|
||||
Log.d("LocalApiClient", "Reponse for request:${request.path} cookie:${request.cookie}")
|
||||
// The response handler will invoked internally by the request parser
|
||||
request.parser(response)
|
||||
removeRequest(cookie)
|
||||
} else {
|
||||
Log.e("LocalApiClient", "Received response for unknown request: ${cookie}")
|
||||
}
|
||||
}
|
||||
|
||||
// Tracks in-flight requests and their callback handlers by cookie. This should
|
||||
// always be manipulated via the addRequest and removeRequest methods.
|
||||
private var requests = HashMap<String, LocalAPIRequest<*>>()
|
||||
private var requestLock = Any()
|
||||
|
||||
fun addRequest(request: LocalAPIRequest<*>) {
|
||||
synchronized(requestLock) { requests[request.cookie] = request }
|
||||
}
|
||||
|
||||
fun removeRequest(cookie: String) {
|
||||
synchronized(requestLock) { requests.remove(cookie) }
|
||||
}
|
||||
|
||||
// localapi Invocations
|
||||
|
||||
fun getStatus(responseHandler: StatusResponseHandler) {
|
||||
val req = LocalAPIRequest.status(responseHandler)
|
||||
executeRequest<IpnState.Status>(req)
|
||||
}
|
||||
|
||||
fun getBugReportId(responseHandler: BugReportIdHandler) {
|
||||
val req = LocalAPIRequest.bugReportId(responseHandler)
|
||||
executeRequest<BugReportID>(req)
|
||||
}
|
||||
|
||||
fun getPrefs(responseHandler: PrefsHandler) {
|
||||
val req = LocalAPIRequest.prefs(responseHandler)
|
||||
executeRequest<Ipn.Prefs>(req)
|
||||
}
|
||||
|
||||
// (jonathan) TODO: A (likely) exhaustive list of localapi endpoints required for
|
||||
// a fully functioning client. This is a work in progress and will be updated
|
||||
// See: corp/xcode/Shared/LocalAPIClient.swift for the various verbs, parameters,
|
||||
// and body contents for each endpoint. Endpoints are defined in LocalAPIEndpoint
|
||||
//
|
||||
// fetchFileTargets
|
||||
// sendFiles
|
||||
// getWaitingFiles
|
||||
// recieveWaitingFile
|
||||
// inidicateFileRecieved
|
||||
// debug
|
||||
// debugLog
|
||||
// uploadClientMetrics
|
||||
// start
|
||||
// startLoginInteractive
|
||||
// logout
|
||||
// profiles
|
||||
// currentProfile
|
||||
// addProfile
|
||||
// switchProfile
|
||||
// deleteProfile
|
||||
// tailnetLocalStatus
|
||||
// signNode
|
||||
// verifyDeepling
|
||||
// ping
|
||||
// setTailFSFileServerAddress
|
||||
|
||||
// Run some tests to validate the APIs work before we have anything
|
||||
// that calls them. This runs after a short delay to avoid not-ready
|
||||
// errors
|
||||
// (jonathan) TODO: Do we need some kind of "onReady" callback?
|
||||
// (jonathan) TODO: Remove these we're further along
|
||||
|
||||
fun runAPITests() = runBlocking {
|
||||
delay(5000L)
|
||||
getStatus { result ->
|
||||
if (result.failed) {
|
||||
Log.e("LocalApiClient", "Error getting status: ${result.error}")
|
||||
} else {
|
||||
val status = result.success
|
||||
Log.d("LocalApiClient", "Got status: ${status}")
|
||||
}
|
||||
}
|
||||
|
||||
getBugReportId { result ->
|
||||
if (result.failed) {
|
||||
Log.e("LocalApiClient", "Error getting bug report id: ${result.error}")
|
||||
} else {
|
||||
val bugReportId = result.success
|
||||
Log.d("LocalApiClient", "Got bug report id: ${bugReportId}")
|
||||
}
|
||||
}
|
||||
|
||||
getPrefs { result ->
|
||||
if (result.failed) {
|
||||
Log.e("LocalApiClient", "Error getting prefs: ${result.error}")
|
||||
} else {
|
||||
val prefs = result.success
|
||||
Log.d("LocalApiClient", "Got prefs: ${prefs}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.localapi
|
||||
|
||||
import com.tailscale.ipn.ui.model.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
enum class LocalAPIEndpoint(val rawValue: String) {
|
||||
Debug("debug"),
|
||||
Debug_Log("debug-log"),
|
||||
BugReport("bugreport"),
|
||||
Prefs("prefs"),
|
||||
FileTargets("file-targets"),
|
||||
UploadMetrics("upload-client-metrics"),
|
||||
Start("start"),
|
||||
LoginInteractive("login-interactive"),
|
||||
ResetAuth("reset-auth"),
|
||||
Logout("logout"),
|
||||
Profiles("profiles"),
|
||||
ProfilesCurrent("profiles/current"),
|
||||
Status("status"),
|
||||
TKAStatus("tka/status"),
|
||||
TKASitng("tka/sign"),
|
||||
TKAVerifyDeeplink("tka/verify-deeplink"),
|
||||
Ping("ping"),
|
||||
Files("files"),
|
||||
FilePut("file-put"),
|
||||
TailFSServerAddress("tailfs/fileserver-address");
|
||||
|
||||
val prefix = "/localapi/v0/"
|
||||
|
||||
fun path(): String {
|
||||
return prefix + rawValue
|
||||
}
|
||||
}
|
||||
|
||||
// Potential local and upstream errors. Error handling in localapi in the go layer
|
||||
// is inconsistent but different clients already deal with that inconsistency so
|
||||
// 'fixing' it will likely break other things.
|
||||
//
|
||||
// For now, anything that isn't an { error: "message" } will be passed along
|
||||
// as UNPARSEABLE_RESPONSE. We can add additional error translation in the parseError
|
||||
// method as needed.
|
||||
//
|
||||
// (jonathan) TODO: Audit local API for all of the possible error results and clean
|
||||
// it up if possible.
|
||||
enum class APIErrorVals(val rawValue: String) {
|
||||
UNPARSEABLE_RESPONSE("Unparseable localAPI response"),
|
||||
NOT_READY("Not Ready");
|
||||
|
||||
fun toError(): Error {
|
||||
return Error(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
class LocalAPIRequest<T>(
|
||||
val path: String,
|
||||
val method: String,
|
||||
val body: String? = null,
|
||||
val responseHandler: (Result<T>) -> Unit,
|
||||
val parser: (String) -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
val cookieLock = Any()
|
||||
var cookieCounter: Int = 0
|
||||
val decoder = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun getCookie(): String {
|
||||
synchronized(cookieLock) {
|
||||
cookieCounter += 1
|
||||
return cookieCounter.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun status(responseHandler: StatusResponseHandler): LocalAPIRequest<IpnState.Status> {
|
||||
val path = LocalAPIEndpoint.Status.path()
|
||||
return LocalAPIRequest<IpnState.Status>(path, "GET", null, responseHandler) { resp ->
|
||||
responseHandler(decode<IpnState.Status>(resp))
|
||||
}
|
||||
}
|
||||
|
||||
fun bugReportId(responseHandler: BugReportIdHandler): LocalAPIRequest<BugReportID> {
|
||||
val path = LocalAPIEndpoint.BugReport.path()
|
||||
return LocalAPIRequest<BugReportID>(path, "POST", null, responseHandler) { resp ->
|
||||
responseHandler(parseString(resp))
|
||||
}
|
||||
}
|
||||
|
||||
fun prefs(responseHandler: PrefsHandler): LocalAPIRequest<Ipn.Prefs> {
|
||||
val path = LocalAPIEndpoint.Prefs.path()
|
||||
return LocalAPIRequest<Ipn.Prefs>(path, "GET", null, responseHandler) { resp ->
|
||||
responseHandler(decode<Ipn.Prefs>(resp))
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the response was a generic error
|
||||
fun parseError(respData: String): Error {
|
||||
try {
|
||||
val err = Json.decodeFromString<Errors.GenericError>(respData)
|
||||
return Error(err.error)
|
||||
} catch (e: Exception) {
|
||||
return Error(APIErrorVals.UNPARSEABLE_RESPONSE.toError())
|
||||
}
|
||||
}
|
||||
|
||||
// Handles responses that are raw strings. Returns an error result if the string
|
||||
// is empty
|
||||
fun parseString(respData: String): Result<String> {
|
||||
return if (respData.length > 0) Result(respData)
|
||||
else Result(APIErrorVals.UNPARSEABLE_RESPONSE.toError())
|
||||
}
|
||||
|
||||
// Attempt to decode the response into the expected type. If that fails, then try
|
||||
// parsing as an error.
|
||||
inline fun <reified T> decode(respData: String): Result<T> {
|
||||
try {
|
||||
val message = decoder.decodeFromString<T>(respData)
|
||||
return Result(message)
|
||||
} catch (e: Exception) {
|
||||
return Result(parseError(respData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val cookie: String = getCookie()
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.localapi
|
||||
|
||||
// Go-like result type with an optional value and an optional Error
|
||||
// This guarantees that only one of the two is non-null
|
||||
class Result<T> {
|
||||
val success: T?
|
||||
val error: Error?
|
||||
|
||||
constructor(success: T?, error: Error?) {
|
||||
if (success != null && error != null) {
|
||||
throw IllegalArgumentException("Result cannot have both a success and an error")
|
||||
}
|
||||
if (success == null && error == null) {
|
||||
throw IllegalArgumentException("Result must have either a success or an error")
|
||||
}
|
||||
|
||||
this.success = success
|
||||
this.error = error
|
||||
}
|
||||
|
||||
constructor(success: T) : this(success, null) {}
|
||||
constructor(error: Error) : this(null, error) {}
|
||||
|
||||
var successful: Boolean = false
|
||||
get() = success != null
|
||||
|
||||
var failed: Boolean = false
|
||||
get() = error != null
|
||||
}
|
Before Width: | Height: | Size: 641 B |
Before Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 879 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.8 KiB |
@ -1,36 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="100"
|
||||
android:viewportHeight="100">
|
||||
<group android:translateX="20"
|
||||
android:translateY="20">
|
||||
<path
|
||||
android:pathData="M15,30.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#FFFDFA"/>
|
||||
<path
|
||||
android:pathData="M30,30.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#FFFDFA"/>
|
||||
<path
|
||||
android:pathData="M15,45.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#54514D"/>
|
||||
<path
|
||||
android:pathData="M45,45.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#54514D"/>
|
||||
<path
|
||||
android:pathData="M30,45.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#FFFDFA"/>
|
||||
<path
|
||||
android:pathData="M45,30.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#FFFDFA"/>
|
||||
<path
|
||||
android:pathData="M15,15.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#54514D"/>
|
||||
<path
|
||||
android:pathData="M30,14.9999m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#54514D"/>
|
||||
<path
|
||||
android:pathData="M45,14.9999m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#54514D"/>
|
||||
</group>
|
||||
</vector>
|
@ -1,35 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="60"
|
||||
android:viewportHeight="60">
|
||||
<group>
|
||||
<path
|
||||
android:pathData="M15,30.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#FFFDFA"/>
|
||||
<path
|
||||
android:pathData="M30,30.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#FFFDFA"/>
|
||||
<path
|
||||
android:pathData="M15,45.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#54514D"/>
|
||||
<path
|
||||
android:pathData="M45,45.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#54514D"/>
|
||||
<path
|
||||
android:pathData="M30,45.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#FFFDFA"/>
|
||||
<path
|
||||
android:pathData="M45,30.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#FFFDFA"/>
|
||||
<path
|
||||
android:pathData="M15,15.0002m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#54514D"/>
|
||||
<path
|
||||
android:pathData="M30,14.9999m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#54514D"/>
|
||||
<path
|
||||
android:pathData="M45,14.9999m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:fillColor="#54514D"/>
|
||||
</group>
|
||||
</vector>
|
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 1021 B |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 10 KiB |
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#1F2125</color>
|
||||
</resources>
|
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Tailscale</string>
|
||||
<string name="tile_name">Tailscale</string>
|
||||
</resources>
|
@ -1,42 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.Context;
|
||||
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignIn;
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
|
||||
|
||||
// Google implements helpers for Google services.
|
||||
public final class Google {
|
||||
static String getIdTokenForActivity(Activity act) {
|
||||
GoogleSignInAccount acc = GoogleSignIn.getLastSignedInAccount(act);
|
||||
return acc.getIdToken();
|
||||
}
|
||||
|
||||
static void googleSignIn(Activity act, String serverOAuthID, int reqCode) {
|
||||
act.runOnUiThread(new Runnable() {
|
||||
@Override public void run() {
|
||||
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
||||
.requestIdToken(serverOAuthID)
|
||||
.requestEmail()
|
||||
.build();
|
||||
GoogleSignInClient client = GoogleSignIn.getClient(act, gso);
|
||||
Intent signInIntent = client.getSignInIntent();
|
||||
App.startActivityForResult(act, signInIntent, reqCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void googleSignOut(Context ctx) {
|
||||
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
||||
.build();
|
||||
GoogleSignInClient client = GoogleSignIn.getClient(ctx, gso);
|
||||
client.signOut();
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
@ -1,488 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package jni implements various helper functions for communicating with the Android JVM
|
||||
// though JNI.
|
||||
package jni
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -Wall
|
||||
|
||||
#include <jni.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static jint jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args) {
|
||||
return (*vm)->AttachCurrentThread(vm, p_env, thr_args);
|
||||
}
|
||||
|
||||
static jint jni_DetachCurrentThread(JavaVM *vm) {
|
||||
return (*vm)->DetachCurrentThread(vm);
|
||||
}
|
||||
|
||||
static jint jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version) {
|
||||
return (*vm)->GetEnv(vm, (void **)env, version);
|
||||
}
|
||||
|
||||
static jclass jni_FindClass(JNIEnv *env, const char *name) {
|
||||
return (*env)->FindClass(env, name);
|
||||
}
|
||||
|
||||
static jthrowable jni_ExceptionOccurred(JNIEnv *env) {
|
||||
return (*env)->ExceptionOccurred(env);
|
||||
}
|
||||
|
||||
static void jni_ExceptionClear(JNIEnv *env) {
|
||||
(*env)->ExceptionClear(env);
|
||||
}
|
||||
|
||||
static jclass jni_GetObjectClass(JNIEnv *env, jobject obj) {
|
||||
return (*env)->GetObjectClass(env, obj);
|
||||
}
|
||||
|
||||
static jmethodID jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
|
||||
return (*env)->GetMethodID(env, clazz, name, sig);
|
||||
}
|
||||
|
||||
static jmethodID jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
|
||||
return (*env)->GetStaticMethodID(env, clazz, name, sig);
|
||||
}
|
||||
|
||||
static jsize jni_GetStringLength(JNIEnv *env, jstring str) {
|
||||
return (*env)->GetStringLength(env, str);
|
||||
}
|
||||
|
||||
static const jchar *jni_GetStringChars(JNIEnv *env, jstring str) {
|
||||
return (*env)->GetStringChars(env, str, NULL);
|
||||
}
|
||||
|
||||
static jstring jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len) {
|
||||
return (*env)->NewString(env, unicodeChars, len);
|
||||
}
|
||||
|
||||
static jboolean jni_IsSameObject(JNIEnv *env, jobject ref1, jobject ref2) {
|
||||
return (*env)->IsSameObject(env, ref1, ref2);
|
||||
}
|
||||
|
||||
static jobject jni_NewGlobalRef(JNIEnv *env, jobject obj) {
|
||||
return (*env)->NewGlobalRef(env, obj);
|
||||
}
|
||||
|
||||
static void jni_DeleteGlobalRef(JNIEnv *env, jobject obj) {
|
||||
(*env)->DeleteGlobalRef(env, obj);
|
||||
}
|
||||
|
||||
static void jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) {
|
||||
(*env)->CallStaticVoidMethodA(env, cls, method, args);
|
||||
}
|
||||
|
||||
static jint jni_CallStaticIntMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) {
|
||||
return (*env)->CallStaticIntMethodA(env, cls, method, args);
|
||||
}
|
||||
|
||||
static jobject jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) {
|
||||
return (*env)->CallStaticObjectMethodA(env, cls, method, args);
|
||||
}
|
||||
|
||||
static jobject jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) {
|
||||
return (*env)->CallObjectMethodA(env, obj, method, args);
|
||||
}
|
||||
|
||||
static jboolean jni_CallBooleanMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) {
|
||||
return (*env)->CallBooleanMethodA(env, obj, method, args);
|
||||
}
|
||||
|
||||
static jint jni_CallIntMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) {
|
||||
return (*env)->CallIntMethodA(env, obj, method, args);
|
||||
}
|
||||
|
||||
static void jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) {
|
||||
(*env)->CallVoidMethodA(env, obj, method, args);
|
||||
}
|
||||
|
||||
static jbyteArray jni_NewByteArray(JNIEnv *env, jsize length) {
|
||||
return (*env)->NewByteArray(env, length);
|
||||
}
|
||||
|
||||
static jboolean *jni_GetBooleanArrayElements(JNIEnv *env, jbooleanArray arr) {
|
||||
return (*env)->GetBooleanArrayElements(env, arr, NULL);
|
||||
}
|
||||
|
||||
static void jni_ReleaseBooleanArrayElements(JNIEnv *env, jbooleanArray arr, jboolean *elems, jint mode) {
|
||||
(*env)->ReleaseBooleanArrayElements(env, arr, elems, mode);
|
||||
}
|
||||
|
||||
static jbyte *jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr) {
|
||||
return (*env)->GetByteArrayElements(env, arr, NULL);
|
||||
}
|
||||
|
||||
static jint *jni_GetIntArrayElements(JNIEnv *env, jintArray arr) {
|
||||
return (*env)->GetIntArrayElements(env, arr, NULL);
|
||||
}
|
||||
|
||||
static void jni_ReleaseIntArrayElements(JNIEnv *env, jintArray arr, jint *elems, jint mode) {
|
||||
(*env)->ReleaseIntArrayElements(env, arr, elems, mode);
|
||||
}
|
||||
|
||||
static jlong *jni_GetLongArrayElements(JNIEnv *env, jlongArray arr) {
|
||||
return (*env)->GetLongArrayElements(env, arr, NULL);
|
||||
}
|
||||
|
||||
static void jni_ReleaseLongArrayElements(JNIEnv *env, jlongArray arr, jlong *elems, jint mode) {
|
||||
(*env)->ReleaseLongArrayElements(env, arr, elems, mode);
|
||||
}
|
||||
|
||||
static void jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *elems, jint mode) {
|
||||
(*env)->ReleaseByteArrayElements(env, arr, elems, mode);
|
||||
}
|
||||
|
||||
static jsize jni_GetArrayLength(JNIEnv *env, jarray arr) {
|
||||
return (*env)->GetArrayLength(env, arr);
|
||||
}
|
||||
|
||||
static void jni_DeleteLocalRef(JNIEnv *env, jobject localRef) {
|
||||
return (*env)->DeleteLocalRef(env, localRef);
|
||||
}
|
||||
|
||||
static jobject jni_GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index) {
|
||||
return (*env)->GetObjectArrayElement(env, array, index);
|
||||
}
|
||||
|
||||
static jboolean jni_IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz) {
|
||||
return (*env)->IsInstanceOf(env, obj, clazz);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
type JVM C.JavaVM
|
||||
|
||||
type Env C.JNIEnv
|
||||
|
||||
type (
|
||||
Class C.jclass
|
||||
Object C.jobject
|
||||
MethodID C.jmethodID
|
||||
String C.jstring
|
||||
ByteArray C.jbyteArray
|
||||
ObjectArray C.jobjectArray
|
||||
BooleanArray C.jbooleanArray
|
||||
LongArray C.jlongArray
|
||||
IntArray C.jintArray
|
||||
Boolean C.jboolean
|
||||
Value uint64 // All JNI types fit into 64-bits.
|
||||
)
|
||||
|
||||
// Cached class handles.
|
||||
var classes struct {
|
||||
once sync.Once
|
||||
stringClass, integerClass Class
|
||||
|
||||
integerIntValue MethodID
|
||||
}
|
||||
|
||||
func env(e *Env) *C.JNIEnv {
|
||||
return (*C.JNIEnv)(unsafe.Pointer(e))
|
||||
}
|
||||
|
||||
func javavm(vm *JVM) *C.JavaVM {
|
||||
return (*C.JavaVM)(unsafe.Pointer(vm))
|
||||
}
|
||||
|
||||
// Do invokes a function with a temporary JVM environment. The
|
||||
// environment is not valid after the function returns.
|
||||
func Do(vm *JVM, f func(env *Env) error) error {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
var env *C.JNIEnv
|
||||
if res := C.jni_GetEnv(javavm(vm), &env, C.JNI_VERSION_1_6); res != C.JNI_OK {
|
||||
if res != C.JNI_EDETACHED {
|
||||
panic(fmt.Errorf("JNI GetEnv failed with error %d", res))
|
||||
}
|
||||
if C.jni_AttachCurrentThread(javavm(vm), &env, nil) != C.JNI_OK {
|
||||
panic(errors.New("runInJVM: AttachCurrentThread failed"))
|
||||
}
|
||||
defer C.jni_DetachCurrentThread(javavm(vm))
|
||||
}
|
||||
|
||||
return f((*Env)(unsafe.Pointer(env)))
|
||||
}
|
||||
|
||||
func Bool(b bool) Boolean {
|
||||
if b {
|
||||
return C.JNI_TRUE
|
||||
}
|
||||
return C.JNI_FALSE
|
||||
}
|
||||
|
||||
func varArgs(args []Value) *C.jvalue {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
return (*C.jvalue)(unsafe.Pointer(&args[0]))
|
||||
}
|
||||
|
||||
func IsSameObject(e *Env, ref1, ref2 Object) bool {
|
||||
same := C.jni_IsSameObject(env(e), C.jobject(ref1), C.jobject(ref2))
|
||||
return same == C.JNI_TRUE
|
||||
}
|
||||
|
||||
func CallStaticIntMethod(e *Env, cls Class, method MethodID, args ...Value) (int, error) {
|
||||
res := C.jni_CallStaticIntMethodA(env(e), C.jclass(cls), C.jmethodID(method), varArgs(args))
|
||||
return int(res), exception(e)
|
||||
}
|
||||
|
||||
func CallStaticVoidMethod(e *Env, cls Class, method MethodID, args ...Value) error {
|
||||
C.jni_CallStaticVoidMethodA(env(e), C.jclass(cls), C.jmethodID(method), varArgs(args))
|
||||
return exception(e)
|
||||
}
|
||||
|
||||
func CallVoidMethod(e *Env, obj Object, method MethodID, args ...Value) error {
|
||||
C.jni_CallVoidMethodA(env(e), C.jobject(obj), C.jmethodID(method), varArgs(args))
|
||||
return exception(e)
|
||||
}
|
||||
|
||||
func CallStaticObjectMethod(e *Env, cls Class, method MethodID, args ...Value) (Object, error) {
|
||||
res := C.jni_CallStaticObjectMethodA(env(e), C.jclass(cls), C.jmethodID(method), varArgs(args))
|
||||
return Object(res), exception(e)
|
||||
}
|
||||
|
||||
func CallObjectMethod(e *Env, obj Object, method MethodID, args ...Value) (Object, error) {
|
||||
res := C.jni_CallObjectMethodA(env(e), C.jobject(obj), C.jmethodID(method), varArgs(args))
|
||||
return Object(res), exception(e)
|
||||
}
|
||||
|
||||
func CallBooleanMethod(e *Env, obj Object, method MethodID, args ...Value) (bool, error) {
|
||||
res := C.jni_CallBooleanMethodA(env(e), C.jobject(obj), C.jmethodID(method), varArgs(args))
|
||||
return res == C.JNI_TRUE, exception(e)
|
||||
}
|
||||
|
||||
func CallIntMethod(e *Env, obj Object, method MethodID, args ...Value) (int32, error) {
|
||||
res := C.jni_CallIntMethodA(env(e), C.jobject(obj), C.jmethodID(method), varArgs(args))
|
||||
return int32(res), exception(e)
|
||||
}
|
||||
|
||||
// GetByteArrayElements returns the contents of the byte array.
|
||||
func GetByteArrayElements(e *Env, jarr ByteArray) []byte {
|
||||
if jarr == 0 {
|
||||
return nil
|
||||
}
|
||||
size := C.jni_GetArrayLength(env(e), C.jarray(jarr))
|
||||
elems := C.jni_GetByteArrayElements(env(e), C.jbyteArray(jarr))
|
||||
defer C.jni_ReleaseByteArrayElements(env(e), C.jbyteArray(jarr), elems, 0)
|
||||
backing := (*(*[1 << 30]byte)(unsafe.Pointer(elems)))[:size:size]
|
||||
s := make([]byte, len(backing))
|
||||
copy(s, backing)
|
||||
return s
|
||||
}
|
||||
|
||||
// GetBooleanArrayElements returns the contents of the boolean array.
|
||||
func GetBooleanArrayElements(e *Env, jarr BooleanArray) []bool {
|
||||
if jarr == 0 {
|
||||
return nil
|
||||
}
|
||||
size := C.jni_GetArrayLength(env(e), C.jarray(jarr))
|
||||
elems := C.jni_GetBooleanArrayElements(env(e), C.jbooleanArray(jarr))
|
||||
defer C.jni_ReleaseBooleanArrayElements(env(e), C.jbooleanArray(jarr), elems, 0)
|
||||
backing := (*(*[1 << 30]C.jboolean)(unsafe.Pointer(elems)))[:size:size]
|
||||
r := make([]bool, len(backing))
|
||||
for i, b := range backing {
|
||||
r[i] = b == C.JNI_TRUE
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// GetStringArrayElements returns the contents of the String array.
|
||||
func GetStringArrayElements(e *Env, jarr ObjectArray) []string {
|
||||
var strings []string
|
||||
iterateObjectArray(e, jarr, func(e *Env, idx int, item Object) {
|
||||
s := GoString(e, String(item))
|
||||
strings = append(strings, s)
|
||||
})
|
||||
return strings
|
||||
}
|
||||
|
||||
// GetIntArrayElements returns the contents of the int array.
|
||||
func GetIntArrayElements(e *Env, jarr IntArray) []int {
|
||||
if jarr == 0 {
|
||||
return nil
|
||||
}
|
||||
size := C.jni_GetArrayLength(env(e), C.jarray(jarr))
|
||||
elems := C.jni_GetIntArrayElements(env(e), C.jintArray(jarr))
|
||||
defer C.jni_ReleaseIntArrayElements(env(e), C.jintArray(jarr), elems, 0)
|
||||
backing := (*(*[1 << 27]C.jint)(unsafe.Pointer(elems)))[:size:size]
|
||||
r := make([]int, len(backing))
|
||||
for i, l := range backing {
|
||||
r[i] = int(l)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// GetLongArrayElements returns the contents of the long array.
|
||||
func GetLongArrayElements(e *Env, jarr LongArray) []int64 {
|
||||
if jarr == 0 {
|
||||
return nil
|
||||
}
|
||||
size := C.jni_GetArrayLength(env(e), C.jarray(jarr))
|
||||
elems := C.jni_GetLongArrayElements(env(e), C.jlongArray(jarr))
|
||||
defer C.jni_ReleaseLongArrayElements(env(e), C.jlongArray(jarr), elems, 0)
|
||||
backing := (*(*[1 << 27]C.jlong)(unsafe.Pointer(elems)))[:size:size]
|
||||
r := make([]int64, len(backing))
|
||||
for i, l := range backing {
|
||||
r[i] = int64(l)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func iterateObjectArray(e *Env, jarr ObjectArray, f func(e *Env, idx int, item Object)) {
|
||||
if jarr == 0 {
|
||||
return
|
||||
}
|
||||
size := C.jni_GetArrayLength(env(e), C.jarray(jarr))
|
||||
for i := 0; i < int(size); i++ {
|
||||
item := C.jni_GetObjectArrayElement(env(e), C.jobjectArray(jarr), C.jint(i))
|
||||
f(e, i, Object(item))
|
||||
C.jni_DeleteLocalRef(env(e), item)
|
||||
}
|
||||
}
|
||||
|
||||
// NewByteArray allocates a Java byte array with the content. It
|
||||
// panics if the allocation fails.
|
||||
func NewByteArray(e *Env, content []byte) ByteArray {
|
||||
jarr := C.jni_NewByteArray(env(e), C.jsize(len(content)))
|
||||
if jarr == 0 {
|
||||
panic(fmt.Errorf("jni: NewByteArray(%d) failed", len(content)))
|
||||
}
|
||||
elems := C.jni_GetByteArrayElements(env(e), jarr)
|
||||
defer C.jni_ReleaseByteArrayElements(env(e), jarr, elems, 0)
|
||||
backing := (*(*[1 << 30]byte)(unsafe.Pointer(elems)))[:len(content):len(content)]
|
||||
copy(backing, content)
|
||||
return ByteArray(jarr)
|
||||
}
|
||||
|
||||
// ClassLoader returns a reference to the Java ClassLoader associated
|
||||
// with obj.
|
||||
func ClassLoaderFor(e *Env, obj Object) Object {
|
||||
cls := GetObjectClass(e, obj)
|
||||
getClassLoader := GetMethodID(e, cls, "getClassLoader", "()Ljava/lang/ClassLoader;")
|
||||
clsLoader, err := CallObjectMethod(e, Object(obj), getClassLoader)
|
||||
if err != nil {
|
||||
// Class.getClassLoader should never fail.
|
||||
panic(err)
|
||||
}
|
||||
return Object(clsLoader)
|
||||
}
|
||||
|
||||
// LoadClass invokes the underlying ClassLoader's loadClass method and
|
||||
// returns the class.
|
||||
func LoadClass(e *Env, loader Object, class string) (Class, error) {
|
||||
cls := GetObjectClass(e, loader)
|
||||
loadClass := GetMethodID(e, cls, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;")
|
||||
name := JavaString(e, class)
|
||||
loaded, err := CallObjectMethod(e, loader, loadClass, Value(name))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return Class(loaded), exception(e)
|
||||
}
|
||||
|
||||
// exception returns an error corresponding to the pending
|
||||
// exception, and clears it. exceptionError returns nil if no
|
||||
// exception is pending.
|
||||
func exception(e *Env) error {
|
||||
thr := C.jni_ExceptionOccurred(env(e))
|
||||
if thr == 0 {
|
||||
return nil
|
||||
}
|
||||
C.jni_ExceptionClear(env(e))
|
||||
cls := GetObjectClass(e, Object(thr))
|
||||
toString := GetMethodID(e, cls, "toString", "()Ljava/lang/String;")
|
||||
msg, err := CallObjectMethod(e, Object(thr), toString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New(GoString(e, String(msg)))
|
||||
}
|
||||
|
||||
// GetObjectClass returns the Java Class for an Object.
|
||||
func GetObjectClass(e *Env, obj Object) Class {
|
||||
if obj == 0 {
|
||||
panic("null object")
|
||||
}
|
||||
cls := C.jni_GetObjectClass(env(e), C.jobject(obj))
|
||||
if err := exception(e); err != nil {
|
||||
// GetObjectClass should never fail.
|
||||
panic(err)
|
||||
}
|
||||
return Class(cls)
|
||||
}
|
||||
|
||||
// GetStaticMethodID returns the id for a static method. It panics if the method
|
||||
// wasn't found.
|
||||
func GetStaticMethodID(e *Env, cls Class, name, signature string) MethodID {
|
||||
mname := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(mname))
|
||||
msig := C.CString(signature)
|
||||
defer C.free(unsafe.Pointer(msig))
|
||||
m := C.jni_GetStaticMethodID(env(e), C.jclass(cls), mname, msig)
|
||||
if err := exception(e); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return MethodID(m)
|
||||
}
|
||||
|
||||
// GetMethodID returns the id for a method. It panics if the method
|
||||
// wasn't found.
|
||||
func GetMethodID(e *Env, cls Class, name, signature string) MethodID {
|
||||
mname := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(mname))
|
||||
msig := C.CString(signature)
|
||||
defer C.free(unsafe.Pointer(msig))
|
||||
m := C.jni_GetMethodID(env(e), C.jclass(cls), mname, msig)
|
||||
if err := exception(e); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return MethodID(m)
|
||||
}
|
||||
|
||||
func NewGlobalRef(e *Env, obj Object) Object {
|
||||
return Object(C.jni_NewGlobalRef(env(e), C.jobject(obj)))
|
||||
}
|
||||
|
||||
func DeleteGlobalRef(e *Env, obj Object) {
|
||||
C.jni_DeleteGlobalRef(env(e), C.jobject(obj))
|
||||
}
|
||||
|
||||
// JavaString converts the string to a JVM jstring.
|
||||
func JavaString(e *Env, str string) String {
|
||||
if str == "" {
|
||||
return 0
|
||||
}
|
||||
utf16Chars := utf16.Encode([]rune(str))
|
||||
res := C.jni_NewString(env(e), (*C.jchar)(unsafe.Pointer(&utf16Chars[0])), C.int(len(utf16Chars)))
|
||||
return String(res)
|
||||
}
|
||||
|
||||
// GoString converts the JVM jstring to a Go string.
|
||||
func GoString(e *Env, str String) string {
|
||||
if str == 0 {
|
||||
return ""
|
||||
}
|
||||
strlen := C.jni_GetStringLength(env(e), C.jstring(str))
|
||||
chars := C.jni_GetStringChars(env(e), C.jstring(str))
|
||||
var utf16Chars []uint16
|
||||
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&utf16Chars))
|
||||
hdr.Data = uintptr(unsafe.Pointer(chars))
|
||||
hdr.Cap = int(strlen)
|
||||
hdr.Len = int(strlen)
|
||||
utf8 := utf16.Decode(utf16Chars)
|
||||
return string(utf8)
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package localapiservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ctx = context.Background()
|
||||
|
||||
type BadStatusHandler struct{}
|
||||
|
||||
func (b *BadStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestBadStatus(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(2*time.Second))
|
||||
client := New(&BadStatusHandler{})
|
||||
defer cancel()
|
||||
|
||||
_, err := client.Call(ctx, "POST", "test", nil)
|
||||
|
||||
if err.Error() != "request failed with status code 400" {
|
||||
t.Error("Expected bad status error, but got", err)
|
||||
}
|
||||
}
|
||||
|
||||
type TimeoutHandler struct{}
|
||||
|
||||
var successfulResponse = "successful response!"
|
||||
|
||||
func (b *TimeoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(6 * time.Second)
|
||||
w.Write([]byte(successfulResponse))
|
||||
}
|
||||
|
||||
func TestTimeout(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(2*time.Second))
|
||||
client := New(&TimeoutHandler{})
|
||||
defer cancel()
|
||||
|
||||
_, err := client.Call(ctx, "GET", "test", nil)
|
||||
|
||||
if err.Error() != "timeout for test" {
|
||||
t.Error("Expected timeout error, but got", err)
|
||||
}
|
||||
}
|
||||
|
||||
type SuccessfulHandler struct{}
|
||||
|
||||
func (b *SuccessfulHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(successfulResponse))
|
||||
}
|
||||
|
||||
func TestSuccess(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(2*time.Second))
|
||||
client := New(&SuccessfulHandler{})
|
||||
defer cancel()
|
||||
|
||||
w, err := client.Call(ctx, "GET", "test", nil)
|
||||
|
||||
if err != nil {
|
||||
t.Error("Expected no error, but got", err)
|
||||
}
|
||||
|
||||
report, err := io.ReadAll(w.Body())
|
||||
if string(report) != successfulResponse {
|
||||
t.Error("Expected successful report, but got", report)
|
||||
}
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package localapiservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
)
|
||||
|
||||
type LocalAPIService struct {
|
||||
h http.Handler
|
||||
}
|
||||
|
||||
func New(h http.Handler) *LocalAPIService {
|
||||
return &LocalAPIService{h: h}
|
||||
}
|
||||
|
||||
// Call calls the given endpoint on the local API using the given HTTP method
|
||||
// optionally sending the given body. It returns a Response representing the
|
||||
// result of the call and an error if the call could not be completed or the
|
||||
// local API returned a status code in the 400 series or greater.
|
||||
// Note - Response includes a response body available from the Body method, it
|
||||
// is the caller's responsibility to close this.
|
||||
func (cl *LocalAPIService) Call(ctx context.Context, method, endpoint string, body io.Reader) (*Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, endpoint, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating new request for %s: %w", endpoint, err)
|
||||
}
|
||||
deadline, _ := ctx.Deadline()
|
||||
pipeReader, pipeWriter := net.Pipe()
|
||||
pipeReader.SetDeadline(deadline)
|
||||
pipeWriter.SetDeadline(deadline)
|
||||
|
||||
resp := &Response{
|
||||
headers: http.Header{},
|
||||
status: http.StatusOK,
|
||||
bodyReader: pipeReader,
|
||||
bodyWriter: pipeWriter,
|
||||
startWritingBody: make(chan interface{}),
|
||||
}
|
||||
|
||||
go func() {
|
||||
cl.h.ServeHTTP(resp, req)
|
||||
resp.Flush()
|
||||
pipeWriter.Close()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-resp.startWritingBody:
|
||||
if resp.StatusCode() >= 400 {
|
||||
return resp, fmt.Errorf("request failed with status code %d", resp.StatusCode())
|
||||
}
|
||||
return resp, nil
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("timeout for %s", endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalAPIService) GetBugReportID(ctx context.Context, bugReportChan chan<- string, fallbackLog string) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
r, err := s.Call(ctx, "POST", "/localapi/v0/bugreport", nil)
|
||||
defer r.Body().Close()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("get bug report: %s", err)
|
||||
bugReportChan <- fallbackLog
|
||||
return
|
||||
}
|
||||
logBytes, err := io.ReadAll(r.Body())
|
||||
if err != nil {
|
||||
log.Printf("read bug report: %s", err)
|
||||
bugReportChan <- fallbackLog
|
||||
return
|
||||
}
|
||||
bugReportChan <- string(logBytes)
|
||||
}
|
||||
|
||||
func (s *LocalAPIService) Login(ctx context.Context, backend *ipnlocal.LocalBackend) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
defer cancel()
|
||||
r, err := s.Call(ctx, "POST", "/localapi/v0/login-interactive", nil)
|
||||
defer r.Body().Close()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("login: %s", err)
|
||||
backend.StartLoginInteractive()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalAPIService) Logout(ctx context.Context, backend *ipnlocal.LocalBackend) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
defer cancel()
|
||||
r, err := s.Call(ctx, "POST", "/localapi/v0/logout", nil)
|
||||
defer r.Body().Close()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("logout: %s", err)
|
||||
logoutctx, logoutcancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
defer logoutcancel()
|
||||
backend.Logout(logoutctx)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
@ -1,195 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package localapiservice
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tailscale/tailscale-android/cmd/jni"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
)
|
||||
|
||||
// #include <jni.h>
|
||||
import "C"
|
||||
|
||||
// Shims the LocalApiClient class from the Kotlin side to the Go side's LocalAPIService.
|
||||
var shim struct {
|
||||
// localApiClient is a global reference to the com.tailscale.ipn.ui.localapi.LocalApiClient class.
|
||||
clientClass jni.Class
|
||||
|
||||
// notifierClass is a global reference to the com.tailscale.ipn.ui.notifier.Notifier class.
|
||||
notifierClass jni.Class
|
||||
|
||||
// Typically a shared LocalAPIService instance.
|
||||
service *LocalAPIService
|
||||
|
||||
backend *ipnlocal.LocalBackend
|
||||
|
||||
cancelWatchBus func()
|
||||
|
||||
jvm *jni.JVM
|
||||
}
|
||||
|
||||
//export Java_com_tailscale_ipn_ui_localapi_Request_doRequest
|
||||
func Java_com_tailscale_ipn_ui_localapi_Request_doRequest(
|
||||
env *C.JNIEnv,
|
||||
cls C.jclass,
|
||||
jmethod C.jstring,
|
||||
jpath C.jstring,
|
||||
jbody C.jbyteArray) {
|
||||
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
log.Printf("doRequest() panicked with %q, stack: %s", p, debug.Stack())
|
||||
panic(p)
|
||||
}
|
||||
}()
|
||||
|
||||
jenv := (*jni.Env)(unsafe.Pointer(env))
|
||||
|
||||
// The HTTP verb
|
||||
methodRef := jni.NewGlobalRef(jenv, jni.Object(jmethod))
|
||||
methodStr := jni.GoString(jenv, jni.String(methodRef))
|
||||
defer jni.DeleteGlobalRef(jenv, methodRef)
|
||||
|
||||
// The API Path
|
||||
pathRef := jni.NewGlobalRef(jenv, jni.Object(jpath))
|
||||
pathStr := jni.GoString(jenv, jni.String(pathRef))
|
||||
defer jni.DeleteGlobalRef(jenv, pathRef)
|
||||
|
||||
// The body string. This is optional and may be empty.
|
||||
bodyRef := jni.NewGlobalRef(jenv, jni.Object(jbody))
|
||||
bodyArray := jni.GetByteArrayElements(jenv, jni.ByteArray(bodyRef))
|
||||
defer jni.DeleteGlobalRef(jenv, bodyRef)
|
||||
|
||||
resp := doLocalAPIRequest(pathStr, methodStr, bodyArray)
|
||||
|
||||
jrespBody := jni.NewByteArray(jenv, resp)
|
||||
respBody := jni.Value(jrespBody)
|
||||
onResponse := jni.GetMethodID(jenv, shim.clientClass, "onResponse", "([B)V")
|
||||
|
||||
jni.CallVoidMethod(jenv, jni.Object(cls), onResponse, respBody)
|
||||
}
|
||||
|
||||
func doLocalAPIRequest(path string, method string, body []byte) []byte {
|
||||
if shim.service == nil {
|
||||
return []byte("{\"error\":\"Not Ready\"}")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
var reader io.Reader = nil
|
||||
if len(body) > 0 {
|
||||
reader = bytes.NewReader(body)
|
||||
}
|
||||
|
||||
r, err := shim.service.Call(ctx, method, path, reader)
|
||||
if err != nil {
|
||||
log.Printf("error calling %s %q: %s", method, path, err)
|
||||
return []byte("{\"error\":\"" + err.Error() + "\"}")
|
||||
}
|
||||
|
||||
defer r.Body().Close()
|
||||
respBytes, err := io.ReadAll(r.Body())
|
||||
if err != nil {
|
||||
return []byte("{\"error\":\"" + err.Error() + "\"}")
|
||||
}
|
||||
return respBytes
|
||||
}
|
||||
|
||||
// Assign a localAPIService to our shim for handling incoming localapi requests from the Kotlin side.
|
||||
func ConfigureShim(jvm *jni.JVM, appCtx jni.Object, s *LocalAPIService, b *ipnlocal.LocalBackend) {
|
||||
shim.service = s
|
||||
shim.backend = b
|
||||
|
||||
configureLocalAPIJNIHandler(jvm, appCtx)
|
||||
|
||||
// Let the Kotlin side know we're ready to handle requests.
|
||||
jni.Do(jvm, func(env *jni.Env) error {
|
||||
onReadyAPI := jni.GetStaticMethodID(env, shim.clientClass, "onReady", "()V")
|
||||
jni.CallStaticVoidMethod(env, shim.clientClass, onReadyAPI)
|
||||
|
||||
onNotifyNot := jni.GetStaticMethodID(env, shim.notifierClass, "onReady", "()V")
|
||||
jni.CallStaticVoidMethod(env, shim.notifierClass, onNotifyNot)
|
||||
|
||||
log.Printf("LocalAPI Shim ready")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Loads the Kotlin-side LocalApiClient class and stores it in a global reference.
|
||||
func configureLocalAPIJNIHandler(jvm *jni.JVM, appCtx jni.Object) error {
|
||||
shim.jvm = jvm
|
||||
|
||||
return jni.Do(jvm, func(env *jni.Env) error {
|
||||
loader := jni.ClassLoaderFor(env, appCtx)
|
||||
cl, err := jni.LoadClass(env, loader, "com.tailscale.ipn.ui.localapi.Request")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shim.clientClass = jni.Class(jni.NewGlobalRef(env, jni.Object(cl)))
|
||||
|
||||
cl, err = jni.LoadClass(env, loader, "com.tailscale.ipn.ui.notifier.Notifier")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shim.notifierClass = jni.Class(jni.NewGlobalRef(env, jni.Object(cl)))
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
//export Java_com_tailscale_ipn_ui_notifier_Notifier_stopIPNBusWatcher
|
||||
func Java_com_tailscale_ipn_ui_notifier_Notifier_stopIPNBusWatcher(
|
||||
env *C.JNIEnv,
|
||||
cls C.jclass) {
|
||||
|
||||
if shim.cancelWatchBus != nil {
|
||||
log.Printf("Stop watching IPN bus")
|
||||
shim.cancelWatchBus()
|
||||
shim.cancelWatchBus = nil
|
||||
} else {
|
||||
log.Printf("Not watching IPN bus, nothing to cancel")
|
||||
}
|
||||
}
|
||||
|
||||
//export Java_com_tailscale_ipn_ui_notifier_Notifier_startIPNBusWatcher
|
||||
func Java_com_tailscale_ipn_ui_notifier_Notifier_startIPNBusWatcher(
|
||||
env *C.JNIEnv,
|
||||
cls C.jclass,
|
||||
jmask C.jint) {
|
||||
|
||||
jenv := (*jni.Env)(unsafe.Pointer(env))
|
||||
|
||||
log.Printf("Start watching IPN bus")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
shim.cancelWatchBus = cancel
|
||||
opts := ipn.NotifyWatchOpt(jmask)
|
||||
|
||||
shim.backend.WatchNotifications(ctx, opts, func() {
|
||||
// onWatchAdded
|
||||
}, func(roNotify *ipn.Notify) bool {
|
||||
js, err := json.Marshal(roNotify)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
jni.Do(shim.jvm, func(env *jni.Env) error {
|
||||
jjson := jni.NewByteArray(jenv, js)
|
||||
onNotify := jni.GetStaticMethodID(jenv, shim.notifierClass, "onNotify", "([B)V")
|
||||
jni.CallStaticVoidMethod(jenv, shim.notifierClass, onNotify, jni.Value(jjson))
|
||||
return nil
|
||||
})
|
||||
return true
|
||||
})
|
||||
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package localapiservice
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Response represents the result of processing an localAPI request.
|
||||
// On completion, the response body can be read out of the bodyWriter.
|
||||
type Response struct {
|
||||
headers http.Header
|
||||
status int
|
||||
bodyWriter net.Conn
|
||||
bodyReader net.Conn
|
||||
startWritingBody chan interface{}
|
||||
startWritingBodyOnce sync.Once
|
||||
}
|
||||
|
||||
func (r *Response) Header() http.Header {
|
||||
return r.headers
|
||||
}
|
||||
|
||||
// Write writes the data to the response body which an then be
|
||||
// read out as a json object.
|
||||
func (r *Response) Write(data []byte) (int, error) {
|
||||
r.Flush()
|
||||
if r.status == 0 {
|
||||
r.WriteHeader(http.StatusOK)
|
||||
}
|
||||
return r.bodyWriter.Write(data)
|
||||
}
|
||||
|
||||
func (r *Response) WriteHeader(statusCode int) {
|
||||
r.status = statusCode
|
||||
}
|
||||
|
||||
func (r *Response) Body() net.Conn {
|
||||
return r.bodyReader
|
||||
}
|
||||
|
||||
func (r *Response) StatusCode() int {
|
||||
return r.status
|
||||
}
|
||||
|
||||
func (r *Response) Flush() {
|
||||
r.startWritingBodyOnce.Do(func() {
|
||||
close(r.startWritingBody)
|
||||
})
|
||||
}
|
@ -1,449 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/tailscale-android/cmd/jni"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/logtail/filch"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
type backend struct {
|
||||
engine wgengine.Engine
|
||||
backend *ipnlocal.LocalBackend
|
||||
sys *tsd.System
|
||||
devices *multiTUN
|
||||
settings settingsFunc
|
||||
lastCfg *router.Config
|
||||
lastDNSCfg *dns.OSConfig
|
||||
netMon *netmon.Monitor
|
||||
|
||||
logIDPublic logid.PublicID
|
||||
logger *logtail.Logger
|
||||
|
||||
// avoidEmptyDNS controls whether to use fallback nameservers
|
||||
// when no nameservers are provided by Tailscale.
|
||||
avoidEmptyDNS bool
|
||||
|
||||
jvm *jni.JVM
|
||||
appCtx jni.Object
|
||||
}
|
||||
|
||||
type settingsFunc func(*router.Config, *dns.OSConfig) error
|
||||
|
||||
const defaultMTU = 1280 // minimalMTU from wgengine/userspace.go
|
||||
|
||||
const (
|
||||
logPrefKey = "privatelogid"
|
||||
loginMethodPrefKey = "loginmethod"
|
||||
customLoginServerPrefKey = "customloginserver"
|
||||
exitNodePrefKey = "exitnode"
|
||||
exitAllowLANPrefKey = "exitallowlan"
|
||||
)
|
||||
|
||||
const (
|
||||
loginMethodGoogle = "google"
|
||||
loginMethodWeb = "web"
|
||||
)
|
||||
|
||||
// googleDNSServers are used on ChromeOS, where an empty VpnBuilder DNS setting results
|
||||
// in erasing the platform DNS servers. The developer docs say this is not supposed to happen,
|
||||
// but nonetheless it does.
|
||||
var googleDNSServers = []netip.Addr{
|
||||
netip.MustParseAddr("8.8.8.8"),
|
||||
netip.MustParseAddr("8.8.4.4"),
|
||||
netip.MustParseAddr("2001:4860:4860::8888"),
|
||||
netip.MustParseAddr("2001:4860:4860::8844"),
|
||||
}
|
||||
|
||||
// errVPNNotPrepared is used when VPNService.Builder.establish returns
|
||||
// null, either because the VPNService is not yet prepared or because
|
||||
// VPN status was revoked.
|
||||
var errVPNNotPrepared = errors.New("VPN service not prepared or was revoked")
|
||||
|
||||
// errMultipleUsers is used when we get a "INTERACT_ACROSS_USERS" error, which
|
||||
// happens due to a bug in Android. See:
|
||||
//
|
||||
// https://github.com/tailscale/tailscale/issues/2180
|
||||
var errMultipleUsers = errors.New("VPN cannot be created on this device due to an Android bug with multiple users")
|
||||
|
||||
func newBackend(dataDir string, jvm *jni.JVM, appCtx jni.Object, store *stateStore,
|
||||
settings settingsFunc) (*backend, error) {
|
||||
|
||||
sys := new(tsd.System)
|
||||
sys.Set(store)
|
||||
|
||||
logf := logger.RusagePrefixLog(log.Printf)
|
||||
b := &backend{
|
||||
jvm: jvm,
|
||||
devices: newTUNDevices(),
|
||||
settings: settings,
|
||||
appCtx: appCtx,
|
||||
}
|
||||
var logID logid.PrivateID
|
||||
logID.UnmarshalText([]byte("dead0000dead0000dead0000dead0000dead0000dead0000dead0000dead0000"))
|
||||
storedLogID, err := store.read(logPrefKey)
|
||||
// In all failure cases we ignore any errors and continue with the dead value above.
|
||||
if err != nil || storedLogID == nil {
|
||||
// Read failed or there was no previous log id.
|
||||
newLogID, err := logid.NewPrivateID()
|
||||
if err == nil {
|
||||
logID = newLogID
|
||||
enc, err := newLogID.MarshalText()
|
||||
if err == nil {
|
||||
store.write(logPrefKey, enc)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logID.UnmarshalText([]byte(storedLogID))
|
||||
}
|
||||
|
||||
netMon, err := netmon.New(logf)
|
||||
if err != nil {
|
||||
log.Printf("netmon.New: %w", err)
|
||||
}
|
||||
b.netMon = netMon
|
||||
b.SetupLogs(dataDir, logID, logf)
|
||||
dialer := new(tsdial.Dialer)
|
||||
cb := &router.CallbackRouter{
|
||||
SetBoth: b.setCfg,
|
||||
SplitDNS: false,
|
||||
GetBaseConfigFunc: b.getDNSBaseConfig,
|
||||
}
|
||||
engine, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Tun: b.devices,
|
||||
Router: cb,
|
||||
DNS: cb,
|
||||
Dialer: dialer,
|
||||
SetSubsystem: sys.Set,
|
||||
NetMon: b.netMon,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("runBackend: NewUserspaceEngine: %v", err)
|
||||
}
|
||||
sys.Set(engine)
|
||||
b.logIDPublic = logID.Public()
|
||||
ns, err := netstack.Create(logf, sys.Tun.Get(), engine, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("netstack.Create: %w", err)
|
||||
}
|
||||
sys.Set(ns)
|
||||
ns.ProcessLocalIPs = false // let Android kernel handle it; VpnBuilder sets this up
|
||||
ns.ProcessSubnets = true // for Android-being-an-exit-node support
|
||||
sys.NetstackRouter.Set(true)
|
||||
if w, ok := sys.Tun.GetOK(); ok {
|
||||
w.Start()
|
||||
}
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logID.Public(), sys, 0)
|
||||
if err != nil {
|
||||
engine.Close()
|
||||
return nil, fmt.Errorf("runBackend: NewLocalBackend: %v", err)
|
||||
}
|
||||
if err := ns.Start(lb); err != nil {
|
||||
return nil, fmt.Errorf("startNetstack: %w", err)
|
||||
}
|
||||
if b.logger != nil {
|
||||
lb.SetLogFlusher(b.logger.StartFlush)
|
||||
}
|
||||
b.engine = engine
|
||||
b.backend = lb
|
||||
b.sys = sys
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *backend) Start(notify func(n ipn.Notify)) error {
|
||||
b.backend.SetNotifyCallback(notify)
|
||||
return b.backend.Start(ipn.Options{})
|
||||
}
|
||||
|
||||
func (b *backend) NetworkChanged() {
|
||||
if b.sys != nil {
|
||||
if nm, ok := b.sys.NetMon.GetOK(); ok {
|
||||
nm.InjectEvent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) setCfg(rcfg *router.Config, dcfg *dns.OSConfig) error {
|
||||
return b.settings(rcfg, dcfg)
|
||||
}
|
||||
|
||||
func (b *backend) updateTUN(service jni.Object, rcfg *router.Config, dcfg *dns.OSConfig) error {
|
||||
if reflect.DeepEqual(rcfg, b.lastCfg) && reflect.DeepEqual(dcfg, b.lastDNSCfg) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close previous tunnel(s).
|
||||
// This is necessary for ChromeOS, native Android devices
|
||||
// seem to handle seamless handover between tunnels correctly.
|
||||
//
|
||||
// TODO(eliasnaur): If seamless handover becomes a desirable feature, skip
|
||||
// the closing on ChromeOS.
|
||||
b.CloseTUNs()
|
||||
|
||||
if len(rcfg.LocalAddrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
err := jni.Do(b.jvm, func(env *jni.Env) error {
|
||||
cls := jni.GetObjectClass(env, service)
|
||||
// Construct a VPNService.Builder. IPNService.newBuilder calls
|
||||
// setConfigureIntent, and allowFamily for both IPv4 and IPv6.
|
||||
m := jni.GetMethodID(env, cls, "newBuilder", "()Landroid/net/VpnService$Builder;")
|
||||
builder, err := jni.CallObjectMethod(env, service, m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("IPNService.newBuilder: %v", err)
|
||||
}
|
||||
bcls := jni.GetObjectClass(env, builder)
|
||||
|
||||
// builder.setMtu.
|
||||
setMtu := jni.GetMethodID(env, bcls, "setMtu", "(I)Landroid/net/VpnService$Builder;")
|
||||
const mtu = defaultMTU
|
||||
if _, err := jni.CallObjectMethod(env, builder, setMtu, jni.Value(mtu)); err != nil {
|
||||
return fmt.Errorf("VpnService.Builder.setMtu: %v", err)
|
||||
}
|
||||
|
||||
// builder.addDnsServer
|
||||
addDnsServer := jni.GetMethodID(env, bcls, "addDnsServer", "(Ljava/lang/String;)Landroid/net/VpnService$Builder;")
|
||||
// builder.addSearchDomain.
|
||||
addSearchDomain := jni.GetMethodID(env, bcls, "addSearchDomain", "(Ljava/lang/String;)Landroid/net/VpnService$Builder;")
|
||||
if dcfg != nil {
|
||||
nameservers := dcfg.Nameservers
|
||||
if b.avoidEmptyDNS && len(nameservers) == 0 {
|
||||
nameservers = googleDNSServers
|
||||
}
|
||||
for _, dns := range nameservers {
|
||||
_, err = jni.CallObjectMethod(env,
|
||||
builder,
|
||||
addDnsServer,
|
||||
jni.Value(jni.JavaString(env, dns.String())),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("VpnService.Builder.addDnsServer(%v): %v", dns, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, dom := range dcfg.SearchDomains {
|
||||
_, err = jni.CallObjectMethod(env,
|
||||
builder,
|
||||
addSearchDomain,
|
||||
jni.Value(jni.JavaString(env, dom.WithoutTrailingDot())),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("VpnService.Builder.addSearchDomain(%v): %v", dom, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// builder.addRoute.
|
||||
addRoute := jni.GetMethodID(env, bcls, "addRoute", "(Ljava/lang/String;I)Landroid/net/VpnService$Builder;")
|
||||
for _, route := range rcfg.Routes {
|
||||
// Normalize route address; Builder.addRoute does not accept non-zero masked bits.
|
||||
route = route.Masked()
|
||||
_, err = jni.CallObjectMethod(env,
|
||||
builder,
|
||||
addRoute,
|
||||
jni.Value(jni.JavaString(env, route.Addr().String())),
|
||||
jni.Value(route.Bits()),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("VpnService.Builder.addRoute(%v): %v", route, err)
|
||||
}
|
||||
}
|
||||
|
||||
// builder.addAddress.
|
||||
addAddress := jni.GetMethodID(env, bcls, "addAddress", "(Ljava/lang/String;I)Landroid/net/VpnService$Builder;")
|
||||
for _, addr := range rcfg.LocalAddrs {
|
||||
_, err = jni.CallObjectMethod(env,
|
||||
builder,
|
||||
addAddress,
|
||||
jni.Value(jni.JavaString(env, addr.Addr().String())),
|
||||
jni.Value(addr.Bits()),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("VpnService.Builder.addAddress(%v): %v", addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// builder.establish.
|
||||
establish := jni.GetMethodID(env, bcls, "establish", "()Landroid/os/ParcelFileDescriptor;")
|
||||
parcelFD, err := jni.CallObjectMethod(env, builder, establish)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "INTERACT_ACROSS_USERS") {
|
||||
return errMultipleUsers
|
||||
}
|
||||
return fmt.Errorf("VpnService.Builder.establish: %v", err)
|
||||
}
|
||||
if parcelFD == 0 {
|
||||
return errVPNNotPrepared
|
||||
}
|
||||
|
||||
// detachFd.
|
||||
parcelCls := jni.GetObjectClass(env, parcelFD)
|
||||
detachFd := jni.GetMethodID(env, parcelCls, "detachFd", "()I")
|
||||
tunFD, err := jni.CallIntMethod(env, parcelFD, detachFd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("detachFd: %v", err)
|
||||
}
|
||||
|
||||
// Create TUN device.
|
||||
tunDev, _, err := tun.CreateUnmonitoredTUNFromFD(int(tunFD))
|
||||
if err != nil {
|
||||
unix.Close(int(tunFD))
|
||||
return err
|
||||
}
|
||||
|
||||
b.devices.add(tunDev)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
b.lastCfg = nil
|
||||
b.CloseTUNs()
|
||||
return err
|
||||
}
|
||||
b.lastCfg = rcfg
|
||||
b.lastDNSCfg = dcfg
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseVPN closes any active TUN devices.
|
||||
func (b *backend) CloseTUNs() {
|
||||
b.lastCfg = nil
|
||||
b.devices.Shutdown()
|
||||
}
|
||||
|
||||
// SetupLogs sets up remote logging.
|
||||
func (b *backend) SetupLogs(logDir string, logID logid.PrivateID, logf logger.Logf) {
|
||||
if b.netMon == nil {
|
||||
panic("netMon must be created prior to SetupLogs")
|
||||
}
|
||||
transport := logpolicy.NewLogtailTransport(logtail.DefaultHost, b.netMon, log.Printf)
|
||||
|
||||
logcfg := logtail.Config{
|
||||
Collection: logtail.CollectionNode,
|
||||
PrivateID: logID,
|
||||
Stderr: log.Writer(),
|
||||
MetricsDelta: clientmetric.EncodeLogTailMetricsDelta,
|
||||
IncludeProcID: true,
|
||||
IncludeProcSequence: true,
|
||||
HTTPC: &http.Client{Transport: transport},
|
||||
CompressLogs: true,
|
||||
}
|
||||
logcfg.FlushDelayFn = func() time.Duration { return 2 * time.Minute }
|
||||
|
||||
filchOpts := filch.Options{
|
||||
ReplaceStderr: true,
|
||||
}
|
||||
|
||||
var filchErr error
|
||||
if logDir != "" {
|
||||
logPath := filepath.Join(logDir, "ipn.log.")
|
||||
logcfg.Buffer, filchErr = filch.New(logPath, filchOpts)
|
||||
}
|
||||
|
||||
b.logger = logtail.NewLogger(logcfg, logf)
|
||||
|
||||
log.SetFlags(0)
|
||||
log.SetOutput(b.logger)
|
||||
|
||||
log.Printf("goSetupLogs: success")
|
||||
|
||||
if logDir == "" {
|
||||
log.Printf("SetupLogs: no logDir, storing logs in memory")
|
||||
}
|
||||
if filchErr != nil {
|
||||
log.Printf("SetupLogs: filch setup failed: %v", filchErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) getPlatformDNSConfig() string {
|
||||
var baseConfig string
|
||||
err := jni.Do(b.jvm, func(env *jni.Env) error {
|
||||
cls := jni.GetObjectClass(env, b.appCtx)
|
||||
m := jni.GetMethodID(env, cls, "getDnsConfigObj", "()Lcom/tailscale/ipn/DnsConfig;")
|
||||
dns, err := jni.CallObjectMethod(env, b.appCtx, m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getDnsConfigObj: %v", err)
|
||||
}
|
||||
dnsCls := jni.GetObjectClass(env, dns)
|
||||
m = jni.GetMethodID(env, dnsCls, "getDnsConfigAsString", "()Ljava/lang/String;")
|
||||
n, err := jni.CallObjectMethod(env, dns, m)
|
||||
baseConfig = jni.GoString(env, jni.String(n))
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("getPlatformDNSConfig JNI: %v", err)
|
||||
return ""
|
||||
}
|
||||
return baseConfig
|
||||
}
|
||||
|
||||
func (b *backend) getDNSBaseConfig() (ret dns.OSConfig, _ error) {
|
||||
defer func() {
|
||||
// If we couldn't find any base nameservers, ultimately fall back to
|
||||
// Google's. Normally Tailscale doesn't ever pick a default nameserver
|
||||
// for users but in this case Android's APIs for reading the underlying
|
||||
// DNS config are lacking, and almost all Android phones use Google
|
||||
// services anyway, so it's a reasonable default: it's an ecosystem the
|
||||
// user has selected by having an Android device.
|
||||
if len(ret.Nameservers) == 0 && googleSignInEnabled() {
|
||||
log.Printf("getDNSBaseConfig: none found; falling back to Google public DNS")
|
||||
ret.Nameservers = append(ret.Nameservers, googleDNSServers...)
|
||||
}
|
||||
}()
|
||||
baseConfig := b.getPlatformDNSConfig()
|
||||
lines := strings.Split(baseConfig, "\n")
|
||||
if len(lines) == 0 {
|
||||
return dns.OSConfig{}, nil
|
||||
}
|
||||
|
||||
config := dns.OSConfig{}
|
||||
addrs := strings.Trim(lines[0], " \n")
|
||||
for _, addr := range strings.Split(addrs, " ") {
|
||||
ip, err := netip.ParseAddr(addr)
|
||||
if err == nil {
|
||||
config.Nameservers = append(config.Nameservers, ip)
|
||||
}
|
||||
}
|
||||
|
||||
if len(lines) > 1 {
|
||||
for _, s := range strings.Split(strings.Trim(lines[1], " \n"), " ") {
|
||||
domain, err := dnsname.ToFQDN(s)
|
||||
if err != nil {
|
||||
log.Printf("getDNSBaseConfig: unable to parse %q: %v", s, err)
|
||||
continue
|
||||
}
|
||||
config.SearchDomains = append(config.SearchDomains, domain)
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
@ -1,195 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
// JNI implementations of Java native callback methods.
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/tailscale/tailscale-android/cmd/jni"
|
||||
)
|
||||
|
||||
// #include <jni.h>
|
||||
import "C"
|
||||
|
||||
var (
|
||||
// onVPNPrepared is notified when VpnService.prepare succeeds.
|
||||
onVPNPrepared = make(chan struct{}, 1)
|
||||
// onVPNClosed is notified when VpnService.prepare fails, or when
|
||||
// the a running VPN connection is closed.
|
||||
onVPNClosed = make(chan struct{}, 1)
|
||||
// onVPNRevoked is notified whenever the VPN service is revoked.
|
||||
onVPNRevoked = make(chan struct{}, 1)
|
||||
|
||||
// onVPNRequested receives global IPNService references when
|
||||
// a VPN connection is requested.
|
||||
onVPNRequested = make(chan jni.Object)
|
||||
// onDisconnect receives global IPNService references when
|
||||
// disconnecting.
|
||||
onDisconnect = make(chan jni.Object)
|
||||
|
||||
// onGoogleToken receives google ID tokens.
|
||||
onGoogleToken = make(chan string)
|
||||
|
||||
// onFileShare receives file sharing intents.
|
||||
onFileShare = make(chan []File, 1)
|
||||
|
||||
// onWriteStorageGranted is notified when we are granted WRITE_STORAGE_PERMISSION.
|
||||
onWriteStorageGranted = make(chan struct{}, 1)
|
||||
|
||||
// onDNSConfigChanged is notified when the network changes and the DNS config needs to be updated.
|
||||
onDNSConfigChanged = make(chan struct{}, 1)
|
||||
)
|
||||
|
||||
const (
|
||||
// Request codes for Android callbacks.
|
||||
// requestSignin is for Google Sign-In.
|
||||
requestSignin C.jint = 1000 + iota
|
||||
// requestPrepareVPN is for when Android's VpnService.prepare
|
||||
// completes.
|
||||
requestPrepareVPN
|
||||
)
|
||||
|
||||
// resultOK is Android's Activity.RESULT_OK.
|
||||
const resultOK = -1
|
||||
|
||||
//export Java_com_tailscale_ipn_App_onVPNPrepared
|
||||
func Java_com_tailscale_ipn_App_onVPNPrepared(env *C.JNIEnv, class C.jclass) {
|
||||
notifyVPNPrepared()
|
||||
}
|
||||
|
||||
//export Java_com_tailscale_ipn_App_onWriteStorageGranted
|
||||
func Java_com_tailscale_ipn_App_onWriteStorageGranted(env *C.JNIEnv, class C.jclass) {
|
||||
select {
|
||||
case onWriteStorageGranted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func notifyVPNPrepared() {
|
||||
select {
|
||||
case onVPNPrepared <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func notifyVPNRevoked() {
|
||||
select {
|
||||
case onVPNRevoked <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func notifyVPNClosed() {
|
||||
select {
|
||||
case onVPNClosed <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
//export Java_com_tailscale_ipn_IPNService_requestVPN
|
||||
func Java_com_tailscale_ipn_IPNService_requestVPN(env *C.JNIEnv, this C.jobject) {
|
||||
jenv := (*jni.Env)(unsafe.Pointer(env))
|
||||
onVPNRequested <- jni.NewGlobalRef(jenv, jni.Object(this))
|
||||
}
|
||||
|
||||
//export Java_com_tailscale_ipn_IPNService_connect
|
||||
func Java_com_tailscale_ipn_IPNService_connect(env *C.JNIEnv, this C.jobject) {
|
||||
requestBackend(ConnectEvent{Enable: true})
|
||||
}
|
||||
|
||||
//export Java_com_tailscale_ipn_IPNService_disconnect
|
||||
func Java_com_tailscale_ipn_IPNService_disconnect(env *C.JNIEnv, this C.jobject) {
|
||||
jenv := (*jni.Env)(unsafe.Pointer(env))
|
||||
onDisconnect <- jni.NewGlobalRef(jenv, jni.Object(this))
|
||||
}
|
||||
|
||||
//export Java_com_tailscale_ipn_StartVPNWorker_connect
|
||||
func Java_com_tailscale_ipn_StartVPNWorker_connect(env *C.JNIEnv, this C.jobject) {
|
||||
requestBackend(ConnectEvent{Enable: true})
|
||||
}
|
||||
|
||||
//export Java_com_tailscale_ipn_StopVPNWorker_disconnect
|
||||
func Java_com_tailscale_ipn_StopVPNWorker_disconnect(env *C.JNIEnv, this C.jobject) {
|
||||
requestBackend(ConnectEvent{Enable: false})
|
||||
}
|
||||
|
||||
//export Java_com_tailscale_ipn_Peer_onActivityResult0
|
||||
func Java_com_tailscale_ipn_Peer_onActivityResult0(env *C.JNIEnv, cls C.jclass, act C.jobject, reqCode, resCode C.jint) {
|
||||
switch reqCode {
|
||||
case requestSignin:
|
||||
if resCode != resultOK {
|
||||
onGoogleToken <- ""
|
||||
break
|
||||
}
|
||||
jenv := (*jni.Env)(unsafe.Pointer(env))
|
||||
m := jni.GetStaticMethodID(jenv, googleClass,
|
||||
"getIdTokenForActivity", "(Landroid/app/Activity;)Ljava/lang/String;")
|
||||
idToken, err := jni.CallStaticObjectMethod(jenv, googleClass, m, jni.Value(act))
|
||||
if err != nil {
|
||||
fatalErr(err)
|
||||
break
|
||||
}
|
||||
tok := jni.GoString(jenv, jni.String(idToken))
|
||||
onGoogleToken <- tok
|
||||
case requestPrepareVPN:
|
||||
if resCode == resultOK {
|
||||
notifyVPNPrepared()
|
||||
} else {
|
||||
notifyVPNClosed()
|
||||
notifyVPNRevoked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//export Java_com_tailscale_ipn_App_onShareIntent
|
||||
func Java_com_tailscale_ipn_App_onShareIntent(env *C.JNIEnv, cls C.jclass, nfiles C.jint, jtypes C.jintArray, jmimes C.jobjectArray, jitems C.jobjectArray, jnames C.jobjectArray, jsizes C.jlongArray) {
|
||||
const (
|
||||
typeNone = 0
|
||||
typeInline = 1
|
||||
typeURI = 2
|
||||
)
|
||||
jenv := (*jni.Env)(unsafe.Pointer(env))
|
||||
types := jni.GetIntArrayElements(jenv, jni.IntArray(jtypes))
|
||||
mimes := jni.GetStringArrayElements(jenv, jni.ObjectArray(jmimes))
|
||||
items := jni.GetStringArrayElements(jenv, jni.ObjectArray(jitems))
|
||||
names := jni.GetStringArrayElements(jenv, jni.ObjectArray(jnames))
|
||||
sizes := jni.GetLongArrayElements(jenv, jni.LongArray(jsizes))
|
||||
var files []File
|
||||
for i := 0; i < int(nfiles); i++ {
|
||||
f := File{
|
||||
Type: FileType(types[i]),
|
||||
MIMEType: mimes[i],
|
||||
Name: names[i],
|
||||
}
|
||||
if f.Name == "" {
|
||||
f.Name = "file.bin"
|
||||
}
|
||||
switch f.Type {
|
||||
case FileTypeText:
|
||||
f.Text = items[i]
|
||||
f.Size = int64(len(f.Text))
|
||||
case FileTypeURI:
|
||||
f.URI = items[i]
|
||||
f.Size = sizes[i]
|
||||
default:
|
||||
panic("unknown file type")
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
select {
|
||||
case <-onFileShare:
|
||||
default:
|
||||
}
|
||||
onFileShare <- files
|
||||
}
|
||||
|
||||
//export Java_com_tailscale_ipn_App_onDnsConfigChanged
|
||||
func Java_com_tailscale_ipn_App_onDnsConfigChanged(env *C.JNIEnv, cls C.jclass) {
|
||||
select {
|
||||
case onDNSConfigChanged <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 15 KiB |
@ -1,282 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
)
|
||||
|
||||
// multiTUN implements a tun.Device that supports multiple
|
||||
// underlying devices. This is necessary because Android VPN devices
|
||||
// have static configurations and wgengine.NewUserspaceEngine
|
||||
// assumes a single static tun.Device.
|
||||
type multiTUN struct {
|
||||
// devices is for adding new devices.
|
||||
devices chan tun.Device
|
||||
// event is the combined event channel from all active devices.
|
||||
events chan tun.Event
|
||||
|
||||
close chan struct{}
|
||||
closeErr chan error
|
||||
|
||||
reads chan ioRequest
|
||||
writes chan ioRequest
|
||||
mtus chan chan mtuReply
|
||||
names chan chan nameReply
|
||||
shutdowns chan struct{}
|
||||
shutdownDone chan struct{}
|
||||
}
|
||||
|
||||
// tunDevice wraps and drives a single run.Device.
|
||||
type tunDevice struct {
|
||||
dev tun.Device
|
||||
// close closes the device.
|
||||
close chan struct{}
|
||||
closeDone chan error
|
||||
// readDone is notified when the read goroutine is done.
|
||||
readDone chan struct{}
|
||||
}
|
||||
|
||||
type ioRequest struct {
|
||||
data [][]byte
|
||||
sizes []int
|
||||
offset int
|
||||
reply chan<- ioReply
|
||||
}
|
||||
|
||||
type ioReply struct {
|
||||
count int
|
||||
err error
|
||||
}
|
||||
|
||||
type mtuReply struct {
|
||||
mtu int
|
||||
err error
|
||||
}
|
||||
|
||||
type nameReply struct {
|
||||
name string
|
||||
err error
|
||||
}
|
||||
|
||||
func newTUNDevices() *multiTUN {
|
||||
d := &multiTUN{
|
||||
devices: make(chan tun.Device),
|
||||
events: make(chan tun.Event),
|
||||
close: make(chan struct{}),
|
||||
closeErr: make(chan error),
|
||||
reads: make(chan ioRequest),
|
||||
writes: make(chan ioRequest),
|
||||
mtus: make(chan chan mtuReply),
|
||||
names: make(chan chan nameReply),
|
||||
shutdowns: make(chan struct{}),
|
||||
shutdownDone: make(chan struct{}),
|
||||
}
|
||||
go d.run()
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *multiTUN) run() {
|
||||
var devices []*tunDevice
|
||||
// readDone is the readDone channel of the device being read from.
|
||||
var readDone chan struct{}
|
||||
// runDone is the closeDone channel of the device being written to.
|
||||
var runDone chan error
|
||||
for {
|
||||
select {
|
||||
case <-readDone:
|
||||
// The oldest device has reached EOF, replace it.
|
||||
n := copy(devices, devices[1:])
|
||||
devices = devices[:n]
|
||||
if len(devices) > 0 {
|
||||
// Start reading from the next device.
|
||||
dev := devices[0]
|
||||
readDone = dev.readDone
|
||||
go d.readFrom(dev)
|
||||
}
|
||||
case <-runDone:
|
||||
// A device completed runDevice, replace it.
|
||||
if len(devices) > 0 {
|
||||
dev := devices[len(devices)-1]
|
||||
runDone = dev.closeDone
|
||||
go d.runDevice(dev)
|
||||
}
|
||||
case <-d.shutdowns:
|
||||
// Shut down all devices.
|
||||
for _, dev := range devices {
|
||||
close(dev.close)
|
||||
<-dev.closeDone
|
||||
<-dev.readDone
|
||||
}
|
||||
devices = nil
|
||||
d.shutdownDone <- struct{}{}
|
||||
case <-d.close:
|
||||
var derr error
|
||||
for _, dev := range devices {
|
||||
if err := <-dev.closeDone; err != nil {
|
||||
derr = err
|
||||
}
|
||||
}
|
||||
d.closeErr <- derr
|
||||
return
|
||||
case dev := <-d.devices:
|
||||
if len(devices) > 0 {
|
||||
// Ask the most recent device to stop.
|
||||
prev := devices[len(devices)-1]
|
||||
close(prev.close)
|
||||
}
|
||||
wrap := &tunDevice{
|
||||
dev: dev,
|
||||
close: make(chan struct{}),
|
||||
closeDone: make(chan error),
|
||||
readDone: make(chan struct{}, 1),
|
||||
}
|
||||
if len(devices) == 0 {
|
||||
// Start using this first device.
|
||||
readDone = wrap.readDone
|
||||
go d.readFrom(wrap)
|
||||
runDone = wrap.closeDone
|
||||
go d.runDevice(wrap)
|
||||
}
|
||||
devices = append(devices, wrap)
|
||||
case m := <-d.mtus:
|
||||
r := mtuReply{mtu: defaultMTU}
|
||||
if len(devices) > 0 {
|
||||
dev := devices[len(devices)-1]
|
||||
r.mtu, r.err = dev.dev.MTU()
|
||||
}
|
||||
m <- r
|
||||
case n := <-d.names:
|
||||
var r nameReply
|
||||
if len(devices) > 0 {
|
||||
dev := devices[len(devices)-1]
|
||||
r.name, r.err = dev.dev.Name()
|
||||
}
|
||||
n <- r
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *multiTUN) readFrom(dev *tunDevice) {
|
||||
defer func() {
|
||||
dev.readDone <- struct{}{}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case r := <-d.reads:
|
||||
n, err := dev.dev.Read(r.data, r.sizes, r.offset)
|
||||
stop := false
|
||||
if err != nil {
|
||||
select {
|
||||
case <-dev.close:
|
||||
stop = true
|
||||
err = nil
|
||||
default:
|
||||
}
|
||||
}
|
||||
r.reply <- ioReply{n, err}
|
||||
if stop {
|
||||
return
|
||||
}
|
||||
case <-d.close:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *multiTUN) runDevice(dev *tunDevice) {
|
||||
defer func() {
|
||||
// The documentation for https://developer.android.com/reference/android/net/VpnService.Builder#establish()
|
||||
// states that "Therefore, after draining the old file
|
||||
// descriptor...", but pending Reads are never unblocked
|
||||
// when a new descriptor is created.
|
||||
//
|
||||
// Close it instead and hope that no packets are lost.
|
||||
dev.closeDone <- dev.dev.Close()
|
||||
}()
|
||||
// Pump device events.
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case e := <-dev.dev.Events():
|
||||
d.events <- e
|
||||
case <-dev.close:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case w := <-d.writes:
|
||||
n, err := dev.dev.Write(w.data, w.offset)
|
||||
w.reply <- ioReply{n, err}
|
||||
case <-dev.close:
|
||||
// Device closed.
|
||||
return
|
||||
case <-d.close:
|
||||
// Multi-device closed.
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *multiTUN) add(dev tun.Device) {
|
||||
d.devices <- dev
|
||||
}
|
||||
|
||||
func (d *multiTUN) File() *os.File {
|
||||
// The underlying file descriptor is not constant on Android.
|
||||
// Let's hope no-one uses it.
|
||||
panic("not available on Android")
|
||||
}
|
||||
|
||||
func (d *multiTUN) Read(data [][]byte, sizes []int, offset int) (int, error) {
|
||||
r := make(chan ioReply)
|
||||
d.reads <- ioRequest{data, sizes, offset, r}
|
||||
rep := <-r
|
||||
return rep.count, rep.err
|
||||
}
|
||||
|
||||
func (d *multiTUN) Write(data [][]byte, offset int) (int, error) {
|
||||
r := make(chan ioReply)
|
||||
d.writes <- ioRequest{data, nil, offset, r}
|
||||
rep := <-r
|
||||
return rep.count, rep.err
|
||||
}
|
||||
|
||||
func (d *multiTUN) MTU() (int, error) {
|
||||
r := make(chan mtuReply)
|
||||
d.mtus <- r
|
||||
rep := <-r
|
||||
return rep.mtu, rep.err
|
||||
}
|
||||
|
||||
func (d *multiTUN) Name() (string, error) {
|
||||
r := make(chan nameReply)
|
||||
d.names <- r
|
||||
rep := <-r
|
||||
return rep.name, rep.err
|
||||
}
|
||||
|
||||
func (d *multiTUN) Events() <-chan tun.Event {
|
||||
return d.events
|
||||
}
|
||||
|
||||
func (d *multiTUN) Shutdown() {
|
||||
d.shutdowns <- struct{}{}
|
||||
<-d.shutdownDone
|
||||
}
|
||||
|
||||
func (d *multiTUN) Close() error {
|
||||
close(d.close)
|
||||
return <-d.closeErr
|
||||
}
|
||||
|
||||
func (d *multiTUN) BatchSize() int {
|
||||
// TODO(raggi): currently Android disallows the necessary ioctls to enable
|
||||
// batching. File a bug.
|
||||
return 1
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build pprof
|
||||
// +build pprof
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
)
|
||||
|
||||
func init() {
|
||||
go func() {
|
||||
http.ListenAndServe(":6060", nil)
|
||||
}()
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
|
||||
"github.com/tailscale/tailscale-android/cmd/jni"
|
||||
)
|
||||
|
||||
// stateStore is the Go interface for a persistent storage
|
||||
// backend by androidx.security.crypto.EncryptedSharedPreferences (see
|
||||
// App.java).
|
||||
type stateStore struct {
|
||||
jvm *jni.JVM
|
||||
// appCtx is the global Android app context.
|
||||
appCtx jni.Object
|
||||
|
||||
// Cached method ids on appCtx.
|
||||
encrypt jni.MethodID
|
||||
decrypt jni.MethodID
|
||||
}
|
||||
|
||||
func newStateStore(jvm *jni.JVM, appCtx jni.Object) *stateStore {
|
||||
s := &stateStore{
|
||||
jvm: jvm,
|
||||
appCtx: appCtx,
|
||||
}
|
||||
jni.Do(jvm, func(env *jni.Env) error {
|
||||
appCls := jni.GetObjectClass(env, appCtx)
|
||||
s.encrypt = jni.GetMethodID(
|
||||
env, appCls,
|
||||
"encryptToPref", "(Ljava/lang/String;Ljava/lang/String;)V",
|
||||
)
|
||||
s.decrypt = jni.GetMethodID(
|
||||
env, appCls,
|
||||
"decryptFromPref", "(Ljava/lang/String;)Ljava/lang/String;",
|
||||
)
|
||||
return nil
|
||||
})
|
||||
return s
|
||||
}
|
||||
|
||||
func prefKeyFor(id ipn.StateKey) string {
|
||||
return "statestore-" + string(id)
|
||||
}
|
||||
|
||||
func (s *stateStore) ReadString(key string, def string) (string, error) {
|
||||
data, err := s.read(key)
|
||||
if err != nil {
|
||||
return def, err
|
||||
}
|
||||
if data == nil {
|
||||
return def, nil
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (s *stateStore) WriteString(key string, val string) error {
|
||||
return s.write(key, []byte(val))
|
||||
}
|
||||
|
||||
func (s *stateStore) ReadBool(key string, def bool) (bool, error) {
|
||||
data, err := s.read(key)
|
||||
if err != nil {
|
||||
return def, err
|
||||
}
|
||||
if data == nil {
|
||||
return def, nil
|
||||
}
|
||||
return string(data) == "true", nil
|
||||
}
|
||||
|
||||
func (s *stateStore) WriteBool(key string, val bool) error {
|
||||
data := []byte("false")
|
||||
if val {
|
||||
data = []byte("true")
|
||||
}
|
||||
return s.write(key, data)
|
||||
}
|
||||
|
||||
func (s *stateStore) ReadState(id ipn.StateKey) ([]byte, error) {
|
||||
state, err := s.read(prefKeyFor(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if state == nil {
|
||||
return nil, ipn.ErrStateNotExist
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func (s *stateStore) WriteState(id ipn.StateKey, bs []byte) error {
|
||||
prefKey := prefKeyFor(id)
|
||||
return s.write(prefKey, bs)
|
||||
}
|
||||
|
||||
func (s *stateStore) read(key string) ([]byte, error) {
|
||||
var data []byte
|
||||
err := jni.Do(s.jvm, func(env *jni.Env) error {
|
||||
jfile := jni.JavaString(env, key)
|
||||
plain, err := jni.CallObjectMethod(env, s.appCtx, s.decrypt,
|
||||
jni.Value(jfile))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b64 := jni.GoString(env, jni.String(plain))
|
||||
if b64 == "" {
|
||||
return nil
|
||||
}
|
||||
data, err = base64.RawStdEncoding.DecodeString(b64)
|
||||
return err
|
||||
})
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (s *stateStore) write(key string, value []byte) error {
|
||||
bs64 := base64.RawStdEncoding.EncodeToString(value)
|
||||
err := jni.Do(s.jvm, func(env *jni.Env) error {
|
||||
jfile := jni.JavaString(env, key)
|
||||
jplain := jni.JavaString(env, bs64)
|
||||
err := jni.CallVoidMethod(env, s.appCtx, s.encrypt,
|
||||
jni.Value(jfile), jni.Value(jplain))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/tailscale/tailscale-android/cmd/jni"
|
||||
"tailscale.com/util/syspolicy"
|
||||
)
|
||||
|
||||
// androidHandler is a syspolicy handler for the Android version of the Tailscale client,
|
||||
// which lets the main networking code read values set via the Android RestrictionsManager.
|
||||
type androidHandler struct {
|
||||
a *App
|
||||
}
|
||||
|
||||
func (h androidHandler) ReadString(key string) (string, error) {
|
||||
if key == "" {
|
||||
return "", syspolicy.ErrNoSuchKey
|
||||
}
|
||||
retVal := ""
|
||||
err := jni.Do(h.a.jvm, func(env *jni.Env) error {
|
||||
cls := jni.GetObjectClass(env, h.a.appCtx)
|
||||
m := jni.GetMethodID(env, cls, "getSyspolicyStringValue", "(Ljava/lang/String;)Ljava/lang/String;")
|
||||
strObj, err := jni.CallObjectMethod(env, h.a.appCtx, m, jni.Value(jni.JavaString(env, key)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
retVal = jni.GoString(env, jni.String(strObj))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("syspolicy: failed to get string value via JNI: %v", err)
|
||||
}
|
||||
return retVal, err
|
||||
}
|
||||
|
||||
func (h androidHandler) ReadBoolean(key string) (bool, error) {
|
||||
if key == "" {
|
||||
return false, syspolicy.ErrNoSuchKey
|
||||
}
|
||||
retVal := false
|
||||
err := jni.Do(h.a.jvm, func(env *jni.Env) error {
|
||||
cls := jni.GetObjectClass(env, h.a.appCtx)
|
||||
m := jni.GetMethodID(env, cls, "getSyspolicyBooleanValue", "(Ljava/lang/String;)Z")
|
||||
b, err := jni.CallBooleanMethod(env, h.a.appCtx, m, jni.Value(jni.JavaString(env, key)))
|
||||
retVal = b
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("syspolicy: failed to get bool value via JNI: %v", err)
|
||||
}
|
||||
return retVal, err
|
||||
}
|
||||
|
||||
func (h androidHandler) ReadUInt64(key string) (uint64, error) {
|
||||
if key == "" {
|
||||
return 0, syspolicy.ErrNoSuchKey
|
||||
}
|
||||
// TODO(angott): drop ReadUInt64 everywhere. We are not using it.
|
||||
log.Fatalf("ReadUInt64 is not implemented on Android")
|
||||
return 0, nil
|
||||
}
|
Before Width: | Height: | Size: 24 KiB |
@ -1,11 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build tools
|
||||
// +build tools
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "gioui.org/cmd/gogio"
|
||||
)
|