android: switch to using gomobile
gomobile replaces our custom JNI bindings Updates tailscale/corp#18202 Signed-off-by: Percy Wegmann <percy@tailscale.com>pull/220/head
parent
98a72c2963
commit
5e7e36e3bc
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package com.tailscale.ipn;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
public class MaybeGoogle {
|
||||||
|
static boolean isGoogle() {
|
||||||
|
return getGoogle() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getIdTokenForActivity(Activity act) {
|
||||||
|
Class<?> google = getGoogle();
|
||||||
|
if (google == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Method method = google.getMethod("getIdTokenForActivity", Activity.class);
|
||||||
|
return (String) method.invoke(null, act);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Class getGoogle() {
|
||||||
|
try {
|
||||||
|
return Class.forName("com.tailscale.ipn.Google");
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package com.tailscale.ipn
|
||||||
|
|
||||||
|
import android.net.VpnService
|
||||||
|
import libtailscale.ParcelFileDescriptor
|
||||||
|
|
||||||
|
class VPNServiceBuilder(private val builder: VpnService.Builder) : libtailscale.VPNServiceBuilder {
|
||||||
|
override fun addAddress(p0: String, p1: Int) {
|
||||||
|
builder.addAddress(p0, p1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addDNSServer(p0: String) {
|
||||||
|
builder.addDnsServer(p0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addRoute(p0: String, p1: Int) {
|
||||||
|
builder.addRoute(p0, p1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addSearchDomain(p0: String) {
|
||||||
|
builder.addSearchDomain(p0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun establish(): ParcelFileDescriptor? {
|
||||||
|
return builder.establish()?.let { ParcelFileDescriptor(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMTU(p0: Long) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ParcelFileDescriptor(private val fd: android.os.ParcelFileDescriptor) :
|
||||||
|
libtailscale.ParcelFileDescriptor {
|
||||||
|
override fun detach(): Int {
|
||||||
|
return fd.detachFd()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package com.tailscale.ipn.ui.util
|
||||||
|
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class InputStreamAdapter(private val inputStream: InputStream) : libtailscale.InputStream {
|
||||||
|
override fun read(): ByteArray? {
|
||||||
|
val b = ByteArray(4096)
|
||||||
|
val i = inputStream.read(b)
|
||||||
|
if (i == -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return b.sliceArray(0..i)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
inputStream.close()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,129 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package libtailscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 IPNService)
|
||||||
|
// onDisconnect receives global IPNService references when
|
||||||
|
// disconnecting.
|
||||||
|
onDisconnect = make(chan IPNService)
|
||||||
|
|
||||||
|
// onGoogleToken receives google ID tokens.
|
||||||
|
onGoogleToken = make(chan string)
|
||||||
|
|
||||||
|
// 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 = 1000 + iota
|
||||||
|
// requestPrepareVPN is for when Android's VpnService.prepare
|
||||||
|
// completes.
|
||||||
|
requestPrepareVPN
|
||||||
|
)
|
||||||
|
|
||||||
|
// resultOK is Android's Activity.RESULT_OK.
|
||||||
|
const resultOK = -1
|
||||||
|
|
||||||
|
func OnShareIntent(nfiles int32, types []int32, mimes []string, items []string, names []string, sizes []int) {
|
||||||
|
// TODO(oxtoacart): actually implement this
|
||||||
|
// const (
|
||||||
|
// typeNone = 0
|
||||||
|
// typeInline = 1
|
||||||
|
// typeURI = 2
|
||||||
|
// )
|
||||||
|
// jenv := (*jni.Env)(unsafe.Pointer(env))
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func OnDnsConfigChanged() {
|
||||||
|
select {
|
||||||
|
case onDNSConfigChanged <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export Java_com_tailscale_ipn_App_onWriteStorageGranted
|
||||||
|
func OnWriteStorageGranted() {
|
||||||
|
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:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var android struct {
|
||||||
|
// mu protects all fields of this structure. However, once a
|
||||||
|
// non-nil jvm is returned from javaVM, all the other fields may
|
||||||
|
// be accessed unlocked.
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
// appCtx is the global Android App context.
|
||||||
|
appCtx AppContext
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package libtailscale
|
||||||
|
|
||||||
|
import _ "golang.org/x/mobile/bind"
|
||||||
|
|
||||||
|
// Start starts the application, storing state in the given dataDir and using
|
||||||
|
// the given appCtx.
|
||||||
|
func Start(dataDir string, appCtx AppContext) Application {
|
||||||
|
return start(dataDir, appCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppContext provides a context within which the Application is running. This
|
||||||
|
// context is a hook into functionality that's implemented on the Java side.
|
||||||
|
type AppContext interface {
|
||||||
|
// Log logs the given tag and logLine
|
||||||
|
Log(tag, logLine string)
|
||||||
|
|
||||||
|
// EncryptToPref stores the given value to an encrypted preference at the
|
||||||
|
// given key.
|
||||||
|
EncryptToPref(key, value string) error
|
||||||
|
|
||||||
|
// DecryptFromPref retrieves the given value from an encrypted preference
|
||||||
|
// at the given key, or returns empty string if unset.
|
||||||
|
DecryptFromPref(key string) (string, error)
|
||||||
|
|
||||||
|
// GetOSVersion gets the Android version.
|
||||||
|
GetOSVersion() (string, error)
|
||||||
|
|
||||||
|
// GetModelName gets the Android device's model name.
|
||||||
|
GetModelName() (string, error)
|
||||||
|
|
||||||
|
// IsPlayVersion reports whether this is the Google Play version of the app
|
||||||
|
// (as opposed to F-droid/sideloaded).
|
||||||
|
IsPlayVersion() bool
|
||||||
|
|
||||||
|
// IsChromeOS reports whether we're on a ChromeOS device.
|
||||||
|
IsChromeOS() (bool, error)
|
||||||
|
|
||||||
|
// GetInterfacesAsString gets a string representation of all network
|
||||||
|
// interfaces.
|
||||||
|
GetInterfacesAsString() (string, error)
|
||||||
|
|
||||||
|
// GetPlatformDNSConfig gets a string representation of the current DNS
|
||||||
|
// configuration.
|
||||||
|
GetPlatformDNSConfig() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPNService corresponds to our IPNService in Java.
|
||||||
|
type IPNService interface {
|
||||||
|
// ID returns the unique ID of this instance of the IPNService. Every time
|
||||||
|
// we start a new IPN service, it should have a new ID.
|
||||||
|
ID() string
|
||||||
|
|
||||||
|
// Protect protects socket identified by the given file descriptor from
|
||||||
|
// being captured by the VPN. The return value indicates whether or not the
|
||||||
|
// socket was successfully protected.
|
||||||
|
Protect(fd int32) bool
|
||||||
|
|
||||||
|
// NewBuilder creates a new VPNServiceBuilder in preparation for starting
|
||||||
|
// the Android VPN.
|
||||||
|
NewBuilder() VPNServiceBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
// VPNServiceBuilder corresponds to Android's VpnService.Builder.
|
||||||
|
type VPNServiceBuilder interface {
|
||||||
|
SetMTU(int) error
|
||||||
|
AddDNSServer(string) error
|
||||||
|
AddSearchDomain(string) error
|
||||||
|
AddRoute(string, int32) error
|
||||||
|
AddAddress(string, int32) error
|
||||||
|
Establish() (ParcelFileDescriptor, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParcelFileDescriptor corresponds to Android's ParcelFileDescriptor.
|
||||||
|
type ParcelFileDescriptor interface {
|
||||||
|
Detach() (int32, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application encapsulates the running Tailscale Application. There is only a
|
||||||
|
// single instance of Application per Android application.
|
||||||
|
type Application interface {
|
||||||
|
// CallLocalAPI provides a mechanism for calling Tailscale's HTTP localapi
|
||||||
|
// without having to call over the network.
|
||||||
|
CallLocalAPI(timeoutMillis int, method, endpoint string, body InputStream) (LocalAPIResponse, error)
|
||||||
|
|
||||||
|
// WatchNotifications provides a mechanism for subscribing to ipn.Notify
|
||||||
|
// updates. The given NotificationCallback's OnNotify function is invoked
|
||||||
|
// on every new ipn.Notify message. The returned NotificationManager
|
||||||
|
// allows the watcher to stop watching notifications.
|
||||||
|
WatchNotifications(mask int, cb NotificationCallback) NotificationManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalAPIResponse is a response to a localapi call, analogous to an http.Response.
|
||||||
|
type LocalAPIResponse interface {
|
||||||
|
StatusCode() int
|
||||||
|
BodyBytes() ([]byte, error)
|
||||||
|
BodyInputStream() InputStream
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationCallback is callback for receiving ipn.Notify messages.
|
||||||
|
type NotificationCallback interface {
|
||||||
|
OnNotify([]byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationManager provides a mechanism for a notification watcher to stop
|
||||||
|
// watching notifications.
|
||||||
|
type NotificationManager interface {
|
||||||
|
Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputStream provides an adapter between Java's InputStream and Go's
|
||||||
|
// io.Reader.
|
||||||
|
type InputStream interface {
|
||||||
|
Read() ([]byte, error)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// The below are global callbacks that allow the Java application to notify Go
|
||||||
|
// of various state changes.
|
||||||
|
|
||||||
|
func OnVPNPrepared() {
|
||||||
|
notifyVPNPrepared()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequestVPN(service IPNService) {
|
||||||
|
onVPNRequested <- service
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServiceDisconnect(service IPNService) {
|
||||||
|
onDisconnect <- service
|
||||||
|
}
|
||||||
|
|
||||||
|
func OnActivityResult(reqCode, resCode int, idToken string) {
|
||||||
|
switch reqCode {
|
||||||
|
case requestSignin:
|
||||||
|
if resCode != resultOK {
|
||||||
|
onGoogleToken <- ""
|
||||||
|
break
|
||||||
|
}
|
||||||
|
onGoogleToken <- idToken
|
||||||
|
case requestPrepareVPN:
|
||||||
|
if resCode == resultOK {
|
||||||
|
notifyVPNPrepared()
|
||||||
|
} else {
|
||||||
|
notifyVPNClosed()
|
||||||
|
notifyVPNRevoked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,149 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package libtailscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"runtime/debug"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CallLocalAPI 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 (app *App) CallLocalAPI(timeoutMillis int, method, endpoint string, body InputStream) (LocalAPIResponse, error) {
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
log.Printf("panic in CallLocalAPI %s: %s", p, debug.Stack())
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
app.ready.Wait()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(uint64(timeoutMillis)*uint64(time.Millisecond)))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if body != nil {
|
||||||
|
defer body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, endpoint, adaptInputStream(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() {
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
log.Printf("panic in CallLocalAPI.ServeHTTP %s: %s", p, debug.Stack())
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
app.localAPIHandler.ServeHTTP(resp, req)
|
||||||
|
resp.Flush()
|
||||||
|
pipeWriter.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-resp.startWritingBody:
|
||||||
|
return resp, nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, fmt.Errorf("timeout for %s", endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) BodyBytes() ([]byte, error) {
|
||||||
|
return io.ReadAll(r.bodyReader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) BodyInputStream() InputStream {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) StatusCode() int {
|
||||||
|
return r.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Flush() {
|
||||||
|
r.startWritingBodyOnce.Do(func() {
|
||||||
|
close(r.startWritingBody)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func adaptInputStream(in InputStream) io.Reader {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r, w := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
defer w.Close()
|
||||||
|
for {
|
||||||
|
b, err := in.Read()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error reading from inputstream: %s", err)
|
||||||
|
}
|
||||||
|
if b == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(b)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return r
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package libtailscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) WatchNotifications(mask int, cb NotificationCallback) NotificationManager {
|
||||||
|
app.ready.Wait()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
go app.backend.WatchNotifications(ctx, ipn.NotifyWatchOpt(mask), func() {}, func(notify *ipn.Notify) bool {
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
log.Printf("panic in WatchNotifications %s: %s", p, debug.Stack())
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
b, err := json.Marshal(notify)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error: WatchNotifications: marshal notify: %s", err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
err = cb.OnNotify(b)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error: WatchNotifications: OnNotify: %s", err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return ¬ificationManager{cancel}
|
||||||
|
}
|
||||||
|
|
||||||
|
type notificationManager struct {
|
||||||
|
cancel func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *notificationManager) Stop() {
|
||||||
|
nm.cancel()
|
||||||
|
}
|
@ -1,506 +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 jnipkg
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
static jint jni_GetJavaVM(JNIEnv *env, JavaVM **jvm) {
|
|
||||||
return (*env)->GetJavaVM(env, jvm);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetArrayLength(e *Env, jarr ByteArray) int {
|
|
||||||
size := C.jni_GetArrayLength(env(e), C.jarray(jarr))
|
|
||||||
return int(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetJavaVM(e *Env) (*JVM, error) {
|
|
||||||
var jvm *C.JavaVM
|
|
||||||
result := C.jni_GetJavaVM(env(e), &jvm)
|
|
||||||
if result != C.JNI_OK {
|
|
||||||
return nil, errors.New("failed to get JavaVM")
|
|
||||||
}
|
|
||||||
return (*JVM)(jvm), nil
|
|
||||||
}
|
|
@ -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,202 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
package localapiservice
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
jnipkg "github.com/tailscale/tailscale-android/pkg/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 jnipkg.Class
|
|
||||||
|
|
||||||
// notifierClass is a global reference to the com.tailscale.ipn.ui.notifier.Notifier class.
|
|
||||||
notifierClass jnipkg.Class
|
|
||||||
|
|
||||||
// Typically a shared LocalAPIService instance.
|
|
||||||
service *LocalAPIService
|
|
||||||
|
|
||||||
backend *ipnlocal.LocalBackend
|
|
||||||
|
|
||||||
busWatchers map[string]func()
|
|
||||||
|
|
||||||
jvm *jnipkg.JVM
|
|
||||||
}
|
|
||||||
|
|
||||||
//export Java_com_tailscale_ipn_ui_localapi_LocalApiClient_doRequest
|
|
||||||
func Java_com_tailscale_ipn_ui_localapi_LocalApiClient_doRequest(
|
|
||||||
env *C.JNIEnv,
|
|
||||||
cls C.jclass,
|
|
||||||
jpath C.jstring,
|
|
||||||
jmethod C.jstring,
|
|
||||||
jbody C.jbyteArray,
|
|
||||||
jcookie C.jstring) {
|
|
||||||
|
|
||||||
jenv := (*jnipkg.Env)(unsafe.Pointer(env))
|
|
||||||
|
|
||||||
// The API Path
|
|
||||||
pathRef := jnipkg.NewGlobalRef(jenv, jnipkg.Object(jpath))
|
|
||||||
pathStr := jnipkg.GoString(jenv, jnipkg.String(pathRef))
|
|
||||||
defer jnipkg.DeleteGlobalRef(jenv, pathRef)
|
|
||||||
|
|
||||||
// The HTTP verb
|
|
||||||
methodRef := jnipkg.NewGlobalRef(jenv, jnipkg.Object(jmethod))
|
|
||||||
methodStr := jnipkg.GoString(jenv, jnipkg.String(methodRef))
|
|
||||||
defer jnipkg.DeleteGlobalRef(jenv, methodRef)
|
|
||||||
|
|
||||||
// The body string. This is optional and may be empty.
|
|
||||||
bodyRef := jnipkg.NewGlobalRef(jenv, jnipkg.Object(jbody))
|
|
||||||
bodyArray := jnipkg.GetByteArrayElements(jenv, jnipkg.ByteArray(bodyRef))
|
|
||||||
defer jnipkg.DeleteGlobalRef(jenv, bodyRef)
|
|
||||||
|
|
||||||
resp := doLocalAPIRequest(pathStr, methodStr, bodyArray)
|
|
||||||
|
|
||||||
jrespBody := jnipkg.NewByteArray(jenv, resp)
|
|
||||||
respBody := jnipkg.Value(jrespBody)
|
|
||||||
cookie := jnipkg.Value(jcookie)
|
|
||||||
onResponse := jnipkg.GetMethodID(jenv, shim.clientClass, "onResponse", "([BLjava/lang/String;)V")
|
|
||||||
|
|
||||||
jnipkg.CallVoidMethod(jenv, jnipkg.Object(cls), onResponse, respBody, cookie)
|
|
||||||
}
|
|
||||||
|
|
||||||
func doLocalAPIRequest(path string, method string, body []byte) []byte {
|
|
||||||
if shim.service == nil {
|
|
||||||
return []byte("{\"error\":\"Not Ready\"}")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*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)
|
|
||||||
defer r.Body().Close()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return []byte("{\"error\":\"" + err.Error() + "\"}")
|
|
||||||
}
|
|
||||||
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 *jnipkg.JVM, appCtx jnipkg.Object, s *LocalAPIService, b *ipnlocal.LocalBackend) {
|
|
||||||
shim.busWatchers = make(map[string]func())
|
|
||||||
shim.service = s
|
|
||||||
shim.backend = b
|
|
||||||
|
|
||||||
configureLocalApiJNIHandler(jvm, appCtx)
|
|
||||||
|
|
||||||
// Let the Kotlin side know we're ready to handle requests.
|
|
||||||
jnipkg.Do(jvm, func(env *jnipkg.Env) error {
|
|
||||||
onReadyAPI := jnipkg.GetStaticMethodID(env, shim.clientClass, "onReady", "()V")
|
|
||||||
jnipkg.CallStaticVoidMethod(env, shim.clientClass, onReadyAPI)
|
|
||||||
|
|
||||||
onNotifyNot := jnipkg.GetStaticMethodID(env, shim.notifierClass, "onReady", "()V")
|
|
||||||
jnipkg.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 *jnipkg.JVM, appCtx jnipkg.Object) error {
|
|
||||||
shim.jvm = jvm
|
|
||||||
|
|
||||||
return jnipkg.Do(jvm, func(env *jnipkg.Env) error {
|
|
||||||
loader := jnipkg.ClassLoaderFor(env, appCtx)
|
|
||||||
cl, err := jnipkg.LoadClass(env, loader, "com.tailscale.ipn.ui.localapi.LocalApiClient")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
shim.clientClass = jnipkg.Class(jnipkg.NewGlobalRef(env, jnipkg.Object(cl)))
|
|
||||||
|
|
||||||
cl, err = jnipkg.LoadClass(env, loader, "com.tailscale.ipn.ui.notifier.Notifier")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
shim.notifierClass = jnipkg.Class(jnipkg.NewGlobalRef(env, jnipkg.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,
|
|
||||||
jsessionId C.jstring) {
|
|
||||||
|
|
||||||
jenv := (*jnipkg.Env)(unsafe.Pointer(env))
|
|
||||||
|
|
||||||
sessionIdRef := jnipkg.NewGlobalRef(jenv, jnipkg.Object(jsessionId))
|
|
||||||
sessionId := jnipkg.GoString(jenv, jnipkg.String(sessionIdRef))
|
|
||||||
defer jnipkg.DeleteGlobalRef(jenv, sessionIdRef)
|
|
||||||
|
|
||||||
cancel := shim.busWatchers[sessionId]
|
|
||||||
if cancel != nil {
|
|
||||||
log.Printf("Deregistering app layer bus watcher with sessionid: %s", sessionId)
|
|
||||||
cancel()
|
|
||||||
delete(shim.busWatchers, sessionId)
|
|
||||||
} else {
|
|
||||||
log.Printf("Error: Could not find bus watcher with sessionid: %s", sessionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//export Java_com_tailscale_ipn_ui_notifier_Notifier_startIPNBusWatcher
|
|
||||||
func Java_com_tailscale_ipn_ui_notifier_Notifier_startIPNBusWatcher(
|
|
||||||
env *C.JNIEnv,
|
|
||||||
cls C.jclass,
|
|
||||||
jsessionId C.jstring,
|
|
||||||
jmask C.jint) {
|
|
||||||
|
|
||||||
jenv := (*jnipkg.Env)(unsafe.Pointer(env))
|
|
||||||
|
|
||||||
sessionIdRef := jnipkg.NewGlobalRef(jenv, jnipkg.Object(jsessionId))
|
|
||||||
sessionId := jnipkg.GoString(jenv, jnipkg.String(sessionIdRef))
|
|
||||||
defer jnipkg.DeleteGlobalRef(jenv, sessionIdRef)
|
|
||||||
|
|
||||||
log.Printf("Registering app layer bus watcher with sessionid: %s", sessionId)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
shim.busWatchers[sessionId] = 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
|
|
||||||
}
|
|
||||||
jnipkg.Do(shim.jvm, func(env *jnipkg.Env) error {
|
|
||||||
jjson := jnipkg.JavaString(env, string(js))
|
|
||||||
onNotify := jnipkg.GetMethodID(env, shim.notifierClass, "onNotify", "(Ljava/lang/String;Ljava/lang/String;)V")
|
|
||||||
jnipkg.CallVoidMethod(env, jnipkg.Object(cls), onNotify, jnipkg.Value(jjson), jnipkg.Value(jsessionId))
|
|
||||||
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,85 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
jnipkg "github.com/tailscale/tailscale-android/pkg/jni"
|
|
||||||
"github.com/tailscale/tailscale-android/pkg/localapiservice"
|
|
||||||
"tailscale.com/ipn/ipnlocal"
|
|
||||||
"tailscale.com/types/logid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// #include <jni.h>
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
type App struct {
|
|
||||||
jvm *jnipkg.JVM
|
|
||||||
// appCtx is a global reference to the com.tailscale.ipn.App instance.
|
|
||||||
appCtx jnipkg.Object
|
|
||||||
|
|
||||||
store *stateStore
|
|
||||||
logIDPublicAtomic atomic.Pointer[logid.PublicID]
|
|
||||||
|
|
||||||
localAPI *localapiservice.LocalAPIService
|
|
||||||
backend *ipnlocal.LocalBackend
|
|
||||||
}
|
|
||||||
|
|
||||||
var android struct {
|
|
||||||
// mu protects all fields of this structure. However, once a
|
|
||||||
// non-nil jvm is returned from javaVM, all the other fields may
|
|
||||||
// be accessed unlocked.
|
|
||||||
mu sync.Mutex
|
|
||||||
jvm *jnipkg.JVM
|
|
||||||
|
|
||||||
// appCtx is the global Android App context.
|
|
||||||
appCtx C.jobject
|
|
||||||
}
|
|
||||||
|
|
||||||
func initJVM(env *C.JNIEnv, ctx C.jobject) {
|
|
||||||
android.mu.Lock()
|
|
||||||
defer android.mu.Unlock()
|
|
||||||
jenv := (*jnipkg.Env)(unsafe.Pointer(env))
|
|
||||||
res, err := jnipkg.GetJavaVM(jenv)
|
|
||||||
if err != nil {
|
|
||||||
panic("eror: GetJavaVM failed")
|
|
||||||
}
|
|
||||||
android.jvm = res
|
|
||||||
android.appCtx = C.jobject(jnipkg.NewGlobalRef(jenv, jnipkg.Object(ctx)))
|
|
||||||
}
|
|
||||||
|
|
||||||
//export Java_com_tailscale_ipn_App_initBackend
|
|
||||||
func Java_com_tailscale_ipn_App_initBackend(env *C.JNIEnv, class C.jclass, jdataDir C.jbyteArray, context C.jobject) {
|
|
||||||
initJVM(env, context)
|
|
||||||
jenv := (*jnipkg.Env)(unsafe.Pointer(env))
|
|
||||||
dirBytes := jnipkg.GetByteArrayElements(jenv, jnipkg.ByteArray(jdataDir))
|
|
||||||
if dirBytes == nil {
|
|
||||||
panic("runGoMain: GetByteArrayElements failed")
|
|
||||||
}
|
|
||||||
n := jnipkg.GetArrayLength(jenv, jnipkg.ByteArray(jdataDir))
|
|
||||||
dataDir := C.GoStringN((*C.char)(unsafe.Pointer(&dirBytes[0])), C.int(n))
|
|
||||||
|
|
||||||
// Set XDG_CACHE_HOME to make os.UserCacheDir work.
|
|
||||||
if _, exists := os.LookupEnv("XDG_CACHE_HOME"); !exists {
|
|
||||||
cachePath := filepath.Join(dataDir, "cache")
|
|
||||||
os.Setenv("XDG_CACHE_HOME", cachePath)
|
|
||||||
}
|
|
||||||
// Set XDG_CONFIG_HOME to make os.UserConfigDir work.
|
|
||||||
if _, exists := os.LookupEnv("XDG_CONFIG_HOME"); !exists {
|
|
||||||
cfgPath := filepath.Join(dataDir, "config")
|
|
||||||
os.Setenv("XDG_CONFIG_HOME", cfgPath)
|
|
||||||
}
|
|
||||||
// Set HOME to make os.UserHomeDir work.
|
|
||||||
if _, exists := os.LookupEnv("HOME"); !exists {
|
|
||||||
os.Setenv("HOME", dataDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
dataDirChan <- dataDir
|
|
||||||
main()
|
|
||||||
}
|
|
@ -1,151 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
jnipkg "github.com/tailscale/tailscale-android/pkg/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 jnipkg.Object)
|
|
||||||
// onDisconnect receives global IPNService references when
|
|
||||||
// disconnecting.
|
|
||||||
onDisconnect = make(chan jnipkg.Object)
|
|
||||||
|
|
||||||
onConnect = make(chan ConnectEvent)
|
|
||||||
|
|
||||||
// onGoogleToken receives google ID tokens.
|
|
||||||
onGoogleToken = make(chan string)
|
|
||||||
|
|
||||||
// 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_IPNService_requestVPN
|
|
||||||
func Java_com_tailscale_ipn_IPNService_requestVPN(env *C.JNIEnv, this C.jobject) {
|
|
||||||
jenv := (*jnipkg.Env)(unsafe.Pointer(env))
|
|
||||||
onVPNRequested <- jnipkg.NewGlobalRef(jenv, jnipkg.Object(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
//export Java_com_tailscale_ipn_IPNService_connect
|
|
||||||
func Java_com_tailscale_ipn_IPNService_connect(env *C.JNIEnv, this C.jobject) {
|
|
||||||
onConnect <- ConnectEvent{Enable: true}
|
|
||||||
}
|
|
||||||
|
|
||||||
//export Java_com_tailscale_ipn_IPNService_disconnect
|
|
||||||
func Java_com_tailscale_ipn_IPNService_disconnect(env *C.JNIEnv, this C.jobject) {
|
|
||||||
jenv := (*jnipkg.Env)(unsafe.Pointer(env))
|
|
||||||
onDisconnect <- jnipkg.NewGlobalRef(jenv, jnipkg.Object(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
//export Java_com_tailscale_ipn_StartVPNWorker_connect
|
|
||||||
func Java_com_tailscale_ipn_StartVPNWorker_connect(env *C.JNIEnv, this C.jobject) {
|
|
||||||
onConnect <- ConnectEvent{Enable: true}
|
|
||||||
}
|
|
||||||
|
|
||||||
//export Java_com_tailscale_ipn_StopVPNWorker_disconnect
|
|
||||||
func Java_com_tailscale_ipn_StopVPNWorker_disconnect(env *C.JNIEnv, this C.jobject) {
|
|
||||||
onConnect <- 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 := (*jnipkg.Env)(unsafe.Pointer(env))
|
|
||||||
m := jnipkg.GetStaticMethodID(jenv, googleClass,
|
|
||||||
"getIdTokenForActivity", "(Landroid/app/Activity;)Ljava/lang/String;")
|
|
||||||
idToken, err := jnipkg.CallStaticObjectMethod(jenv, googleClass, m, jnipkg.Value(act))
|
|
||||||
if err != nil {
|
|
||||||
fatalErr(err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
tok := jnipkg.GoString(jenv, jnipkg.String(idToken))
|
|
||||||
onGoogleToken <- tok
|
|
||||||
case requestPrepareVPN:
|
|
||||||
if resCode == resultOK {
|
|
||||||
notifyVPNPrepared()
|
|
||||||
} else {
|
|
||||||
notifyVPNClosed()
|
|
||||||
notifyVPNRevoked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//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:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func notifyVPNPrepared() {
|
|
||||||
select {
|
|
||||||
case onVPNPrepared <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func notifyVPNRevoked() {
|
|
||||||
select {
|
|
||||||
case onVPNRevoked <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func notifyVPNClosed() {
|
|
||||||
select {
|
|
||||||
case onVPNClosed <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var android struct {
|
|
||||||
// mu protects all fields of this structure. However, once a
|
|
||||||
// non-nil jvm is returned from javaVM, all the other fields may
|
|
||||||
// be accessed unlocked.
|
|
||||||
mu sync.Mutex
|
|
||||||
jvm *jnipkg.JVM
|
|
||||||
|
|
||||||
// appCtx is the global Android App context.
|
|
||||||
appCtx C.jobject
|
|
||||||
}
|
|
Loading…
Reference in New Issue