From 6d9acbb479306e16462347d07639895410b160da Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Mon, 9 Nov 2020 16:34:45 +0100 Subject: [PATCH] cmd/tailscale,java: refactor Google Sign-In into separate class In preparation for the F-Droid release, refactor the non-free Google dependency into a separate Java class and make the Go client tolerate missing support. Signed-off-by: Elias Naur --- .../src/main/java/com/tailscale/ipn/App.java | 52 ++----------------- .../main/java/com/tailscale/ipn/Google.java | 43 +++++++++++++++ .../src/main/java/com/tailscale/ipn/Peer.java | 5 +- cmd/tailscale/callbacks.go | 49 ++++++++++++++--- cmd/tailscale/main.go | 45 +++++++++++++--- cmd/tailscale/ui.go | 6 +-- 6 files changed, 133 insertions(+), 67 deletions(-) create mode 100644 android/src/main/java/com/tailscale/ipn/Google.java diff --git a/android/src/main/java/com/tailscale/ipn/App.java b/android/src/main/java/com/tailscale/ipn/App.java index cbcb176..61c4d88 100644 --- a/android/src/main/java/com/tailscale/ipn/App.java +++ b/android/src/main/java/com/tailscale/ipn/App.java @@ -31,19 +31,11 @@ import java.security.GeneralSecurityException; import androidx.security.crypto.EncryptedSharedPreferences; import androidx.security.crypto.MasterKey; -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; - import androidx.browser.customtabs.CustomTabsIntent; import org.gioui.Gio; public class App extends Application { - private final static int REQUEST_SIGNIN = 1001; - private final static int REQUEST_PREPARE_VPN = 1002; - private final static String PEER_TAG = "peer"; private final static Handler mainHandler = new Handler(Looper.getMainLooper()); @@ -103,13 +95,6 @@ public class App extends Application { ); } - void googleSignOut() { - GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .build(); - GoogleSignInClient client = GoogleSignIn.getClient(this, gso); - client.signOut(); - } - void setTileStatus(boolean wantRunning) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { return; @@ -174,54 +159,24 @@ public class App extends Application { return getPackageManager().hasSystemFeature("android.hardware.type.pc"); } - void googleSignIn(Activity act, String serverOAuthID) { - 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(); - startActivityForResult(act, signInIntent, REQUEST_SIGNIN); - } - }); - } - - void prepareVPN(Activity act) { + 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, REQUEST_PREPARE_VPN); + startActivityForResult(act, intent, reqCode); } } }); } - private void startActivityForResult(Activity act, Intent intent, int request) { + static void startActivityForResult(Activity act, Intent intent, int request) { Fragment f = act.getFragmentManager().findFragmentByTag(PEER_TAG); f.startActivityForResult(intent, request); } - static void onActivityResult(Activity act, int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case App.REQUEST_SIGNIN: - if (resultCode == Activity.RESULT_OK) { - GoogleSignInAccount acc = GoogleSignIn.getLastSignedInAccount(act); - onSignin(acc.getIdToken()); - } else { - onSignin(null); - } - case App.REQUEST_PREPARE_VPN: - if (resultCode == Activity.RESULT_OK) { - onVPNPrepared(); - } - } - } - void showURL(Activity act, String url) { act.runOnUiThread(new Runnable() { @Override public void run() { @@ -234,7 +189,6 @@ public class App extends Application { }); } - static native void onSignin(String idToken); static native void onVPNPrepared(); private static native void onConnectivityChanged(boolean connected); } diff --git a/android/src/main/java/com/tailscale/ipn/Google.java b/android/src/main/java/com/tailscale/ipn/Google.java new file mode 100644 index 0000000..44f8226 --- /dev/null +++ b/android/src/main/java/com/tailscale/ipn/Google.java @@ -0,0 +1,43 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +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(); + } +} diff --git a/android/src/main/java/com/tailscale/ipn/Peer.java b/android/src/main/java/com/tailscale/ipn/Peer.java index 95a5586..22cf9c9 100644 --- a/android/src/main/java/com/tailscale/ipn/Peer.java +++ b/android/src/main/java/com/tailscale/ipn/Peer.java @@ -4,11 +4,14 @@ 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) { - App.onActivityResult(getActivity(), requestCode, resultCode, data); + onActivityResult0(getActivity(), requestCode, resultCode); } + + private static native void onActivityResult0(Activity act, int reqCode, int resCode); } diff --git a/cmd/tailscale/callbacks.go b/cmd/tailscale/callbacks.go index b1dcf57..b76ec2d 100644 --- a/cmd/tailscale/callbacks.go +++ b/cmd/tailscale/callbacks.go @@ -41,21 +41,30 @@ func init() { connected.Store(true) } +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) { + onVPNPrepared() +} + +func onVPNPrepared() { select { case vpnPrepared <- struct{}{}: default: } } -//export Java_com_tailscale_ipn_App_onSignin -func Java_com_tailscale_ipn_App_onSignin(env *C.JNIEnv, class C.jclass, idToken C.jstring) { - jenv := jni.EnvFor(uintptr(unsafe.Pointer(env))) - tok := jni.GoString(jenv, jni.String(idToken)) - onGoogleToken <- tok -} - //export Java_com_tailscale_ipn_IPNService_connect func Java_com_tailscale_ipn_IPNService_connect(env *C.JNIEnv, this C.jobject) { jenv := jni.EnvFor(uintptr(unsafe.Pointer(env))) @@ -81,3 +90,29 @@ func Java_com_tailscale_ipn_App_onConnectivityChanged(env *C.JNIEnv, cls C.jclas func Java_com_tailscale_ipn_QuickToggleService_onTileClick(env *C.JNIEnv, cls C.jclass) { requestBackend(ToggleEvent{}) } + +//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.EnvFor(uintptr(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 { + break + } + onVPNPrepared() + } +} diff --git a/cmd/tailscale/main.go b/cmd/tailscale/main.go index 6a260c9..803bd0a 100644 --- a/cmd/tailscale/main.go +++ b/cmd/tailscale/main.go @@ -27,9 +27,11 @@ import ( //go:generate go run github.com/go-bindata/go-bindata/go-bindata -nocompress -o logo.go tailscale.png google.png type App struct { - jvm jni.JVM + jvm jni.JVM + // appCtx is a global reference to the com.tailscale.ipn.App instance. appCtx jni.Object - store *stateStore + + store *stateStore // updates is notifies whenever netState or browseURL changes. updates chan struct{} @@ -52,6 +54,11 @@ type App struct { prefs *ipn.Prefs } +var ( + // googleClass is a global reference to the com.tailscale.ipn.Google class. + googleClass jni.Class +) + type clientState struct { browseURL string backend BackendState @@ -111,6 +118,19 @@ func main() { updates: make(chan struct{}, 1), vpnClosed: make(chan struct{}, 1), } + err := jni.Do(a.jvm, func(env jni.Env) error { + loader := jni.ClassLoaderFor(env, a.appCtx) + cl, err := jni.LoadClass(env, loader, "com.tailscale.ipn.Google") + if err != nil { + // Ignore load errors; the Google class is not included in F-Droid builds. + return nil + } + googleClass = jni.Class(jni.NewGlobalRef(env, jni.Object(cl))) + return nil + }) + if err != nil { + fatalErr(err) + } a.store = newStateStore(a.jvm, a.appCtx) go func() { if err := a.runBackend(); err != nil { @@ -369,6 +389,10 @@ func (a *App) modelName() string { return model } +func googleSignInEnabled() bool { + return googleClass != 0 +} + // updateNotification updates the foreground persistent status notification. func (a *App) updateNotification(service jni.Object, state ipn.State) error { var msg, title string @@ -652,7 +676,8 @@ func (a *App) updateState(act jni.Object, state *clientState) { } func (a *App) prepareVPN(act jni.Object) error { - return a.callVoidMethod(a.appCtx, "prepareVPN", "(Landroid/app/Activity;)V", jni.Value(act)) + return a.callVoidMethod(a.appCtx, "prepareVPN", "(Landroid/app/Activity;I)V", + jni.Value(act), jni.Value(requestPrepareVPN)) } func requestBackend(e UIEvent) { @@ -693,8 +718,13 @@ func (a *App) processUIEvents(w *app.Window, events []UIEvent, act jni.Object, s } func (a *App) signOut() { + if googleClass == 0 { + return + } err := jni.Do(a.jvm, func(env jni.Env) error { - return a.callVoidMethod(a.appCtx, "googleSignOut", "()V") + m := jni.GetStaticMethodID(env, googleClass, + "googleSignOut", "(Landroid/content/Context;)V") + return jni.CallStaticVoidMethod(env, googleClass, m, jni.Value(a.appCtx)) }) if err != nil { fatalErr(err) @@ -702,12 +732,15 @@ func (a *App) signOut() { } func (a *App) googleSignIn(act jni.Object) { - if act == 0 { + if act == 0 || googleClass == 0 { return } err := jni.Do(a.jvm, func(env jni.Env) error { sid := jni.JavaString(env, serverOAuthID) - return a.callVoidMethod(a.appCtx, "googleSignIn", "(Landroid/app/Activity;Ljava/lang/String;)V", jni.Value(act), jni.Value(sid)) + m := jni.GetStaticMethodID(env, googleClass, + "googleSignIn", "(Landroid/app/Activity;Ljava/lang/String;I)V") + return jni.CallStaticVoidMethod(env, googleClass, m, + jni.Value(act), jni.Value(sid), jni.Value(requestSignin)) }) if err != nil { fatalErr(err) diff --git a/cmd/tailscale/ui.go b/cmd/tailscale/ui.go index 2fdec5c..a46091a 100644 --- a/cmd/tailscale/ui.go +++ b/cmd/tailscale/ui.go @@ -33,8 +33,6 @@ import ( _ "image/png" ) -const enableGoogleSignin = true - type UI struct { theme *material.Theme store *stateStore @@ -339,7 +337,7 @@ func (ui *UI) layoutSignIn(gtx layout.Context, state *BackendState) layout.Dimen border := widget.Border{Color: rgb(textColor), CornerRadius: unit.Dp(4), Width: unit.Px(1)} return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx, layout.Rigid(func(gtx C) D { - if !enableGoogleSignin { + if !googleSignInEnabled() { return D{} } return layout.Inset{Bottom: unit.Dp(16)}.Layout(gtx, func(gtx C) D { @@ -374,7 +372,7 @@ func (ui *UI) layoutSignIn(gtx layout.Context, state *BackendState) layout.Dimen }), layout.Rigid(func(gtx C) D { label := "Sign in with other" - if !enableGoogleSignin { + if !googleSignInEnabled() { label = "Sign in" } return ui.withLoader(gtx, ui.signinType == webSignin, func(gtx C) D {