diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-08-20 16:27:55 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-08-20 16:27:55 +0200 |
| commit | 53ae95ac6e58c074c7bd7bb6ea25ad91999056f9 (patch) | |
| tree | ed6e03b73629cd06ae9bffa18420e0669be7dcb1 /gui | |
| parent | 60a7b74ac3230ac33f9b45147985915a9b7ab4df (diff) | |
| parent | 11c448b7adc1092e9cc5a05e6029d49e1ca47af1 (diff) | |
| download | mullvadvpn-53ae95ac6e58c074c7bd7bb6ea25ad91999056f9.tar.xz mullvadvpn-53ae95ac6e58c074c7bd7bb6ea25ad91999056f9.zip | |
Merge branch 'remove-jsonrpc-code-and-dependencies' into master
Diffstat (limited to 'gui')
| -rw-r--r-- | gui/package-lock.json | 38 | ||||
| -rw-r--r-- | gui/package.json | 3 | ||||
| -rw-r--r-- | gui/src/main/jsonrpc-client.ts | 450 | ||||
| -rw-r--r-- | gui/test/jsonrpc-transport.spec.ts | 122 |
4 files changed, 0 insertions, 613 deletions
diff --git a/gui/package-lock.json b/gui/package-lock.json index aaffe0d2b0..96c793cf13 100644 --- a/gui/package-lock.json +++ b/gui/package-lock.json @@ -726,26 +726,6 @@ "integrity": "sha512-hkgzYF+qnIl8uTO8rmUSVSfQ8BIfMXC4yJAF4n8BE758YsKBZvFC4NumnAegj7KmylP0liEZNpb9RRGFMbFejA==", "dev": true }, - "@types/stream-chain": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/stream-chain/-/stream-chain-2.0.0.tgz", - "integrity": "sha512-O3IRJcZi4YddlS8jgasH87l+rdNmad9uPAMmMZCfRVhumbWMX6lkBWnIqr9kokO5sx8LHp8peQ1ELhMZHbR0Gg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/stream-json": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/stream-json/-/stream-json-1.0.0.tgz", - "integrity": "sha512-W9B6R5GPbTq72Oz/oJZ8qSl3+6g2xbBK0et+pK/LwGpoSWsHh2AVEYLuQUBUzg+ps4xZGdYJP5nqsW17DFbxzQ==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/node": "*", - "@types/stream-chain": "*" - } - }, "@types/styled-components": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.0.tgz", @@ -8052,11 +8032,6 @@ "graceful-fs": "^4.1.6" } }, - "jsonrpc-lite": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/jsonrpc-lite/-/jsonrpc-lite-2.0.7.tgz", - "integrity": "sha512-BzDgvW9iZzVS0hgWaoM1RhBg4eRDqy+JjJtD2+23MbWd+H30ld8qWkUW1LtnHliL4QlYwrYur8ZZkVpiyPkrYQ==" - }, "jsx-ast-utils": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz", @@ -11834,11 +11809,6 @@ "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", "dev": true }, - "stream-chain": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.1.0.tgz", - "integrity": "sha512-PAUXdRGm0G8P0+/+JEd3O9kfmB9kwmr2nKIc5zhcsHn0KdBByD5PJ2po21iDzc+TZsOSEbU8j4JbAevJsZkLyQ==" - }, "stream-combiner": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", @@ -11854,14 +11824,6 @@ "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", "dev": true }, - "stream-json": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.3.0.tgz", - "integrity": "sha512-3qLDv/xnwmleb5kssgmKbGPqcLou2tbFIgj3CM3fy5PxKaAmeCv103Zp4LKIALdE30zMHTJn09yVwxq773btaw==", - "requires": { - "stream-chain": "^2.1.0" - } - }, "stream-shift": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", diff --git a/gui/package.json b/gui/package.json index 68aae86602..3e4a1aa25f 100644 --- a/gui/package.json +++ b/gui/package.json @@ -20,7 +20,6 @@ "gettext-parser": "^4.0.3", "google-protobuf": "^4.0.0-rc.2", "history": "^4.6.1", - "jsonrpc-lite": "^2.0.7", "linux-app-list": "^1.0.1", "mkdirp": "^1.0.3", "moment": "^2.24.0", @@ -34,7 +33,6 @@ "reactxp": "^2.0.0", "redux": "^4.0.5", "sprintf-js": "^1.1.2", - "stream-json": "^1.3.0", "styled-components": "^5.1.0", "uuid": "^3.0.1", "validated": "^2.0.1" @@ -62,7 +60,6 @@ "@types/react-simple-maps": "^0.12.1", "@types/sinon": "^7.0.5", "@types/sprintf-js": "^1.1.2", - "@types/stream-json": "^1.0.0", "@types/styled-components": "^5.1.0", "@types/topojson-specification": "^1.0.1", "@types/uuid": "^3.4.4", diff --git a/gui/src/main/jsonrpc-client.ts b/gui/src/main/jsonrpc-client.ts deleted file mode 100644 index e7e8721804..0000000000 --- a/gui/src/main/jsonrpc-client.ts +++ /dev/null @@ -1,450 +0,0 @@ -import assert from 'assert'; -import log from 'electron-log'; -import { EventEmitter } from 'events'; -import jsonrpc from 'jsonrpc-lite'; -import * as net from 'net'; -import StreamValues from 'stream-json/streamers/StreamValues'; -import * as uuid from 'uuid'; - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -export interface IUnansweredRequest { - resolve: (value: any) => void; - reject: (value: any) => void; - timerId: NodeJS.Timeout; - message: object; -} - -export interface IJsonRpcErrorResponse { - type: 'error'; - payload: { - id: string; - error: { - code: number; - message: string; - }; - }; -} -export interface IJsonRpcNotification { - type: 'notification'; - payload: { - method: string; - params: { - subscription: string; - result: any; - }; - }; -} -export interface IJsonRpcSuccess { - type: 'success'; - payload: { - id: string; - result: any; - }; -} -export type JsonRpcMessage = IJsonRpcErrorResponse | IJsonRpcNotification | IJsonRpcSuccess; - -export class RemoteError extends Error { - constructor(private codeValue: number, private detailsValue: string) { - super(`Remote JSON-RPC error ${codeValue}: ${detailsValue}`); - } - - get code(): number { - return this.codeValue; - } - - get details(): string { - return this.detailsValue; - } -} - -export class TimeOutError extends Error { - constructor(private jsonRpcMessageValue: object) { - super('Request timed out'); - } - - get jsonRpcMessage(): object { - return this.jsonRpcMessageValue; - } -} - -export class SubscriptionError extends Error { - constructor(message: string, private replyValue: any) { - super(`${message}: ${JSON.stringify(replyValue)}`); - } - - get reply(): any { - return this.replyValue; - } -} - -export class WebSocketError extends Error { - get code(): number { - return this.codeValue; - } - - private static reason(code: number): string { - switch (code) { - case 1006: - return 'Abnormal closure'; - case 1011: - return 'Internal error'; - case 1012: - return 'Service restart'; - case 1014: - return 'Bad gateway'; - default: - return `Unknown (${code})`; - } - } - constructor(private codeValue: number) { - super(WebSocketError.reason(codeValue)); - } -} - -export class TransportError extends Error {} - -const DEFAULT_TIMEOUT_MILLIS = 5000; - -export default class JsonRpcClient<T> extends EventEmitter { - private unansweredRequests: Map<string, IUnansweredRequest> = new Map(); - private subscriptions: Map<string | number, (value: any) => void> = new Map(); - private transport: ITransport<T>; - - constructor(transport: ITransport<T>) { - super(); - - this.transport = transport; - } - - /// Connect websocket - public connect(connectionParams: T): Promise<void> { - return new Promise((resolve, reject) => { - this.disconnect(); - - log.info('Connecting to transport with params', connectionParams); - - // A flag used to determine if Promise was resolved. - let isPromiseResolved = false; - - const transport = this.transport; - - transport.onOpen = () => { - log.info('Transport is connected'); - this.emit('open'); - - // Resolve the Promise - resolve(); - isPromiseResolved = true; - }; - - transport.onMessage = (obj) => { - this.onMessage(obj); - }; - - transport.onClose = (error?: Error) => { - // Remove all subscriptions since they are connection based - this.subscriptions.clear(); - - this.emit('close', error); - - // Prevent rejecting a previously resolved Promise. - if (!isPromiseResolved) { - reject(error); - } - }; - transport.connect(connectionParams); - - this.transport = transport; - }); - } - - public disconnect() { - if (this.transport) { - this.transport.close(); - } - } - - public async subscribe(event: string, listener: (value: any) => void): Promise<string | number> { - log.silly(`Adding a listener for ${event}`); - - try { - const subscriptionId = await this.send(`${event}_subscribe`); - - if (typeof subscriptionId === 'string' || typeof subscriptionId === 'number') { - this.subscriptions.set(subscriptionId, listener); - } else { - throw new SubscriptionError( - 'The subscription id was not a string or a number', - subscriptionId, - ); - } - - return subscriptionId; - } catch (e) { - log.error(`Failed adding listener to ${event}: ${e.message}`); - throw e; - } - } - - public async unsubscribe(event: string, subscriptionId: string | number): Promise<void> { - log.silly(`Removing a listener for ${event}`); - - try { - if (this.subscriptions.has(subscriptionId)) { - await this.send(`${event}_unsubscribe`, [subscriptionId]); - } - } catch (e) { - log.error(`Failed removing listener to ${event}: ${e.message}`); - throw e; - } finally { - this.subscriptions.delete(subscriptionId); - } - } - - public send(action: string, data?: any, timeout: number = DEFAULT_TIMEOUT_MILLIS): Promise<any> { - return new Promise((resolve, reject) => { - const transport = this.transport; - if (!transport) { - reject(new Error('RPC client transport is not connected.')); - return; - } - - const id = uuid.v4(); - const payload = this.prepareParams(data); - const timerId = global.setTimeout(() => this.onTimeout(id), timeout); - const message = jsonrpc.request(id, action, payload); - this.unansweredRequests.set(id, { - resolve, - reject, - timerId, - message, - }); - - try { - log.silly('Sending message', id, action); - transport.send(JSON.stringify(message)); - } catch (error) { - log.error(`Failed sending RPC message "${action}": ${error.message}`); - - // clean up on error - this.unansweredRequests.delete(id); - clearTimeout(timerId); - - throw error; - } - }); - } - - private prepareParams(data?: any): any[] | object { - // JSONRPC only accepts arrays and objects as params, but - // this isn't very nice to use, so this method wraps other - // types in an array. The choice of array is based on try-and-error - - if (data === undefined) { - return []; - } else if (data === null) { - return [null]; - } else if (Array.isArray(data) || typeof data === 'object') { - return data; - } else { - return [data]; - } - } - - private onTimeout(requestId: string) { - const request = this.unansweredRequests.get(requestId); - - this.unansweredRequests.delete(requestId); - - if (request) { - log.warn(`Request ${requestId} timed out: `, request.message); - request.reject(new TimeOutError(request.message)); - } else { - log.warn(`Request ${requestId} timed out but it seems to already have been answered`); - } - } - - private onMessage(obj: object) { - let message: ReturnType<typeof jsonrpc.parseObject>; - try { - message = jsonrpc.parseObject(obj); - } catch (error) { - log.error(`Failed to parse JSON-RPC message: ${error} for object`); - return; - } - - if (message.type === 'notification') { - this.onNotification(message as IJsonRpcNotification); - } else { - this.onReply(message as IJsonRpcErrorResponse | IJsonRpcSuccess); - } - } - - private onNotification(message: IJsonRpcNotification) { - const subscriptionId = message.payload.params.subscription; - const listener = this.subscriptions.get(subscriptionId); - - if (listener) { - log.silly(`Got notification for ${message.payload.method}`); - listener(message.payload.params.result); - } else { - log.warn(`Got notification for ${message.payload.method} but no one is listening for it`); - } - } - - private onReply(message: IJsonRpcErrorResponse | IJsonRpcSuccess) { - const id = message.payload.id; - const request = this.unansweredRequests.get(id); - this.unansweredRequests.delete(id); - - if (request) { - log.silly('Got answer to', id, message.type); - - clearTimeout(request.timerId); - - if (message.type === 'error') { - const error = message.payload.error; - request.reject(new RemoteError(error.code, error.message)); - } else { - const reply = message.payload.result; - request.resolve(reply); - } - } else { - log.warn(`Got reply to ${id} but no one was waiting for it`); - } - } -} - -export interface ITransport<T> { - onOpen: () => void; - onMessage: (data: object) => void; - onClose: (error?: Error) => void; - close(): void; - send(message: string): void; - connect(params: T): void; -} - -// Given the correct parameters, this transport supports named pipes/unix -// domain sockets, and also TCP/UDP sockets -export class SocketTransport implements ITransport<{ path: string }> { - private connection?: net.Socket; - private jsonStream?: NodeJS.ReadWriteStream; - private socketReady = false; - private lastError?: Error; - public onMessage = (_message: object) => { - // no-op - }; - public onClose = (_error?: Error) => { - // no-op - }; - public onOpen = () => { - // no-op - }; - - public connect(options: { path: string }) { - assert(!this.connection, 'Make sure to close the existing socket'); - - const jsonStream = StreamValues.withParser() - .on('data', this.onJsonStreamData) - .once('error', this.onJsonStreamError); - - const connection = new net.Socket() - .once('ready', this.onSocketReady) - .once('error', this.onSocketError) - .once('close', this.onSocketClose); - - this.connection = connection; - this.jsonStream = jsonStream; - this.socketReady = false; - this.lastError = undefined; - - log.debug('Connect socket'); - - connection.pipe(jsonStream); - connection.connect(options); - } - - public close() { - if (this.connection) { - log.debug('Close socket'); - - this.cleanupConnection(true); - - this.onClose(); - } - } - - public send(msg: string) { - if (this.socketReady && this.connection) { - this.connection.write(msg); - } else { - throw new TransportError('Socket not connected'); - } - } - - private onSocketReady = () => { - this.socketReady = true; - - log.debug('Socket is ready'); - - this.onOpen(); - }; - - private onSocketError = (error: Error) => { - this.lastError = error; - - log.error('Socket error: ', error); - }; - - private onSocketClose = (hadError: boolean) => { - this.cleanupConnection(false); - - if (hadError) { - log.debug(`Socket was closed due to an error: `, this.lastError); - - this.onClose(this.lastError); - } else { - log.debug(`Socket was closed by peer`); - - this.onClose(new TransportError('Socket was closed by peer')); - } - }; - - private onJsonStreamData = (data: { key: number; value: any }) => { - this.onMessage(data.value); - }; - - private onJsonStreamError = (error: Error) => { - log.error('Socket JSON stream error: ', error); - - if (this.connection) { - // This will destroy the socket and emit "error" and "close" events - this.connection.destroy(error); - } - }; - - private cleanupConnection(shouldClose: boolean) { - // closing socket is not synchronous, so remove all of the event handlers first - this.connection!.removeListener('ready', this.onSocketReady) - .removeListener('error', this.onSocketError) - .removeListener('close', this.onSocketClose); - - this.jsonStream!.removeListener('data', this.onJsonStreamData).removeListener( - 'error', - this.onJsonStreamError, - ); - - if (shouldClose) { - try { - this.connection!.end(); - } catch (error) { - log.error('Failed to close the socket: ', error); - } - } - - this.connection = undefined; - this.jsonStream = undefined; - this.socketReady = false; - } -} diff --git a/gui/test/jsonrpc-transport.spec.ts b/gui/test/jsonrpc-transport.spec.ts deleted file mode 100644 index 26d812b793..0000000000 --- a/gui/test/jsonrpc-transport.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { expect } from 'chai'; -import { it, describe, beforeEach } from 'mocha'; -import jsonrpc from 'jsonrpc-lite'; -import JsonRpcClient, { ITransport, TimeOutError } from '../src/main/jsonrpc-client'; - -describe('JSON RPC transport', () => { - let client: JsonRpcClient<string>, transport: MockTransport; - - beforeEach(() => { - transport = new MockTransport(); - client = new JsonRpcClient(transport); - return client.connect(''); - }); - - it('should reject failed jsonrpc requests', async () => { - transport.onServerMessage = (msg) => { - const parsedMessage = jsonrpc.parseObject(msg); - - if (parsedMessage.type === 'request' && parsedMessage.payload.method === 'invalid-method') { - transport.reply( - JSON.stringify( - jsonrpc.error( - parsedMessage.payload.id, - new jsonrpc.JsonRpcError('Method not found', -32601), - ), - ), - ); - } - }; - - const sendPromise = client.send('invalid-method'); - - return expect(sendPromise).to.eventually.be.rejectedWith('Method not found'); - }); - - it('should route reply to correct promise', async () => { - transport.onServerMessage = (msg) => { - const parsedMessage = jsonrpc.parseObject(msg); - - if (parsedMessage.type === 'request' && parsedMessage.payload.method === 'a message') { - transport.reply(JSON.stringify(jsonrpc.success(parsedMessage.payload.id, 'a reply'))); - } - }; - - const decoyPromise = client.send('a decoy', [], 100); - const messagePromise = client.send('a message', [], 100); - - return Promise.all([ - expect(messagePromise).to.eventually.be.equal('a reply'), - expect(decoyPromise).to.eventually.be.rejectedWith(TimeOutError), - ]); - }); - - it('should timeout if no response is returned', async () => { - const sendPromise = client.send('timeout-message', {}, 1); - - return expect(sendPromise).to.eventually.be.rejectedWith(TimeOutError, 'Request timed out'); - }); - - it('should route notifications', async () => { - transport.onServerMessage = (msg) => { - const parsedMessage = jsonrpc.parseObject(msg); - - if (parsedMessage.type === 'request' && parsedMessage.payload.method === 'event_subscribe') { - transport.reply(JSON.stringify(jsonrpc.success(parsedMessage.payload.id, 1))); - } - }; - - const eventPromiseHelper = (() => { - let borrowedResolve: ((param: any) => void) | undefined = undefined; - const promise = new Promise((resolve) => (borrowedResolve = resolve)); - /* Flow does not understand that the body of Promise runs immediately. - see https://github.com/facebook/flow/issues/6711 */ - if (!borrowedResolve) { - throw new Error(); - } - return { - resolve: borrowedResolve, - promise, - }; - })(); - - await client.subscribe('event', eventPromiseHelper.resolve); - - transport.reply( - JSON.stringify(jsonrpc.notification('event', { subscription: 1, result: 'beacon' })), - ); - - return expect(eventPromiseHelper.promise).to.eventually.be.equal('beacon'); - }); -}); - -class MockTransport implements ITransport<string> { - public onOpen = () => { - // no-op - }; - public onMessage = (_message: object) => { - // no-op - }; - public onServerMessage = (_message: object) => { - // no-op - }; - public onClose = (_error?: Error) => { - // no-op - }; - - public close() { - this.onClose(); - } - - public send(msg: string) { - this.onServerMessage(JSON.parse(msg)); - } - - public reply(msg: string) { - this.onMessage(JSON.parse(msg)); - } - - public connect(_params: string) { - this.onOpen(); - } -} |
