Skip to content

Commit

Permalink
Define Tunnel interface in typescript (#922)
Browse files Browse the repository at this point in the history
  • Loading branch information
alalamav committed Dec 14, 2020
1 parent eab8703 commit da57869
Show file tree
Hide file tree
Showing 14 changed files with 120 additions and 62 deletions.
11 changes: 5 additions & 6 deletions src/electron/index.ts
Expand Up @@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

/// <reference path="../types/ambient/outlinePlugin.d.ts" />

import * as sentry from '@sentry/electron';
import {app, BrowserWindow, ipcMain, Menu, MenuItemConstructorOptions, nativeImage, shell, Tray} from 'electron';
import * as promiseIpc from 'electron-promise-ipc';
Expand All @@ -27,6 +25,8 @@ import autoLaunch = require('auto-launch'); // tslint:disable-line
import * as connectivity from './connectivity';
import * as errors from '../www/model/errors';

import {ShadowsocksConfig} from '../www/model/shadowsocks';
import {TunnelStatus} from '../www/app/tunnel';
import {TunnelStore, SerializableTunnel} from './tunnel_store';
import {TunnelManager} from './process_manager';

Expand Down Expand Up @@ -282,7 +282,7 @@ app.on('activate', () => {
}
});

promiseIpc.on('is-reachable', (config: cordova.plugins.outline.ServerConfig) => {
promiseIpc.on('is-reachable', (config: ShadowsocksConfig) => {
return connectivity
.isServerReachable(config.host || '', config.port || 0, REACHABILITY_TIMEOUT_MS)
.then(() => {
Expand All @@ -294,8 +294,7 @@ promiseIpc.on('is-reachable', (config: cordova.plugins.outline.ServerConfig) =>
});

// Invoked by both the start-proxying event handler and auto-connect.
async function startVpn(
config: cordova.plugins.outline.ServerConfig, id: string, isAutoConnect = false) {
async function startVpn(config: ShadowsocksConfig, id: string, isAutoConnect = false) {
if (currentTunnel) {
throw new Error('already connected');
}
Expand Down Expand Up @@ -363,7 +362,7 @@ function sendTunnelStatus(status: TunnelStatus, tunnelId: string) {

// Connects to the specified server, if that server is reachable and the credentials are valid.
promiseIpc.on(
'start-proxying', async (args: {config: cordova.plugins.outline.ServerConfig, id: string}) => {
'start-proxying', async (args: {config: ShadowsocksConfig, id: string}) => {
// TODO: Rather than first disconnecting, implement a more efficient switchover (as well as
// being faster, this would help prevent traffic leaks - the Cordova clients already do
// this).
Expand Down
7 changes: 4 additions & 3 deletions src/electron/process_manager.ts
Expand Up @@ -16,7 +16,9 @@ import {ChildProcess, execSync, spawn} from 'child_process';
import {powerMonitor} from 'electron';
import {platform} from 'os';

import {TunnelStatus} from '../www/app/tunnel';
import * as errors from '../www/model/errors';
import {ShadowsocksConfig} from '../www/model/shadowsocks';

import {checkUdpForwardingEnabled, isServerReachable, validateServerCredentials} from './connectivity';
import {RoutingDaemon} from './routing_service';
Expand Down Expand Up @@ -116,8 +118,7 @@ export class TunnelManager {

private reconnectedListener?: () => void;

constructor(
private config: cordova.plugins.outline.ServerConfig, private isAutoConnect: boolean) {
constructor(private config: ShadowsocksConfig, private isAutoConnect: boolean) {
this.routing = new RoutingDaemon(config.host || '', isAutoConnect);

// This trio of Promises, each tied to a helper process' exit, is key to the instance's
Expand Down Expand Up @@ -327,7 +328,7 @@ class SsLocal extends ChildProcessHelper {
super(pathToEmbeddedBinary('shadowsocks-libev', 'ss-local'));
}

start(config: cordova.plugins.outline.ServerConfig) {
start(config: ShadowsocksConfig) {
// ss-local -s x.x.x.x -p 65336 -k mypassword -m aes-128-cfb -l 1081 -u
const args = ['-l', this.proxyPort.toString()];
args.push('-s', config.host || '');
Expand Down
1 change: 1 addition & 0 deletions src/electron/routing_service.ts
Expand Up @@ -18,6 +18,7 @@ import * as sudo from 'sudo-prompt';

import * as errors from '../www/model/errors';

import {TunnelStatus} from '../www/app/tunnel';
import {getServiceStartCommand} from './util';

const SERVICE_NAME =
Expand Down
4 changes: 3 additions & 1 deletion src/electron/tunnel_store.ts
Expand Up @@ -15,10 +15,12 @@
import * as fs from 'fs';
import * as path from 'path';

import {ShadowsocksConfig} from '../www/model/shadowsocks';

// Format to store a tunnel configuration.
export interface SerializableTunnel {
id: string;
config: cordova.plugins.outline.ServerConfig;
config: ShadowsocksConfig;
isUdpSupported?: boolean;
}

Expand Down
41 changes: 9 additions & 32 deletions src/types/ambient/outlinePlugin.d.ts
Expand Up @@ -14,13 +14,9 @@

// Typings for cordova-plugin-outline

// This enum doesn't logically belong in this file - ideally, it would live in "regular" code (most
// likely somewhere in model). However, since we need to reference it from a typings file, it must
// be defined in a typings file.
//
// Additionally, because this is a typings file, we must declare a *const* enum - regular enums are
// backed, perhaps surprisingly, by a JavaScript object.
declare const enum TunnelStatus { CONNECTED, DISCONNECTED, RECONNECTING }
declare type Tunnel = import('../../www/app/tunnel').Tunnel;
declare type TunnelStatus = import('../../www/app/tunnel').TunnelStatus;
declare type ShadowsocksConfig = import('../../www/model/shadowsocks').ShadowsocksConfig;

declare namespace cordova.plugins.outline {
const log: {
Expand All @@ -36,43 +32,24 @@ declare namespace cordova.plugins.outline {
// Quits the application. Only supported in macOS.
function quitApplication(): void;

// Represents a Shadowsocks server configuration.
interface ServerConfig {
method?: string;
password?: string;
host?: string;
port?: number;
name?: string;
}

// Represents a VPN tunnel to a proxy server.
class Tunnel {
// Creates a new instance with |serverConfig|.
// A sequential ID will be generated if |id| is absent.
constructor(serverConfig: ServerConfig, id?: string);
// Implements the Tunnel interface with native functionality.
class Tunnel implements Tunnel {
// Creates a new instance with `config`.
// A sequential ID will be generated if `id` is absent.
constructor(config: ShadowsocksConfig, id?: string);

config: ServerConfig;
config: ShadowsocksConfig;

readonly id: string;

// Starts the VPN service, and tunnels all the traffic to a local Shadowsocks
// server as dictated by its configuration. If there is another running
// instance, broadcasts a disconnect event and stops the running tunnel.
// In such case, restarts tunneling while preserving the VPN tunnel.
// Rejects with an OutlinePluginError.
start(): Promise<void>;

// Stops the tunnel and VPN service.
stop(): Promise<void>;

// Returns whether the tunnel instance is active.
isRunning(): Promise<boolean>;

// Returns whether the proxy server is reachable by attempting to establish
// a socket to the IP and port specified in |config|.
isReachable(): Promise<boolean>;

// Sets a listener, to be called when the VPN tunnel status changes.
onStatusChange(listener: (status: TunnelStatus) => void): void;
}
}
4 changes: 2 additions & 2 deletions src/www/app/cordova_main.ts
Expand Up @@ -18,6 +18,7 @@
import * as sentry from '@sentry/browser';

import {EventQueue} from '../model/events';
import {ServerConfig} from '../model/server';

import {AbstractClipboard, Clipboard, ClipboardListener} from './clipboard';
import {EnvironmentVariables} from './environment';
Expand Down Expand Up @@ -67,8 +68,7 @@ class CordovaPlatform implements OutlinePlatform {
}

getPersistentServerFactory() {
return (serverId: string, config: cordova.plugins.outline.ServerConfig,
eventQueue: EventQueue) => {
return (serverId: string, config: ServerConfig, eventQueue: EventQueue) => {
return new OutlineServer(
serverId, config,
this.hasDeviceSupport() ? new cordova.plugins.outline.Tunnel(config, serverId) :
Expand Down
4 changes: 2 additions & 2 deletions src/www/app/electron_main.ts
Expand Up @@ -17,6 +17,7 @@ import {clipboard, ipcRenderer} from 'electron';
import * as os from 'os';

import {EventQueue} from '../model/events';
import {ServerConfig} from '../model/server';

import {AbstractClipboard, Clipboard, ClipboardListener} from './clipboard';
import {ElectronOutlineTunnel} from './electron_outline_tunnel';
Expand Down Expand Up @@ -90,8 +91,7 @@ main({
return isOsSupported;
},
getPersistentServerFactory: () => {
return (serverId: string, config: cordova.plugins.outline.ServerConfig,
eventQueue: EventQueue) => {
return (serverId: string, config: ServerConfig, eventQueue: EventQueue) => {
return new OutlineServer(
serverId, config,
isOsSupported ? new ElectronOutlineTunnel(config, serverId) :
Expand Down
8 changes: 5 additions & 3 deletions src/www/app/electron_outline_tunnel.ts
Expand Up @@ -16,14 +16,16 @@ import {ipcRenderer} from 'electron';
import * as promiseIpc from 'electron-promise-ipc';

import * as errors from '../model/errors';
import {ShadowsocksConfig} from '../model/shadowsocks';

export class ElectronOutlineTunnel implements cordova.plugins.outline.Tunnel {
import {Tunnel, TunnelStatus} from './tunnel';

export class ElectronOutlineTunnel implements Tunnel {
private statusChangeListener: ((status: TunnelStatus) => void)|null = null;

private running = false;

constructor(public config: cordova.plugins.outline.ServerConfig, public id: string) {
const serverName = this.config.name || this.config.host || '';
constructor(public config: ShadowsocksConfig, public id: string) {
// This event is received when the proxy connects. It is mainly used for signaling the UI that
// the proxy has been automatically connected at startup (if the user was connected at shutdown)
ipcRenderer.on(`proxy-connected-${this.id}`, (e: Event) => {
Expand Down
10 changes: 6 additions & 4 deletions src/www/app/fake_tunnel.ts
Expand Up @@ -12,16 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.

/// <reference path='../../types/ambient/outlinePlugin.d.ts'/>

import * as errors from '../model/errors';
import {ShadowsocksConfig} from '../model/shadowsocks';

import {Tunnel, TunnelStatus} from './tunnel';

// Fake Tunnel implementation for demoing and testing.
// Note that because this implementation does not emit disconnection events, "switching" between
// servers in the server list will not work as expected.
export class FakeOutlineTunnel implements cordova.plugins.outline.Tunnel {
export class FakeOutlineTunnel implements Tunnel {
private running = false;

constructor(public config: cordova.plugins.outline.ServerConfig, public id: string) {}
constructor(public config: ShadowsocksConfig, public id: string) {}

private playBroken() {
return this.config.name?.toLowerCase().includes('broken');
Expand Down
8 changes: 4 additions & 4 deletions src/www/app/outline_server.ts
Expand Up @@ -12,13 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.

/// <reference path='../../types/ambient/outlinePlugin.d.ts'/>

import * as errors from '../model/errors';
import * as events from '../model/events';
import {Server} from '../model/server';
import {ShadowsocksConfig} from '../model/shadowsocks';

import {PersistentServer} from './persistent_server';
import {Tunnel, TunnelStatus} from './tunnel';

export class OutlineServer implements PersistentServer {
// We restrict to AEAD ciphers because unsafe ciphers are not supported in go-tun2socks.
Expand All @@ -27,8 +27,8 @@ export class OutlineServer implements PersistentServer {
['chacha20-ietf-poly1305', 'aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm'];

constructor(
public readonly id: string, public config: cordova.plugins.outline.ServerConfig,
private tunnel: cordova.plugins.outline.Tunnel, private eventQueue: events.EventQueue) {
public readonly id: string, public config: ShadowsocksConfig, private tunnel: Tunnel,
private eventQueue: events.EventQueue) {
this.tunnel.onStatusChange((status: TunnelStatus) => {
let statusEvent: events.OutlineEvent;
switch (status) {
Expand Down
4 changes: 2 additions & 2 deletions src/www/app/persistent_server.ts
Expand Up @@ -16,10 +16,10 @@ import * as uuidv4 from 'uuidv4';

import {ServerAlreadyAdded, ShadowsocksUnsupportedCipher} from '../model/errors';
import * as events from '../model/events';
import {Server, ServerRepository} from '../model/server';
import {Server, ServerConfig, ServerRepository} from '../model/server';

import {OutlineServer} from "./outline_server";

type ServerConfig = cordova.plugins.outline.ServerConfig;

export interface PersistentServer extends Server {
config: ServerConfig;
Expand Down
50 changes: 50 additions & 0 deletions src/www/app/tunnel.ts
@@ -0,0 +1,50 @@
// Copyright 2020 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 {ShadowsocksConfig} from '../../www/model/shadowsocks';

export const enum TunnelStatus {
CONNECTED,
DISCONNECTED,
RECONNECTING
}

// Represents a VPN tunnel to a Shadowsocks proxy server. Implementations provide native tunneling
// functionality through cordova.plugins.oultine.Tunnel and ElectronOutlineTunnel.
export interface Tunnel {
// Unique instance identifier.
readonly id: string;

// Shadowsocks proxy configuration.
config: ShadowsocksConfig;

// Connects a VPN, routing all device traffic to a Shadowsocks server as dictated by `config`.
// If there is another running instance, broadcasts a disconnect event and stops the active
// tunnel. In such case, restarts tunneling while preserving the VPN.
// Throws OutlinePluginError.
start(): Promise<void>;

// Stops the tunnel and VPN service.
stop(): Promise<void>;

// Returns whether the tunnel instance is active.
isRunning(): Promise<boolean>;

// Returns whether the proxy server is reachable by attempting to open a TCP socket
// to the IP and port specified in `config`.
isReachable(): Promise<boolean>;

// Sets a listener, to be called when the tunnel status changes.
onStatusChange(listener: (status: TunnelStatus) => void): void;
}
8 changes: 5 additions & 3 deletions src/www/model/server.ts
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {ShadowsocksConfig} from './shadowsocks';

// TODO: add guidelines for this file

export interface Server {
Expand All @@ -38,10 +40,10 @@ export interface Server {
checkReachable(): Promise<boolean>;
}

export type ServerConfig = ShadowsocksConfig;

export interface ServerRepository {
// TODO: change object to cordova.plugins.uproyx.ServerConfig once we decouple the definition from
// cordova-plugin-outline
add(serverConfig: {}): void;
add(serverConfig: ServerConfig): void;
forget(serverId: string): void;
undoForget(serverId: string): void;
getAll(): Server[];
Expand Down
22 changes: 22 additions & 0 deletions src/www/model/shadowsocks.ts
@@ -0,0 +1,22 @@
// Copyright 2020 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.

// Represents a Shadowsocks proxy server configuration.
export interface ShadowsocksConfig {
host?: string;
port?: number;
password?: string;
method?: string;
name?: string;
}

0 comments on commit da57869

Please sign in to comment.