// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package com.tailscale.ipn.ui.localapi import com.tailscale.ipn.ui.model.* import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json enum class LocalAPIEndpoint(val rawValue: String) { Debug("debug"), Debug_Log("debug-log"), BugReport("bugreport"), Prefs("prefs"), FileTargets("file-targets"), UploadMetrics("upload-client-metrics"), Start("start"), LoginInteractive("login-interactive"), ResetAuth("reset-auth"), Logout("logout"), Profiles("profiles"), ProfilesCurrent("profiles/current"), Status("status"), TKAStatus("tka/status"), TKASitng("tka/sign"), TKAVerifyDeeplink("tka/verify-deeplink"), Ping("ping"), Files("files"), FilePut("file-put"), TailFSServerAddress("tailfs/fileserver-address"); val prefix = "/localapi/v0/" fun path(): String { return prefix + rawValue } } // Potential local and upstream errors. Error handling in localapi in the go layer // is inconsistent but different clients already deal with that inconsistency so // 'fixing' it will likely break other things. // // For now, anything that isn't an { error: "message" } will be passed along // as UNPARSEABLE_RESPONSE. We can add additional error translation in the parseError // method as needed. // // (jonathan) TODO: Audit local API for all of the possible error results and clean // it up if possible. enum class APIErrorVals(val rawValue: String) { UNPARSEABLE_RESPONSE("Unparseable localAPI response"), NOT_READY("Not Ready"); fun toError(): Error { return Error(rawValue) } } class LocalAPIRequest( val path: String, val method: String, val body: String? = null, val responseHandler: (Result) -> Unit, val parser: (String) -> Unit, ) { companion object { val cookieLock = Any() var cookieCounter: Int = 0 val decoder = Json { ignoreUnknownKeys = true } fun getCookie(): String { synchronized(cookieLock) { cookieCounter += 1 return cookieCounter.toString() } } fun status(responseHandler: StatusResponseHandler): LocalAPIRequest { val path = LocalAPIEndpoint.Status.path() return LocalAPIRequest(path, "GET", null, responseHandler) { resp -> responseHandler(decode(resp)) } } fun bugReportId(responseHandler: BugReportIdHandler): LocalAPIRequest { val path = LocalAPIEndpoint.BugReport.path() return LocalAPIRequest(path, "POST", null, responseHandler) { resp -> responseHandler(parseString(resp)) } } fun prefs(responseHandler: PrefsHandler): LocalAPIRequest { val path = LocalAPIEndpoint.Prefs.path() return LocalAPIRequest(path, "GET", null, responseHandler) { resp -> responseHandler(decode(resp)) } } // Check if the response was a generic error fun parseError(respData: String): Error { try { val err = Json.decodeFromString(respData) return Error(err.error) } catch (e: Exception) { return Error(APIErrorVals.UNPARSEABLE_RESPONSE.toError()) } } // Handles responses that are raw strings. Returns an error result if the string // is empty fun parseString(respData: String): Result { return if (respData.length > 0) Result(respData) else Result(APIErrorVals.UNPARSEABLE_RESPONSE.toError()) } // Attempt to decode the response into the expected type. If that fails, then try // parsing as an error. inline fun decode(respData: String): Result { try { val message = decoder.decodeFromString(respData) return Result(message) } catch (e: Exception) { return Result(parseError(respData)) } } } val cookie: String = getCookie() }