android: simplify local API client
Updates tailscale/corp#18202 Signed-off-by: Percy Wegmann <percy@tailscale.com>pull/213/head
parent
e16303e1d8
commit
d42329e2e2
@ -0,0 +1,230 @@
|
|||||||
|
// 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.BugReportID
|
||||||
|
import com.tailscale.ipn.ui.model.Errors
|
||||||
|
import com.tailscale.ipn.ui.model.Ipn
|
||||||
|
import com.tailscale.ipn.ui.model.IpnLocal
|
||||||
|
import com.tailscale.ipn.ui.model.IpnState
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
import kotlinx.serialization.serializer
|
||||||
|
import kotlin.reflect.KType
|
||||||
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
|
private object Endpoint {
|
||||||
|
const val DEBUG = "debug"
|
||||||
|
const val DEBUG_LOG = "debug-log"
|
||||||
|
const val BUG_REPORT = "bugreport"
|
||||||
|
const val PREFS = "prefs"
|
||||||
|
const val FILE_TARGETS = "file-targets"
|
||||||
|
const val UPLOAD_METRICS = "upload-client-metrics"
|
||||||
|
const val START = "start"
|
||||||
|
const val LOGIN_INTERACTIVE = "login-interactive"
|
||||||
|
const val RESET_AUTH = "reset-auth"
|
||||||
|
const val LOGOUT = "logout"
|
||||||
|
const val PROFILES = "profiles/"
|
||||||
|
const val PROFILES_CURRENT = "profiles/current"
|
||||||
|
const val STATUS = "status"
|
||||||
|
const val TKA_STATUS = "tka/status"
|
||||||
|
const val TKA_SIGN = "tka/sign"
|
||||||
|
const val TKA_VERIFY_DEEP_LINK = "tka/verify-deeplink"
|
||||||
|
const val PING = "ping"
|
||||||
|
const val FILES = "files"
|
||||||
|
const val FILE_PUT = "file-put"
|
||||||
|
const val TAILFS_SERVER_ADDRESS = "tailfs/fileserver-address"
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias StatusResponseHandler = (Result<IpnState.Status>) -> Unit
|
||||||
|
typealias BugReportIdHandler = (Result<BugReportID>) -> Unit
|
||||||
|
typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client provides a mechanism for calling Go's LocalAPIClient. Every LocalAPI endpoint has a
|
||||||
|
* corresponding method on this Client.
|
||||||
|
*/
|
||||||
|
class Client(private val scope: CoroutineScope) {
|
||||||
|
fun status(responseHandler: StatusResponseHandler) {
|
||||||
|
get(Endpoint.STATUS, responseHandler = responseHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bugReportId(responseHandler: BugReportIdHandler) {
|
||||||
|
post(Endpoint.BUG_REPORT, responseHandler = responseHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prefs(responseHandler: PrefsHandler) {
|
||||||
|
get(Endpoint.PREFS, responseHandler = responseHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun editPrefs(
|
||||||
|
prefs: Ipn.MaskedPrefs, responseHandler: (Result<Ipn.Prefs>) -> Unit
|
||||||
|
) {
|
||||||
|
val body = Json.encodeToString(prefs).toByteArray()
|
||||||
|
return patch(Endpoint.PREFS, body, responseHandler = responseHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun profiles(responseHandler: (Result<List<IpnLocal.LoginProfile>>) -> Unit) {
|
||||||
|
get(Endpoint.PROFILES, responseHandler = responseHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentProfile(responseHandler: (Result<IpnLocal.LoginProfile>) -> Unit) {
|
||||||
|
return get(Endpoint.PROFILES_CURRENT, responseHandler = responseHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startLoginInteractive(responseHandler: (Result<String>) -> Unit) {
|
||||||
|
return post(Endpoint.LOGIN_INTERACTIVE, responseHandler = responseHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logout(responseHandler: (Result<String>) -> Unit) {
|
||||||
|
return post(Endpoint.LOGOUT, responseHandler = responseHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> get(
|
||||||
|
path: String, body: ByteArray? = null, noinline responseHandler: (Result<T>) -> Unit
|
||||||
|
) {
|
||||||
|
Request(
|
||||||
|
scope = scope,
|
||||||
|
method = "GET",
|
||||||
|
path = path,
|
||||||
|
body = body,
|
||||||
|
responseType = typeOf<T>(),
|
||||||
|
responseHandler = responseHandler
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> put(
|
||||||
|
path: String, body: ByteArray? = null, noinline responseHandler: (Result<T>) -> Unit
|
||||||
|
) {
|
||||||
|
Request(
|
||||||
|
scope = scope,
|
||||||
|
method = "PUT",
|
||||||
|
path = path,
|
||||||
|
body = body,
|
||||||
|
responseType = typeOf<T>(),
|
||||||
|
responseHandler = responseHandler
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> post(
|
||||||
|
path: String, body: ByteArray? = null, noinline responseHandler: (Result<T>) -> Unit
|
||||||
|
) {
|
||||||
|
Request(
|
||||||
|
scope = scope,
|
||||||
|
method = "POST",
|
||||||
|
path = path,
|
||||||
|
body = body,
|
||||||
|
responseType = typeOf<T>(),
|
||||||
|
responseHandler = responseHandler
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> patch(
|
||||||
|
path: String, body: ByteArray? = null, noinline responseHandler: (Result<T>) -> Unit
|
||||||
|
) {
|
||||||
|
Request(
|
||||||
|
scope = scope,
|
||||||
|
method = "PATCH",
|
||||||
|
path = path,
|
||||||
|
body = body,
|
||||||
|
responseType = typeOf<T>(),
|
||||||
|
responseHandler = responseHandler
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> delete(
|
||||||
|
path: String, noinline responseHandler: (Result<T>) -> Unit
|
||||||
|
) {
|
||||||
|
Request(
|
||||||
|
scope = scope,
|
||||||
|
method = "DELETE",
|
||||||
|
path = path,
|
||||||
|
responseType = typeOf<T>(),
|
||||||
|
responseHandler = responseHandler
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Request<T>(
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val method: String,
|
||||||
|
path: String,
|
||||||
|
private val body: ByteArray? = null,
|
||||||
|
private val responseType: KType,
|
||||||
|
private val responseHandler: (Result<T>) -> Unit
|
||||||
|
) {
|
||||||
|
private val fullPath = "/localapi/v0/$path"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LocalAPIRequest"
|
||||||
|
|
||||||
|
private val jsonDecoder = Json { ignoreUnknownKeys = true }
|
||||||
|
private val isReady = CompletableDeferred<Boolean>()
|
||||||
|
|
||||||
|
// Called by the backend when the localAPI is ready to accept requests.
|
||||||
|
@JvmStatic
|
||||||
|
@Suppress("unused")
|
||||||
|
fun onReady() {
|
||||||
|
isReady.complete(true)
|
||||||
|
Log.d(TAG, "Ready")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// @see cmd/localapiclient/localapishim.go
|
||||||
|
//
|
||||||
|
// method: The HTTP method to use.
|
||||||
|
// request: The path to the localAPI endpoint.
|
||||||
|
// body: The body of the request.
|
||||||
|
private external fun doRequest(method: String, request: String, body: ByteArray?)
|
||||||
|
|
||||||
|
fun execute() {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
isReady.await()
|
||||||
|
Log.d(TAG, "Executing request:${method}:${fullPath}")
|
||||||
|
doRequest(method, fullPath, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is called from the JNI layer to publish responses.
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@Suppress("unused", "UNCHECKED_CAST")
|
||||||
|
fun onResponse(respData: ByteArray) {
|
||||||
|
Log.d(TAG, "Response for request: $fullPath")
|
||||||
|
|
||||||
|
val response: Result<T> = when (responseType) {
|
||||||
|
typeOf<String>() -> Result.success(respData.decodeToString() as T)
|
||||||
|
else -> try {
|
||||||
|
Result.success(
|
||||||
|
jsonDecoder.decodeFromStream(
|
||||||
|
Json.serializersModule.serializer(responseType), respData.inputStream()
|
||||||
|
) as T
|
||||||
|
)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
// If we couldn't parse the response body, assume it's an error response
|
||||||
|
try {
|
||||||
|
val error =
|
||||||
|
jsonDecoder.decodeFromStream<Errors.GenericError>(respData.inputStream())
|
||||||
|
throw Exception(error.error)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Result.failure(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The response handler will invoked internally by the request parser
|
||||||
|
scope.launch {
|
||||||
|
responseHandler(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,154 +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.BugReportID
|
|
||||||
import com.tailscale.ipn.ui.model.Ipn
|
|
||||||
import com.tailscale.ipn.ui.model.IpnLocal
|
|
||||||
import com.tailscale.ipn.ui.model.IpnState
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
typealias StatusResponseHandler = (Result<IpnState.Status>) -> Unit
|
|
||||||
typealias BugReportIdHandler = (Result<BugReportID>) -> Unit
|
|
||||||
typealias PrefsHandler = (Result<Ipn.Prefs>) -> Unit
|
|
||||||
|
|
||||||
class LocalApiClient(private val scope: CoroutineScope) {
|
|
||||||
init {
|
|
||||||
Log.d("LocalApiClient", "LocalApiClient created")
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val isReady = CompletableDeferred<Boolean>()
|
|
||||||
|
|
||||||
// Called by the backend when the localAPI is ready to accept requests.
|
|
||||||
@JvmStatic
|
|
||||||
@Suppress("unused")
|
|
||||||
fun onReady() {
|
|
||||||
isReady.complete(true)
|
|
||||||
Log.d("LocalApiClient", "LocalApiClient is ready")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
private external fun doRequest(
|
|
||||||
request: String, method: String, body: ByteArray?, cookie: String
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun <T> executeRequest(request: LocalAPIRequest<T>) {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
isReady.await()
|
|
||||||
Log.d("LocalApiClient", "Executing request:${request.method}:${request.path}")
|
|
||||||
requests[request.cookie] = request
|
|
||||||
doRequest(request.path, request.method, request.body, request.cookie)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is called from the JNI layer to publish localAPIResponses. This should execute on the
|
|
||||||
// same thread that called doRequest.
|
|
||||||
@Suppress("unused")
|
|
||||||
fun onResponse(response: ByteArray, cookie: String) {
|
|
||||||
requests.remove(cookie)?.let { request ->
|
|
||||||
Log.d("LocalApiClient", "Response for request:${request.path} cookie:${request.cookie}")
|
|
||||||
// The response handler will invoked internally by the request parser
|
|
||||||
scope.launch {
|
|
||||||
request.parser(response)
|
|
||||||
}
|
|
||||||
} ?: { 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 = ConcurrentHashMap<String, LocalAPIRequest<*>>()
|
|
||||||
|
|
||||||
// localapi Invocations
|
|
||||||
|
|
||||||
fun getStatus(responseHandler: StatusResponseHandler) {
|
|
||||||
val req = LocalAPIRequest.status(responseHandler)
|
|
||||||
executeRequest(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBugReportId(responseHandler: BugReportIdHandler) {
|
|
||||||
val req = LocalAPIRequest.bugReportId(responseHandler)
|
|
||||||
executeRequest(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPrefs(responseHandler: PrefsHandler) {
|
|
||||||
val req = LocalAPIRequest.prefs(responseHandler)
|
|
||||||
executeRequest(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun editPrefs(prefs: Ipn.MaskedPrefs, responseHandler: (Result<Ipn.Prefs>) -> Unit) {
|
|
||||||
val req = LocalAPIRequest.editPrefs(prefs, responseHandler)
|
|
||||||
executeRequest(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getProfiles(responseHandler: (Result<List<IpnLocal.LoginProfile>>) -> Unit) {
|
|
||||||
val req = LocalAPIRequest.profiles(responseHandler)
|
|
||||||
executeRequest(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentProfile(responseHandler: (Result<IpnLocal.LoginProfile>) -> Unit) {
|
|
||||||
val req = LocalAPIRequest.currentProfile(responseHandler)
|
|
||||||
executeRequest(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startLoginInteractive() {
|
|
||||||
val req = LocalAPIRequest.startLoginInteractive { result ->
|
|
||||||
result.success?.let { Log.d("LocalApiClient", "Login started: $it") }
|
|
||||||
?: run { Log.e("LocalApiClient", "Error starting login: ${result.error}") }
|
|
||||||
}
|
|
||||||
executeRequest<String>(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun logout() {
|
|
||||||
val req = LocalAPIRequest.logout { result ->
|
|
||||||
result.success?.let { Log.d("LocalApiClient", "Logout started: $it") }
|
|
||||||
?: run { Log.e("LocalApiClient", "Error starting logout: ${result.error}") }
|
|
||||||
}
|
|
||||||
executeRequest<String>(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
|
|
||||||
// addProfile
|
|
||||||
// switchProfile
|
|
||||||
// deleteProfile
|
|
||||||
// tailnetLocalStatus
|
|
||||||
// signNode
|
|
||||||
// verifyDeepling
|
|
||||||
// ping
|
|
||||||
// setTailFSFileServerAddress
|
|
||||||
init {
|
|
||||||
Log.d("LocalApiClient", "LocalApiClient created")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,173 +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.BugReportID
|
|
||||||
import com.tailscale.ipn.ui.model.Errors
|
|
||||||
import com.tailscale.ipn.ui.model.Ipn
|
|
||||||
import com.tailscale.ipn.ui.model.IpnLocal
|
|
||||||
import com.tailscale.ipn.ui.model.IpnState
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
private object Endpoint {
|
|
||||||
const val DEBUG = "debug"
|
|
||||||
const val DEBUG_LOG = "debug-log"
|
|
||||||
const val BUG_REPORT = "bugreport"
|
|
||||||
const val PREFS = "prefs"
|
|
||||||
const val FILE_TARGETS = "file-targets"
|
|
||||||
const val UPLOAD_METRICS = "upload-client-metrics"
|
|
||||||
const val START = "start"
|
|
||||||
const val LOGIN_INTERACTIVE = "login-interactive"
|
|
||||||
const val RESET_AUTH = "reset-auth"
|
|
||||||
const val LOGOUT = "logout"
|
|
||||||
const val PROFILES = "profiles/"
|
|
||||||
const val PROFILES_CURRENT = "profiles/current"
|
|
||||||
const val STATUS = "status"
|
|
||||||
const val TKA_STATUS = "tka/status"
|
|
||||||
const val TKA_SIGN = "tka/sign"
|
|
||||||
const val TKA_VERIFY_DEEP_LINK = "tka/verify-deeplink"
|
|
||||||
const val PING = "ping"
|
|
||||||
const val FILES = "files"
|
|
||||||
const val FILE_PUT = "file-put"
|
|
||||||
const val TAILFS_SERVER_ADDRESS = "tailfs/fileserver-address"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"), NO_PREFS("Current prefs not available");
|
|
||||||
|
|
||||||
fun toError(): Error {
|
|
||||||
return Error(rawValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LocalAPIRequest<T>(
|
|
||||||
path: String,
|
|
||||||
val method: String,
|
|
||||||
val body: ByteArray? = null,
|
|
||||||
val parser: (ByteArray) -> Unit,
|
|
||||||
) {
|
|
||||||
val path = "/localapi/v0/$path"
|
|
||||||
val cookie = UUID.randomUUID().toString()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
val decoder = Json { ignoreUnknownKeys = true }
|
|
||||||
|
|
||||||
fun <T> get(path: String, body: ByteArray? = null, parser: (ByteArray) -> Unit) =
|
|
||||||
LocalAPIRequest<T>(
|
|
||||||
method = "GET", path = path, body = body, parser = parser
|
|
||||||
)
|
|
||||||
|
|
||||||
fun <T> put(path: String, body: ByteArray? = null, parser: (ByteArray) -> Unit) =
|
|
||||||
LocalAPIRequest<T>(
|
|
||||||
method = "PUT", path = path, body = body, parser = parser
|
|
||||||
)
|
|
||||||
|
|
||||||
fun <T> post(path: String, body: ByteArray? = null, parser: (ByteArray) -> Unit) =
|
|
||||||
LocalAPIRequest<T>(
|
|
||||||
method = "POST", path = path, body = body, parser = parser
|
|
||||||
)
|
|
||||||
|
|
||||||
fun <T> patch(path: String, body: ByteArray? = null, parser: (ByteArray) -> Unit) =
|
|
||||||
LocalAPIRequest<T>(
|
|
||||||
method = "PATCH", path = path, body = body, parser = parser
|
|
||||||
)
|
|
||||||
|
|
||||||
fun status(responseHandler: StatusResponseHandler): LocalAPIRequest<IpnState.Status> {
|
|
||||||
return get(Endpoint.STATUS) { resp ->
|
|
||||||
responseHandler(decode<IpnState.Status>(resp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bugReportId(responseHandler: BugReportIdHandler): LocalAPIRequest<BugReportID> {
|
|
||||||
return post(Endpoint.BUG_REPORT) { resp ->
|
|
||||||
responseHandler(parseString(resp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun prefs(responseHandler: PrefsHandler): LocalAPIRequest<Ipn.Prefs> {
|
|
||||||
return get(Endpoint.PREFS) { resp ->
|
|
||||||
responseHandler(decode<Ipn.Prefs>(resp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun editPrefs(
|
|
||||||
prefs: Ipn.MaskedPrefs, responseHandler: (Result<Ipn.Prefs>) -> Unit
|
|
||||||
): LocalAPIRequest<Ipn.Prefs> {
|
|
||||||
val body = Json.encodeToString(prefs).toByteArray()
|
|
||||||
return patch(Endpoint.PREFS, body) { resp ->
|
|
||||||
responseHandler(decode<Ipn.Prefs>(resp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun profiles(responseHandler: (Result<List<IpnLocal.LoginProfile>>) -> Unit): LocalAPIRequest<List<IpnLocal.LoginProfile>> {
|
|
||||||
return get(Endpoint.PROFILES) { resp ->
|
|
||||||
responseHandler(decode<List<IpnLocal.LoginProfile>>(resp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun currentProfile(responseHandler: (Result<IpnLocal.LoginProfile>) -> Unit): LocalAPIRequest<IpnLocal.LoginProfile> {
|
|
||||||
return get(Endpoint.PROFILES_CURRENT) { resp ->
|
|
||||||
responseHandler(decode<IpnLocal.LoginProfile>(resp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startLoginInteractive(responseHandler: (Result<String>) -> Unit): LocalAPIRequest<String> {
|
|
||||||
return post(Endpoint.LOGIN_INTERACTIVE) { resp ->
|
|
||||||
responseHandler(parseString(resp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun logout(responseHandler: (Result<String>) -> Unit): LocalAPIRequest<String> {
|
|
||||||
return post(Endpoint.LOGOUT) { resp ->
|
|
||||||
responseHandler(parseString(resp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the response was a generic error
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
fun parseError(respData: ByteArray): Error {
|
|
||||||
return try {
|
|
||||||
val err = Json.decodeFromStream<Errors.GenericError>(respData.inputStream())
|
|
||||||
Error(err.error)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Error(APIErrorVals.UNPARSEABLE_RESPONSE.toError())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles responses that are raw strings. Returns an error result if the string
|
|
||||||
// is empty
|
|
||||||
private fun parseString(respData: ByteArray): Result<String> {
|
|
||||||
return if (respData.isNotEmpty()) Result(respData.decodeToString())
|
|
||||||
else Result(APIErrorVals.UNPARSEABLE_RESPONSE.toError())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to decode the response into the expected type. If that fails, then try
|
|
||||||
// parsing as an error.
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
private inline fun <reified T> decode(respData: ByteArray): Result<T> {
|
|
||||||
return try {
|
|
||||||
val message = decoder.decodeFromStream<T>(respData.inputStream())
|
|
||||||
Result(message)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Result(parseError(respData))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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?
|
|
||||||
|
|
||||||
private 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
|
|
||||||
}
|
|
Loading…
Reference in New Issue