diff --git a/client/electron/go_plugin.ts b/client/electron/go_plugin.ts index e9a3817484..76fe00c9af 100644 --- a/client/electron/go_plugin.ts +++ b/client/electron/go_plugin.ts @@ -18,14 +18,12 @@ import koffi from 'koffi'; import {pathToBackendLibrary} from './app_paths'; -let invokeGoAPIFunc: Function | undefined; - -export type GoApiName = 'FetchResource' | 'EstablishVPN' | 'CloseVPN'; +let invokeMethodFunc: Function | undefined; /** - * Calls a Go function by invoking the `InvokeGoAPI` function in the native backend library. + * Calls a Go function by invoking the `InvokeMethod` function in the native backend library. * - * @param api The name of the Go API to invoke. + * @param method The name of the Go method to invoke. * @param input The input string to pass to the API. * @returns A Promise that resolves to the output string returned by the API. * @throws An Error containing PlatformError details if the API call fails. @@ -34,11 +32,11 @@ export type GoApiName = 'FetchResource' | 'EstablishVPN' | 'CloseVPN'; * Ensure that the function signature and data structures are consistent with the C definitions * in `./client/go/outline/electron/go_plugin.go`. */ -export async function invokeGoApi( - api: GoApiName, +export async function invokeMethod( + method: string, input: string ): Promise { - if (!invokeGoAPIFunc) { + if (!invokeMethodFunc) { const backendLib = koffi.load(pathToBackendLibrary()); // Define C strings and setup auto release @@ -48,19 +46,19 @@ export async function invokeGoApi( backendLib.func('FreeCGoString', 'void', ['str']) ); - // Define InvokeGoAPI data structures and function - const invokeGoApiResult = koffi.struct('InvokeGoAPIResult', { + // Define InvokeMethod data structures and function + const invokeMethodResult = koffi.struct('InvokeMethodResult', { Output: cgoString, ErrorJson: cgoString, }); - invokeGoAPIFunc = promisify( - backendLib.func('InvokeGoAPI', invokeGoApiResult, ['str', 'str']).async + invokeMethodFunc = promisify( + backendLib.func('InvokeMethod', invokeMethodResult, ['str', 'str']).async ); } - console.debug(`[Backend] - calling InvokeGoAPI "${api}" ...`); - const result = await invokeGoAPIFunc(api, input); - console.debug(`[Backend] - GoAPI ${api} returned`, result); + console.debug(`[Backend] - calling InvokeMethod "${method}" ...`); + const result = await invokeMethodFunc(method, input); + console.debug(`[Backend] - InvokeMethod "${method}" returned`, result); if (result.ErrorJson) { throw Error(result.ErrorJson); } diff --git a/client/electron/index.ts b/client/electron/index.ts index eb88a9ed61..c75bb18326 100644 --- a/client/electron/index.ts +++ b/client/electron/index.ts @@ -34,7 +34,7 @@ import { import {autoUpdater} from 'electron-updater'; import {lookupIp} from './connectivity'; -import {GoApiName, invokeGoApi} from './go_plugin'; +import {invokeMethod} from './go_plugin'; import {GoVpnTunnel} from './go_vpn_tunnel'; import {installRoutingServices, RoutingDaemon} from './routing_service'; import {TunnelStore} from './tunnel_store'; @@ -514,19 +514,19 @@ function main() { mainWindow?.webContents.send('outline-ipc-push-clipboard'); }); - // This IPC handler allows the renderer process to call Go API functions exposed by the backend. + // This IPC handler allows the renderer process to call functions exposed by the backend. // It takes two arguments: - // - api: The name of the Go API function to call. - // - input: A string representing the input data to the Go function. + // - method: The name of the method to call. + // - params: A string representing the input data to the function. // // The handler returns the output string from the Go function if successful. // Both the input string and output string need to be interpreted by the renderer process according // to the specific API being called. - // If Go function encounters an error, it throws an Error that can be parsed by the `PlatformError`. + // If the function encounters an error, it throws an Error that can be parsed by the `PlatformError`. ipcMain.handle( - 'outline-ipc-invoke-go-api', - (_, api: GoApiName, input: string): Promise => - invokeGoApi(api, input) + 'outline-ipc-invoke-method', + (_, method: string, params: string): Promise => + invokeMethod(method, params) ); // Connects to a proxy server specified by a config. diff --git a/client/go/outline/electron/go_plugin.go b/client/go/outline/electron/go_plugin.go index cc7a0812bf..d8c613f237 100644 --- a/client/go/outline/electron/go_plugin.go +++ b/client/go/outline/electron/go_plugin.go @@ -17,8 +17,8 @@ package main /* #include // for C.free -// InvokeGoAPIResult is a struct used to pass result from Go to TypeScript boundary. -typedef struct InvokeGoAPIResult_t +// InvokeMethodResult is a struct used to pass result from Go to TypeScript boundary. +typedef struct InvokeMethodResult_t { // A string representing the result of the Go function call. // This may be a raw string or a JSON string depending on the API call. @@ -28,7 +28,7 @@ typedef struct InvokeGoAPIResult_t // Go function call, or NULL if no error occurred. // This error can be parsed by the PlatformError in TypeScript. const char *ErrorJson; -} InvokeGoAPIResult; +} InvokeMethodResult; */ import "C" import ( @@ -41,14 +41,8 @@ import ( "github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors" ) -// API name constants +// Electron specific APIs const ( - // FetchResourceAPI fetches a resource located at a given URL. - // - // - Input: the URL string of the resource to fetch - // - Output: the content in raw string of the fetched resource - FetchResourceAPI = "FetchResource" - // EstablishVPNAPI initiates a VPN connection and directs all network traffic through Outline. // // - Input: a JSON string of [VPNConfig]. @@ -63,25 +57,18 @@ const ( CloseVPNAPI = "CloseVPN" ) -// InvokeGoAPI is the unified entry point for TypeScript to invoke various Go functions. +// InvokeMethod is the unified entry point for TypeScript to invoke various Go functions. // // The input and output are all defined as string, but they may represent either a raw string, // or a JSON string depending on the API call. // // Check the API name constants comment for more details about the input and output format. // -//export InvokeGoAPI -func InvokeGoAPI(api *C.char, input *C.char) C.InvokeGoAPIResult { - apiName := C.GoString(api) - switch apiName { - - case FetchResourceAPI: - res := outline.FetchResource(C.GoString(input)) - return C.InvokeGoAPIResult{ - Output: newCGoString(res.Content), - ErrorJson: marshalCGoErrorJson(platerrors.ToPlatformError(res.Error)), - } - +//export InvokeMethod +func InvokeMethod(method *C.char, input *C.char) C.InvokeMethodResult { + methodName := C.GoString(method) + switch methodName { + // Electron specific APIs case EstablishVPNAPI: res, err := EstablishVPN(C.GoString(input)) return C.InvokeGoAPIResult{ @@ -93,12 +80,13 @@ func InvokeGoAPI(api *C.char, input *C.char) C.InvokeGoAPIResult { err := CloseVPN() return C.InvokeGoAPIResult{ErrorJson: marshalCGoErrorJson(err)} + // Common APIs default: - err := &platerrors.PlatformError{ - Code: platerrors.InternalError, - Message: fmt.Sprintf("unsupported Go API: %s", apiName), + result := outline.InvokeMethod(methodName, C.GoString(input)) + return C.InvokeMethodResult{ + Output: newCGoString(result.Value), + ErrorJson: marshalCGoErrorJson(result.Error), } - return C.InvokeGoAPIResult{ErrorJson: marshalCGoErrorJson(err)} } } diff --git a/client/go/outline/fetch.go b/client/go/outline/fetch.go index 28078b405d..64d360a4fa 100644 --- a/client/go/outline/fetch.go +++ b/client/go/outline/fetch.go @@ -21,47 +21,39 @@ import ( "github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors" ) -// FetchResourceResult represents the result of fetching a resource located at a URL. -// -// We use a struct instead of a tuple to preserve a strongly typed error that gobind recognizes. -type FetchResourceResult struct { - Content string - Error *platerrors.PlatformError -} - -// FetchResource fetches a resource from the given URL. +// fetchResource fetches a resource from the given URL. // // The function makes an HTTP GET request to the specified URL and returns the response body as a // string. If the request fails or the server returns a non-2xx status code, an error is returned. -func FetchResource(url string) *FetchResourceResult { +func fetchResource(url string) (string, error) { resp, err := http.Get(url) if err != nil { - return &FetchResourceResult{Error: &platerrors.PlatformError{ + return "", platerrors.PlatformError{ Code: platerrors.FetchConfigFailed, Message: "failed to fetch the URL", Details: platerrors.ErrorDetails{"url": url}, Cause: platerrors.ToPlatformError(err), - }} + } } body, err := io.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode > 299 { - return &FetchResourceResult{Error: &platerrors.PlatformError{ + return "", platerrors.PlatformError{ Code: platerrors.FetchConfigFailed, Message: "non-successful HTTP status", Details: platerrors.ErrorDetails{ "status": resp.Status, "body": string(body), }, - }} + } } if err != nil { - return &FetchResourceResult{Error: &platerrors.PlatformError{ + return "", platerrors.PlatformError{ Code: platerrors.FetchConfigFailed, Message: "failed to read the body", Details: platerrors.ErrorDetails{"url": url}, Cause: platerrors.ToPlatformError(err), - }} + } } - return &FetchResourceResult{Content: string(body)} + return string(body), nil } diff --git a/client/go/outline/fetch_test.go b/client/go/outline/fetch_test.go index 60db9048bc..5ad000e151 100644 --- a/client/go/outline/fetch_test.go +++ b/client/go/outline/fetch_test.go @@ -30,7 +30,7 @@ func TestFetchResource(t *testing.T) { })) defer server.Close() - result := FetchResource(server.URL) + result := fetchResource(server.URL) require.Nil(t, result.Error) require.Equal(t, "{\"name\": \"my-test-key\"}\n", result.Content) } @@ -55,7 +55,7 @@ func TestFetchResource_Redirection(t *testing.T) { })) defer redirSvr.Close() - result := FetchResource(redirSvr.URL) + result := fetchResource(redirSvr.URL) require.Nil(t, result.Error) require.Equal(t, "ss://my-url-format-test-key\n", result.Content) } @@ -78,7 +78,7 @@ func TestFetchResource_HTTPStatusError(t *testing.T) { })) defer server.Close() - result := FetchResource(server.URL) + result := fetchResource(server.URL) require.Error(t, result.Error) require.Equal(t, platerrors.FetchConfigFailed, result.Error.Code) require.Error(t, result.Error.Cause) @@ -91,7 +91,7 @@ func TestFetchResource_BodyReadError(t *testing.T) { })) defer server.Close() - result := FetchResource(server.URL) + result := fetchResource(server.URL) require.Error(t, result.Error) require.Equal(t, platerrors.FetchConfigFailed, result.Error.Code) require.Error(t, result.Error.Cause) diff --git a/client/go/outline/method_channel.go b/client/go/outline/method_channel.go new file mode 100644 index 0000000000..19cb95f958 --- /dev/null +++ b/client/go/outline/method_channel.go @@ -0,0 +1,56 @@ +// Copyright 2024 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package outline + +import ( + "fmt" + + "github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors" +) + +// API name constants +const ( + // FetchResource fetches a resource located at a given URL. + // - Input: the URL string of the resource to fetch + // - Output: the content in raw string of the fetched resource + MethodFetchResource = "FetchResource" +) + +// InvokeMethodResult represents the result of an InvokeMethod call. +// +// We use a struct instead of a tuple to preserve a strongly typed error that gobind recognizes. +type InvokeMethodResult struct { + Value string + Error *platerrors.PlatformError +} + +// InvokeMethod calls a method by name. +func InvokeMethod(method string, input string) *InvokeMethodResult { + switch method { + case MethodFetchResource: + url := input + content, err := fetchResource(url) + return &InvokeMethodResult{ + Value: content, + Error: platerrors.ToPlatformError(err), + } + + default: + return &InvokeMethodResult{Error: &platerrors.PlatformError{ + Code: platerrors.InternalError, + Message: fmt.Sprintf("unsupported Go method: %s", method), + }} + } +} diff --git a/client/src/cordova/plugin/android/java/org/outline/OutlinePlugin.java b/client/src/cordova/plugin/android/java/org/outline/OutlinePlugin.java index bd10f0a6c7..0b6acec487 100644 --- a/client/src/cordova/plugin/android/java/org/outline/OutlinePlugin.java +++ b/client/src/cordova/plugin/android/java/org/outline/OutlinePlugin.java @@ -43,7 +43,7 @@ import org.outline.vpn.VpnServiceStarter; import org.outline.vpn.VpnTunnelService; import outline.Outline; -import outline.FetchResourceResult; +import outline.InvokeMethodResult; import platerrors.Platerrors; import platerrors.PlatformError; @@ -55,11 +55,11 @@ public class OutlinePlugin extends CordovaPlugin { // Actions supported by this plugin. public enum Action { + INVOKE_METHOD("invokeMethod"), START("start"), STOP("stop"), ON_STATUS_CHANGE("onStatusChange"), IS_RUNNING("isRunning"), - FETCH_RESOURCE("fetchResource"), INIT_ERROR_REPORTING("initializeErrorReporting"), REPORT_EVENTS("reportEvents"), QUIT("quitApplication"); @@ -191,8 +191,21 @@ private void executeAsync( final String action, final JSONArray args, final CallbackContext callback) { cordova.getThreadPool().execute(() -> { try { + if (Action.INVOKE_METHOD.is(action)) { + final String methodName = args.getString(0); + final String input = args.getString(1); + LOG.fine(String.format(Locale.ROOT, "Calling InvokeMethod(%s, %s)", methodName, input)); + final InvokeMethodResult result = Outline.invokeMethod(methodName, input); + if (result.getError() != null) { + LOG.warning(String.format(Locale.ROOT, "InvokeMethod(%s) failed: %s", methodName, result.getError())); + sendActionResult(callback, result.getError()); + } else { + LOG.fine(String.format(Locale.ROOT, "InvokeMethod(%s) result: %s", methodName, result.getValue())); + callback.success(result.getValue()); + } + // Tunnel instance actions: tunnel ID is always the first argument. - if (Action.START.is(action)) { + } else if (Action.START.is(action)) { final String tunnelId = args.getString(0); final String serverName = args.getString(1); final String transportConfig = args.getString(2); @@ -205,17 +218,6 @@ private void executeAsync( final String tunnelId = args.getString(0); boolean isActive = isTunnelActive(tunnelId); callback.sendPluginResult(new PluginResult(PluginResult.Status.OK, isActive)); - } else if (Action.FETCH_RESOURCE.is(action)) { - final String url = args.getString(0); - LOG.fine(String.format(Locale.ROOT, "Fetching resource at %s ...", url)); - final FetchResourceResult result = Outline.fetchResource(url); - if (result.getError() != null) { - LOG.warning(String.format(Locale.ROOT, "Fetch resource failed: %s", result.getError())); - sendActionResult(callback, result.getError()); - } else { - LOG.info(String.format(Locale.ROOT, "Fetch resource result: %s", result.getContent())); - callback.success(result.getContent()); - } // Static actions } else if (Action.INIT_ERROR_REPORTING.is(action)) { diff --git a/client/src/cordova/plugin/apple/src/OutlinePlugin.swift b/client/src/cordova/plugin/apple/src/OutlinePlugin.swift index ff72faa444..4d55896f18 100644 --- a/client/src/cordova/plugin/apple/src/OutlinePlugin.swift +++ b/client/src/cordova/plugin/apple/src/OutlinePlugin.swift @@ -144,21 +144,24 @@ class OutlinePlugin: CDVPlugin { } } - func fetchResource(_ command: CDVInvokedUrlCommand) { - guard let url = command.argument(at: 0) as? String else { - return sendError("Missing URL", callbackId: command.callbackId) + func invokeMethod(_ command: CDVInvokedUrlCommand) { + guard let methodName = command.argument(at: 0) as? String else { + return sendError("Missing method name", callbackId: command.callbackId) } - DDLogInfo("Fetching resource from \(url)") + guard let input = command.argument(at: 1) as? String else { + return sendError("Missing method input", callbackId: command.callbackId) + } + DDLogInfo("Invoking Method \(methodName) with input \(input)") Task { - guard let result = OutlineFetchResource(url) else { - return self.sendError("unexpected fetching result", callbackId: command.callbackId) + guard let result = OutlineInvokeMethod(methodName, input) else { + return self.sendError("unexpected invoke error", callbackId: command.callbackId) } if result.error != nil { let errorJson = marshalErrorJson(error: OutlineError.platformError(result.error!)) return self.sendError(errorJson, callbackId: command.callbackId) } - DDLogInfo("Fetch resource result: \(result.content)") - self.sendSuccess(result.content, callbackId: command.callbackId) + DDLogInfo("InvokeMethod result: \(result.value)") + self.sendSuccess(result.value, callbackId: command.callbackId) } } diff --git a/client/src/www/app/main.cordova.ts b/client/src/www/app/main.cordova.ts index 22a3446253..1b06f704cb 100644 --- a/client/src/www/app/main.cordova.ts +++ b/client/src/www/app/main.cordova.ts @@ -27,12 +27,15 @@ import * as Sentry from '@sentry/browser'; import {AbstractClipboard} from './clipboard'; import {EnvironmentVariables} from './environment'; import {main} from './main'; +import {installDefaultMethodChannel, MethodChannel} from './method_channel'; import {VpnApi} from './outline_server_repository/vpn'; import {CordovaVpnApi} from './outline_server_repository/vpn.cordova'; import {OutlinePlatform} from './platform'; -import {OUTLINE_PLUGIN_NAME, pluginExec} from './plugin.cordova'; -import {ResourceFetcher} from './resource_fetcher'; -import {CordovaResourceFetcher} from './resource_fetcher.cordova'; +import { + OUTLINE_PLUGIN_NAME, + pluginExec, + pluginExecWithErrorCode, +} from './plugin.cordova'; import {AbstractUpdater} from './updater'; import * as interceptors from './url_interceptor'; import {NoOpVpnInstaller, VpnInstaller} from './vpn_installer'; @@ -71,6 +74,12 @@ class CordovaErrorReporter extends SentryErrorReporter { } } +class CordovaMethodChannel implements MethodChannel { + invokeMethod(methodName: string, params: string): Promise { + return pluginExecWithErrorCode('invokeMethod', methodName, params); + } +} + // This class should only be instantiated after Cordova fires the deviceready event. class CordovaPlatform implements OutlinePlatform { getVpnApi(): VpnApi | undefined { @@ -117,10 +126,6 @@ class CordovaPlatform implements OutlinePlatform { return new NoOpVpnInstaller(); } - getResourceFetcher(): ResourceFetcher { - return new CordovaResourceFetcher(); - } - quitApplication() { // Only used in macOS because menu bar apps provide no alternative way of quitting. cordova.exec( @@ -148,5 +153,6 @@ window.handleOpenURL = (url: string) => { }; onceDeviceReady.then(() => { + installDefaultMethodChannel(new CordovaMethodChannel()); main(new CordovaPlatform()); }); diff --git a/client/src/www/app/main.electron.ts b/client/src/www/app/main.electron.ts index d17dfc74bd..2e67ba90b5 100644 --- a/client/src/www/app/main.electron.ts +++ b/client/src/www/app/main.electron.ts @@ -21,9 +21,9 @@ import * as Sentry from '@sentry/electron/renderer'; import {AbstractClipboard} from './clipboard'; import {getLocalizationFunction, main} from './main'; +import {installDefaultMethodChannel, MethodChannel} from './method_channel'; import {VpnApi} from './outline_server_repository/vpn'; import {ElectronVpnApi} from './outline_server_repository/vpn.electron'; -import {ElectronResourceFetcher} from './resource_fetcher.electron'; import {AbstractUpdater} from './updater'; import {UrlInterceptor} from './url_interceptor'; import {VpnInstaller} from './vpn_installer'; @@ -127,6 +127,18 @@ class ElectronErrorReporter implements OutlineErrorReporter { } } +class ElectronMethodChannel implements MethodChannel { + invokeMethod(methodName: string, params: string): Promise { + return window.electron.methodChannel.invoke( + 'invoke-method', + methodName, + params + ); + } +} + +installDefaultMethodChannel(new ElectronMethodChannel()); + main({ getVpnApi(): VpnApi | undefined { if (isOsSupported) { @@ -138,6 +150,5 @@ main({ getErrorReporter: _ => new ElectronErrorReporter(), getUpdater: () => new ElectronUpdater(), getVpnServiceInstaller: () => new ElectronVpnInstaller(), - getResourceFetcher: () => new ElectronResourceFetcher(), quitApplication: () => window.electron.methodChannel.send('quit-app'), }); diff --git a/client/src/www/app/main.ts b/client/src/www/app/main.ts index d035b3a4ef..f451d5c5a7 100644 --- a/client/src/www/app/main.ts +++ b/client/src/www/app/main.ts @@ -26,7 +26,6 @@ import { FakeVpnApi, } from './outline_server_repository/vpn.fake'; import {OutlinePlatform} from './platform'; -import {BrowserResourceFetcher} from './resource_fetcher'; import {Settings} from './settings'; import {EventQueue} from '../model/events'; @@ -61,8 +60,7 @@ function createServerRepo(platform: OutlinePlatform, eventQueue: EventQueue) { vpnApi, eventQueue, window.localStorage, - localize, - platform.getResourceFetcher() + localize ); } @@ -71,8 +69,7 @@ function createServerRepo(platform: OutlinePlatform, eventQueue: EventQueue) { new FakeVpnApi(), eventQueue, window.localStorage, - localize, - new BrowserResourceFetcher() + localize ); if (repo.getAll().length === 0) { diff --git a/client/src/www/app/resource_fetcher.cordova.ts b/client/src/www/app/method_channel.ts similarity index 57% rename from client/src/www/app/resource_fetcher.cordova.ts rename to client/src/www/app/method_channel.ts index 1fbea930cd..dc5d4c5cd6 100644 --- a/client/src/www/app/resource_fetcher.cordova.ts +++ b/client/src/www/app/method_channel.ts @@ -12,14 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {pluginExecWithErrorCode} from './plugin.cordova'; -import {ResourceFetcher} from './resource_fetcher'; +export interface MethodChannel { + invokeMethod(methodName: string, params: string): Promise; +} + +let defaultMethodChannel: MethodChannel; + +export function installDefaultMethodChannel( + methodChannel: MethodChannel +): void { + defaultMethodChannel = methodChannel; +} -/** - * Fetches resources using Cordova plugin. - */ -export class CordovaResourceFetcher implements ResourceFetcher { - fetch(url: string): Promise { - return pluginExecWithErrorCode('fetchResource', url); +export function getDefaultMethodChannel(): MethodChannel { + if (!defaultMethodChannel) { + throw new Error('default MethodChannel not installed'); } + return defaultMethodChannel; } diff --git a/client/src/www/app/outline_server_repository/index.ts b/client/src/www/app/outline_server_repository/index.ts index 1658d183cf..f37f165fb1 100644 --- a/client/src/www/app/outline_server_repository/index.ts +++ b/client/src/www/app/outline_server_repository/index.ts @@ -21,7 +21,6 @@ import {TunnelStatus, VpnApi} from './vpn'; import * as errors from '../../model/errors'; import * as events from '../../model/events'; import {ServerRepository} from '../../model/server'; -import {ResourceFetcher} from '../resource_fetcher'; // DEPRECATED: V0 server persistence format. interface ServersStorageV0Config { @@ -71,8 +70,7 @@ export class OutlineServerRepository implements ServerRepository { private vpnApi: VpnApi, private eventQueue: events.EventQueue, private storage: Storage, - private localize: Localizer, - readonly urlFetcher: ResourceFetcher + private localize: Localizer ) { console.debug('OutlineServerRepository is initializing'); this.loadServers(); @@ -273,13 +271,6 @@ export class OutlineServerRepository implements ServerRepository { accessKey: string, name?: string ): OutlineServer { - return new OutlineServer( - this.vpnApi, - this.urlFetcher, - id, - name, - accessKey, - this.localize - ); + return new OutlineServer(this.vpnApi, id, name, accessKey, this.localize); } } diff --git a/client/src/www/app/outline_server_repository/outline_server_repository.spec.ts b/client/src/www/app/outline_server_repository/outline_server_repository.spec.ts index 38853d10f6..eda01ec090 100644 --- a/client/src/www/app/outline_server_repository/outline_server_repository.spec.ts +++ b/client/src/www/app/outline_server_repository/outline_server_repository.spec.ts @@ -32,7 +32,6 @@ import { ServerForgotten, ServerRenamed, } from '../../model/events'; -import {BrowserResourceFetcher} from '../resource_fetcher'; // TODO(alalama): unit tests for OutlineServer. @@ -395,7 +394,6 @@ function newTestRepo( storage, _ => { return 'Outline Server'; - }, - new BrowserResourceFetcher() + } ); } diff --git a/client/src/www/app/outline_server_repository/server.ts b/client/src/www/app/outline_server_repository/server.ts index 73edd1203a..accf866baf 100644 --- a/client/src/www/app/outline_server_repository/server.ts +++ b/client/src/www/app/outline_server_repository/server.ts @@ -26,7 +26,7 @@ import {StartRequestJson, VpnApi} from './vpn'; import * as errors from '../../model/errors'; import {PlatformError} from '../../model/platform_error'; import {Server, ServerType} from '../../model/server'; -import {ResourceFetcher} from '../resource_fetcher'; +import {getDefaultMethodChannel} from '../method_channel'; // PLEASE DON'T use this class outside of this `outline_server_repository` folder! @@ -39,7 +39,6 @@ export class OutlineServer implements Server { constructor( private vpnApi: VpnApi, - readonly urlFetcher: ResourceFetcher, readonly id: string, public name: string, readonly accessKey: string, @@ -88,10 +87,7 @@ export class OutlineServer implements Server { async connect() { let tunnelConfig: TunnelConfigJson; if (this.type === ServerType.DYNAMIC_CONNECTION) { - tunnelConfig = await fetchTunnelConfig( - this.urlFetcher, - this.tunnelConfigLocation - ); + tunnelConfig = await fetchTunnelConfig(this.tunnelConfigLocation); this.displayAddress = net.joinHostPort( tunnelConfig.firstHop.host, tunnelConfig.firstHop.port.toString() @@ -147,11 +143,13 @@ export class OutlineServer implements Server { /** fetchTunnelConfig fetches information from a dynamic access key and attempts to parse it. */ // TODO(daniellacosse): unit tests async function fetchTunnelConfig( - urlFetcher: ResourceFetcher, configLocation: URL ): Promise { const responseBody = ( - await urlFetcher.fetch(configLocation.toString()) + await getDefaultMethodChannel().invokeMethod( + 'FetchResource', + configLocation.toString() + ) ).trim(); if (!responseBody) { throw new errors.ServerAccessKeyInvalid( diff --git a/client/src/www/app/platform.ts b/client/src/www/app/platform.ts index af01aad458..c5170679a7 100644 --- a/client/src/www/app/platform.ts +++ b/client/src/www/app/platform.ts @@ -15,7 +15,6 @@ import {Clipboard} from './clipboard'; import {EnvironmentVariables} from './environment'; import {VpnApi} from './outline_server_repository/vpn'; -import {ResourceFetcher} from './resource_fetcher'; import {Updater} from './updater'; import {UrlInterceptor} from './url_interceptor'; import {VpnInstaller} from './vpn_installer'; @@ -36,7 +35,5 @@ export interface OutlinePlatform { getVpnServiceInstaller(): VpnInstaller; - getResourceFetcher(): ResourceFetcher; - quitApplication(): void; } diff --git a/client/src/www/app/resource_fetcher.electron.ts b/client/src/www/app/resource_fetcher.electron.ts deleted file mode 100644 index a62a7b2d8a..0000000000 --- a/client/src/www/app/resource_fetcher.electron.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2024 The Outline Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import {ResourceFetcher} from './resource_fetcher'; -import {PlatformError} from '../model/platform_error'; - -/** - * Fetches resources using Electron's IPC to communicate with the main process. - */ -export class ElectronResourceFetcher implements ResourceFetcher { - async fetch(url: string): Promise { - try { - return await window.electron.methodChannel.invoke( - 'invoke-go-api', - 'FetchResource', - url - ); - } catch (e) { - throw PlatformError.parseFrom(e); - } - } -} diff --git a/client/src/www/app/resource_fetcher.ts b/client/src/www/app/resource_fetcher.ts deleted file mode 100644 index 9b85cde41d..0000000000 --- a/client/src/www/app/resource_fetcher.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2024 The Outline Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import {SessionConfigFetchFailed} from '../model/errors'; - -/** - * An interface for fetching resources located at a given URL. - */ -export interface ResourceFetcher { - /** - * Fetches the content of a resource located at the given URL. - * @param url The URL of the resource to fetch. - * @returns A Promise that resolves to a string containing the content of the fetched resource. - */ - fetch(url: string): Promise; -} - -/** - * Fetches resources using the browser's built-in fetch function. - */ -export class BrowserResourceFetcher implements ResourceFetcher { - async fetch(url: string): Promise { - let response: Response; - try { - response = await fetch(url, { - cache: 'no-store', - redirect: 'follow', - }); - } catch (cause) { - throw new SessionConfigFetchFailed( - 'Failed to fetch VPN information from dynamic access key.', - {cause} - ); - } - return await response.text(); - } -} diff --git a/docs/invitation_instructions.md b/docs/invitation_instructions.md index 126a0b49e2..993e13dbff 100644 --- a/docs/invitation_instructions.md +++ b/docs/invitation_instructions.md @@ -12,9 +12,9 @@ Have an invitation to connect to an Outline server? Follow these instructions to |Download| | | ------------- | ------------- | | [Android ›](https://play.google.com/store/apps/details?id=org.outline.android.client) | If Google Play is not accessible, [get it here](https://s3.amazonaws.com/outline-releases/client/android/stable/Outline-Client.apk). | -| [iOS ›](https://itunes.apple.com/app/outline-app/id1356177741) | Get Outline on the App Store | +| [iOS ›](https://apps.apple.com/app/outline-app/id1356177741) | Get Outline on the App Store | | [Windows ›](https://s3.amazonaws.com/outline-releases/client/windows/stable/Outline-Client.exe) | Download Outline.exe and double click to launch. | -| [macOS ›](https://itunes.apple.com/app/outline-app/id1356178125) | Download outline.dmg, double click to install. Add Outline to your applications folder, double click to launch. | +| [macOS ›](https://apps.apple.com/app/outline-app/id1356178125) | Get Outline on the Mac App Store | | [Linux ›](https://s3.amazonaws.com/outline-releases/client/linux/stable/Outline-Client.AppImage) | Download Outline.AppImage, [make it executable](https://docs.appimage.org/introduction/quickstart.html), then double click to launch. | ## 3. Add server and connect