Skip to content

Commit

Permalink
Merge branch 'master' into junyi/modern-linux-routing
Browse files Browse the repository at this point in the history
  • Loading branch information
jyyi1 committed Dec 4, 2024
2 parents d91d388 + 4aa6c2d commit 0831f68
Show file tree
Hide file tree
Showing 19 changed files with 186 additions and 223 deletions.
28 changes: 13 additions & 15 deletions client/electron/go_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<string> {
if (!invokeGoAPIFunc) {
if (!invokeMethodFunc) {
const backendLib = koffi.load(pathToBackendLibrary());

// Define C strings and setup auto release
Expand All @@ -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);
}
Expand Down
16 changes: 8 additions & 8 deletions client/electron/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string> =>
invokeGoApi(api, input)
'outline-ipc-invoke-method',
(_, method: string, params: string): Promise<string> =>
invokeMethod(method, params)
);

// Connects to a proxy server specified by a config.
Expand Down
42 changes: 15 additions & 27 deletions client/go/outline/electron/go_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ package main
/*
#include <stdlib.h> // 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.
Expand All @@ -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 (
Expand All @@ -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].
Expand All @@ -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{

Check failure on line 74 in client/go/outline/electron/go_plugin.go

View workflow job for this annotation

GitHub Actions / Go Backend Test

could not determine kind of name for C.InvokeGoAPIResult
Expand All @@ -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)}
}
}

Expand Down
26 changes: 9 additions & 17 deletions client/go/outline/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
8 changes: 4 additions & 4 deletions client/go/outline/fetch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestFetchResource(t *testing.T) {
}))
defer server.Close()

result := FetchResource(server.URL)
result := fetchResource(server.URL)

Check failure on line 33 in client/go/outline/fetch_test.go

View workflow job for this annotation

GitHub Actions / Go Backend Test

assignment mismatch: 1 variable but fetchResource returns 2 values
require.Nil(t, result.Error)
require.Equal(t, "{\"name\": \"my-test-key\"}\n", result.Content)
}
Expand All @@ -55,7 +55,7 @@ func TestFetchResource_Redirection(t *testing.T) {
}))
defer redirSvr.Close()

result := FetchResource(redirSvr.URL)
result := fetchResource(redirSvr.URL)

Check failure on line 58 in client/go/outline/fetch_test.go

View workflow job for this annotation

GitHub Actions / Go Backend Test

assignment mismatch: 1 variable but fetchResource returns 2 values
require.Nil(t, result.Error)
require.Equal(t, "ss://my-url-format-test-key\n", result.Content)
}
Expand All @@ -78,7 +78,7 @@ func TestFetchResource_HTTPStatusError(t *testing.T) {
}))
defer server.Close()

result := FetchResource(server.URL)
result := fetchResource(server.URL)

Check failure on line 81 in client/go/outline/fetch_test.go

View workflow job for this annotation

GitHub Actions / Go Backend Test

assignment mismatch: 1 variable but fetchResource returns 2 values
require.Error(t, result.Error)
require.Equal(t, platerrors.FetchConfigFailed, result.Error.Code)
require.Error(t, result.Error.Cause)
Expand All @@ -91,7 +91,7 @@ func TestFetchResource_BodyReadError(t *testing.T) {
}))
defer server.Close()

result := FetchResource(server.URL)
result := fetchResource(server.URL)

Check failure on line 94 in client/go/outline/fetch_test.go

View workflow job for this annotation

GitHub Actions / Go Backend Test

assignment mismatch: 1 variable but fetchResource returns 2 values
require.Error(t, result.Error)
require.Equal(t, platerrors.FetchConfigFailed, result.Error.Code)
require.Error(t, result.Error.Cause)
Expand Down
56 changes: 56 additions & 0 deletions client/go/outline/method_channel.go
Original file line number Diff line number Diff line change
@@ -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),
}}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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");
Expand Down Expand Up @@ -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);
Expand All @@ -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)) {
Expand Down
Loading

0 comments on commit 0831f68

Please sign in to comment.