summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls Piņķis <emils@mullvad.net>2018-08-29 11:20:57 +0100
committerEmīls Piņķis <emils@mullvad.net>2018-08-29 16:28:17 +0100
commitcff2be6a76823fa250df6b3a69a796093634dbc6 (patch)
tree366dd4be601188cb3ce2fee621a5d2500d9e7a30
parent0dd43272e4473dbd01ef39ab8bfbb454c47487da (diff)
downloadmullvadvpn-cff2be6a76823fa250df6b3a69a796093634dbc6.tar.xz
mullvadvpn-cff2be6a76823fa250df6b3a69a796093634dbc6.zip
Adjust frontend to use IPC rather than WebSockets
-rw-r--r--gui/packages/desktop/package.json4
-rw-r--r--gui/packages/desktop/src/common/types.js6
-rw-r--r--gui/packages/desktop/src/main/index.js40
-rw-r--r--gui/packages/desktop/src/main/rpc-address-file.js112
-rw-r--r--gui/packages/desktop/src/main/tempdir.js20
-rw-r--r--gui/packages/desktop/src/renderer/app.js41
-rw-r--r--gui/packages/desktop/src/renderer/lib/daemon-rpc.js13
-rw-r--r--gui/packages/desktop/src/renderer/lib/jsonrpc-client.js (renamed from gui/packages/desktop/src/renderer/lib/jsonrpc-transport.js)205
-rw-r--r--gui/packages/desktop/test/jsonrpc-transport.spec.js11
-rw-r--r--gui/yarn.lock19
10 files changed, 203 insertions, 268 deletions
diff --git a/gui/packages/desktop/package.json b/gui/packages/desktop/package.json
index 98b6ab3c85..70ef5cc11c 100644
--- a/gui/packages/desktop/package.json
+++ b/gui/packages/desktop/package.json
@@ -13,6 +13,7 @@
"license": "GPL-3.0",
"dependencies": {
"@mullvad/components": "0.1.0",
+ "JSONStream": "^1.3.4",
"babel-runtime": "^6.26.0",
"connected-react-router": "^4.3.0",
"d3-geo-projection": "^2.3.2",
@@ -33,8 +34,7 @@
"validated": "^1.3.0"
},
"optionalDependencies": {
- "nseventmonitor": "https://github.com/mullvad/NSEventMonitor.git#0.0.9",
- "windows-security": "https://github.com/mullvad/windows-security.git#0.0.5"
+ "nseventmonitor": "https://github.com/mullvad/NSEventMonitor.git#0.0.9"
},
"devDependencies": {
"babel-cli": "^6.26.0",
diff --git a/gui/packages/desktop/src/common/types.js b/gui/packages/desktop/src/common/types.js
deleted file mode 100644
index f9ede46014..0000000000
--- a/gui/packages/desktop/src/common/types.js
+++ /dev/null
@@ -1,6 +0,0 @@
-// @flow
-
-export type RpcCredentials = {
- connectionString: string,
- sharedSecret: string,
-};
diff --git a/gui/packages/desktop/src/main/index.js b/gui/packages/desktop/src/main/index.js
index 34eb88247a..c9067a0146 100644
--- a/gui/packages/desktop/src/main/index.js
+++ b/gui/packages/desktop/src/main/index.js
@@ -10,7 +10,6 @@ import { app, screen, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'el
import TrayIconController from './tray-icon-controller';
import WindowController from './window-controller';
-import RpcAddressFile from './rpc-address-file';
import ShutdownCoordinator from './shutdown-coordinator';
import { resolveBin } from './proc';
@@ -191,45 +190,6 @@ const ApplicationMain = {
},
_registerIpcListeners() {
- ipcMain.on('discover-daemon-connection', async (event) => {
- const addressFile = new RpcAddressFile();
-
- log.debug(`Waiting for RPC address file: "${addressFile.filePath}"`);
-
- try {
- await addressFile.waitUntilExists();
- } catch (error) {
- log.error(`Cannot finish polling the RPC address file: ${error.message}`);
- return;
- }
-
- try {
- if (!addressFile.isTrusted()) {
- log.error(`Cannot verify the credibility of RPC address file`);
- return;
- }
- } catch (error) {
- log.error(`An error occurred during the credibility check: ${error.message}`);
- return;
- }
-
- // There is a race condition here where the owner and permissions of
- // the file can change in the time between we validate the owner and
- // permissions and read the contents of the file. We deem the chance
- // of that to be small enough to ignore.
-
- try {
- const credentials = await addressFile.parse();
-
- log.debug('Read RPC connection info', credentials.connectionString);
-
- event.sender.send('daemon-connection-ready', credentials);
- } catch (error) {
- log.error(`Cannot parse the RPC address file: ${error.message}`);
- return;
- }
- });
-
ipcMain.on('show-window', () => {
const windowController = this._windowController;
if (windowController) {
diff --git a/gui/packages/desktop/src/main/rpc-address-file.js b/gui/packages/desktop/src/main/rpc-address-file.js
deleted file mode 100644
index 9c46165058..0000000000
--- a/gui/packages/desktop/src/main/rpc-address-file.js
+++ /dev/null
@@ -1,112 +0,0 @@
-// @flow
-
-import fs from 'fs';
-import path from 'path';
-import { app } from 'electron';
-import { promisify } from 'util';
-import { getSystemTemporaryDirectory } from './tempdir';
-
-import type { RpcCredentials } from '../common/types';
-
-const fsReadFileAsync = promisify(fs.readFile);
-
-const POLL_INTERVAL = 200;
-
-export default class RpcAddressFile {
- _filePath = getRpcAddressFilePath();
- _pollIntervalId: ?IntervalID;
- _pollPromise: ?Promise<void>;
-
- get filePath(): string {
- return this._filePath;
- }
-
- waitUntilExists(): Promise<void> {
- let promise = this._pollPromise;
-
- if (!promise) {
- promise = new Promise((resolve) => {
- const timer = setInterval(() => {
- fs.exists(this._filePath, (exists) => {
- if (exists) {
- clearInterval(timer);
- resolve();
-
- this._pollPromise = null;
- }
- });
- }, POLL_INTERVAL);
- });
-
- this._pollPromise = promise;
- }
-
- return promise;
- }
-
- async parse(): Promise<RpcCredentials> {
- const data = await fsReadFileAsync(this._filePath, 'utf8');
- const [connectionString, sharedSecret] = data.split('\n', 2);
-
- if (connectionString && sharedSecret !== undefined) {
- return {
- connectionString,
- sharedSecret,
- };
- } else {
- throw new Error('Cannot parse the RPC address file');
- }
- }
-
- isTrusted() {
- const filePath = this._filePath;
- switch (process.platform) {
- case 'win32':
- return isOwnedByLocalSystem(filePath);
- case 'darwin':
- case 'linux':
- return isOwnedAndOnlyWritableByRoot(filePath);
- default:
- throw new Error(`Unknown platform: ${process.platform}`);
- }
- }
-}
-
-function getRpcAddressFilePath() {
- const rpcAddressFileName = '.mullvad_rpc_address';
-
- switch (process.platform) {
- case 'win32': {
- // Windows: %ALLUSERSPROFILE%\{appname}
- const programDataDirectory = process.env.ALLUSERSPROFILE;
- if (programDataDirectory) {
- const appDataDirectory = path.join(programDataDirectory, app.getName());
- return path.join(appDataDirectory, rpcAddressFileName);
- } else {
- throw new Error('Missing %ALLUSERSPROFILE% environment variable');
- }
- }
- default:
- return path.join(getSystemTemporaryDirectory(), rpcAddressFileName);
- }
-}
-
-function isOwnedAndOnlyWritableByRoot(path: string): boolean {
- const stat = fs.statSync(path);
- const isOwnedByRoot = stat.uid === 0;
- const isOnlyWritableByOwner = (stat.mode & parseInt('022', 8)) === 0;
-
- return isOwnedByRoot && isOnlyWritableByOwner;
-}
-
-function isOwnedByLocalSystem(path: string): boolean {
- // $FlowFixMe: this module is only available on Windows
- const winsec = require('windows-security');
- const ownerSid = winsec.getFileOwnerSid(path, null);
- const isWellKnownSid = winsec.isWellKnownSid(
- ownerSid,
- winsec.WellKnownSid.BuiltinAdministratorsSid,
- );
-
- return isWellKnownSid;
-}
diff --git a/gui/packages/desktop/src/main/tempdir.js b/gui/packages/desktop/src/main/tempdir.js
deleted file mode 100644
index 938448949a..0000000000
--- a/gui/packages/desktop/src/main/tempdir.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// @flow
-import path from 'path';
-
-export function getSystemTemporaryDirectory() {
- switch (process.platform) {
- case 'win32': {
- const windowsPath = process.env.windir;
- if (windowsPath) {
- return path.join(windowsPath, 'Temp');
- } else {
- throw new Error('Missing windir in environment variables.');
- }
- }
- case 'darwin':
- case 'linux':
- return '/tmp';
- default:
- throw new Error(`Not implemented for ${process.platform}`);
- }
-}
diff --git a/gui/packages/desktop/src/renderer/app.js b/gui/packages/desktop/src/renderer/app.js
index ad886f963c..e0374f05d5 100644
--- a/gui/packages/desktop/src/renderer/app.js
+++ b/gui/packages/desktop/src/renderer/app.js
@@ -26,7 +26,6 @@ import settingsActions from './redux/settings/actions';
import versionActions from './redux/version/actions';
import daemonActions from './redux/daemon/actions';
-import type { RpcCredentials } from '../common/types';
import type {
DaemonRpcProtocol,
AccountData,
@@ -41,7 +40,6 @@ export default class AppRenderer {
_notificationController = new NotificationController();
_daemonRpc: DaemonRpcProtocol = new DaemonRpc();
_reconnectBackoff = new ReconnectionBackoff();
- _credentials: ?RpcCredentials;
_openConnectionObserver: ?DaemonConnectionObserver;
_closeConnectionObserver: ?DaemonConnectionObserver;
_memoryHistory = createMemoryHistory();
@@ -418,16 +416,7 @@ export default class AppRenderer {
}
async _connectToDaemon(): Promise<void> {
- let credentials;
- try {
- credentials = await this._requestCredentials();
- } catch (error) {
- log.error(`Cannot request the RPC credentials: ${error.message}`);
- return;
- }
-
- this._credentials = credentials;
- this._daemonRpc.connect(credentials.connectionString);
+ this._daemonRpc.connect({ path: getIpcPath() });
}
async _onOpenConnection() {
@@ -437,17 +426,6 @@ export default class AppRenderer {
// reset the reconnect backoff when connection established.
this._reconnectBackoff.reset();
- // authenticate once connected
- const credentials = this._credentials;
- try {
- if (!credentials) {
- throw new Error('Credentials cannot be unset after connection is established.');
- }
- await this._authenticate(credentials.sharedSecret);
- } catch (error) {
- log.error(`Cannot authenticate: ${error.message}`);
- }
-
// attempt to restore the session
try {
await this._restoreSession();
@@ -508,15 +486,6 @@ export default class AppRenderer {
}
}
- _requestCredentials(): Promise<RpcCredentials> {
- return new Promise((resolve) => {
- ipcRenderer.once('daemon-connection-ready', (_event, credentials: RpcCredentials) => {
- resolve(credentials);
- });
- ipcRenderer.send('discover-daemon-connection');
- });
- }
-
async _subscribeStateListener() {
await this._daemonRpc.subscribeStateListener((newState, error) => {
if (error) {
@@ -655,3 +624,11 @@ class AccountDataState {
}
}
}
+
+const getIpcPath = (): string => {
+ if (process.platform === 'win32') {
+ return '//./pipe/Mullvad VPN';
+ } else {
+ return '/var/run/mullvad-vpn';
+ }
+};
diff --git a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js
index 3f3d3af9c1..c403e3fa92 100644
--- a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js
+++ b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js
@@ -1,9 +1,10 @@
// @flow
-import JsonRpcTransport, {
+import JsonRpcClient, {
RemoteError as JsonRpcRemoteError,
TimeOutError as JsonRpcTimeOutError,
-} from './jsonrpc-transport';
+ SocketTransport,
+} from './jsonrpc-client';
import { CommunicationError, InvalidAccountError, NoDaemonError } from '../errors';
import {
@@ -222,7 +223,7 @@ const AppVersionInfoSchema = object({
});
export interface DaemonRpcProtocol {
- connect(string): void;
+ connect({ path: string }): void;
disconnect(): void;
getAccountData(AccountToken): Promise<AccountData>;
getRelayLocations(): Promise<RelayList>;
@@ -268,14 +269,14 @@ export type ConnectionObserver = {
};
export class DaemonRpc implements DaemonRpcProtocol {
- _transport = new JsonRpcTransport();
+ _transport = new JsonRpcClient(new SocketTransport());
async authenticate(sharedSecret: string): Promise<void> {
await this._transport.send('auth', sharedSecret);
}
- connect(connectionString: string) {
- this._transport.connect(connectionString);
+ connect(connectionParams: { path: string }) {
+ this._transport.connect(connectionParams);
}
disconnect() {
diff --git a/gui/packages/desktop/src/renderer/lib/jsonrpc-transport.js b/gui/packages/desktop/src/renderer/lib/jsonrpc-client.js
index e5c81e6b0f..cf779c83be 100644
--- a/gui/packages/desktop/src/renderer/lib/jsonrpc-transport.js
+++ b/gui/packages/desktop/src/renderer/lib/jsonrpc-client.js
@@ -4,6 +4,8 @@ import { EventEmitter } from 'events';
import log from 'electron-log';
import jsonrpc from 'jsonrpc-lite';
import uuid from 'uuid';
+import net from 'net';
+import JSONStream from 'JSONStream';
export type UnansweredRequest = {
resolve: (mixed) => void,
@@ -90,11 +92,11 @@ export class SubscriptionError extends Error {
}
}
-export class ConnectionError extends Error {
+export class WebSocketError extends Error {
_code: number;
constructor(code: number) {
- super(ConnectionError.reason(code));
+ super(WebSocketError.reason(code));
this._code = code;
}
@@ -118,34 +120,39 @@ export class ConnectionError extends Error {
}
}
+export class TransportError extends Error {
+ constructor(reason: string) {
+ super(reason);
+ }
+}
+
const DEFAULT_TIMEOUT_MILLIS = 5000;
-export default class JsonRpcTransport extends EventEmitter {
+export default class JsonRpcClient<T> extends EventEmitter {
_unansweredRequests: Map<string, UnansweredRequest> = new Map();
_subscriptions: Map<string | number, (mixed) => void> = new Map();
- _webSocket: ?WebSocket;
- _websocketFactory: (string) => WebSocket;
+ _transport: Transport<T>;
- constructor(websocketFactory: ?(string) => WebSocket) {
+ constructor(transport: Transport<T>) {
super();
- this._websocketFactory =
- websocketFactory || ((connectionString) => new WebSocket(connectionString));
+
+ this._transport = transport;
}
/// Connect websocket
- connect(connectionString: string): Promise<void> {
+ connect(connectionParams: T): Promise<void> {
return new Promise((resolve, reject) => {
this.disconnect();
- log.info('Connecting to websocket', connectionString);
-
- const webSocket = this._websocketFactory(connectionString);
+ log.info('Connecting to transport with params', connectionParams);
// A flag used to determine if Promise was resolved.
let isPromiseResolved = false;
- webSocket.onopen = () => {
- log.info('Websocket is connected');
+ const transport = this._transport;
+
+ transport.onOpen = () => {
+ log.info('Transport is connected');
this.emit('open');
// Resolve the Promise
@@ -153,40 +160,30 @@ export default class JsonRpcTransport extends EventEmitter {
isPromiseResolved = true;
};
- webSocket.onmessage = (event) => {
- const data = event.data;
- if (typeof data === 'string') {
- this._onMessage(data);
- } else {
- log.error('Got invalid reply from the server', event);
- }
+ transport.onMessage = (obj) => {
+ this._onMessage(obj);
};
- webSocket.onclose = (event) => {
- log.info(`The websocket connection closed with code: ${event.code}`);
-
+ transport.onClose = (error: ?Error) => {
// Remove all subscriptions since they are connection based
this._subscriptions.clear();
- // 1000 is a code used for normal connection closure.
- const connectionError = event.code === 1000 ? null : new ConnectionError(event.code);
-
- this.emit('close', connectionError);
+ this.emit('close', error);
// Prevent rejecting a previously resolved Promise.
if (!isPromiseResolved) {
- reject(connectionError);
+ reject(error);
}
};
+ transport.connect(connectionParams);
- this._webSocket = webSocket;
+ this._transport = transport;
});
}
disconnect() {
- if (this._webSocket) {
- this._webSocket.close();
- this._webSocket = null;
+ if (this._transport) {
+ this._transport.close();
}
}
@@ -211,8 +208,8 @@ export default class JsonRpcTransport extends EventEmitter {
send(action: string, data: mixed, timeout: number = DEFAULT_TIMEOUT_MILLIS): Promise<mixed> {
return new Promise((resolve, reject) => {
- const webSocket = this._webSocket;
- if (!webSocket) {
+ const transport = this._transport;
+ if (!transport) {
reject(new Error('Websocket is not connected.'));
return;
}
@@ -230,7 +227,7 @@ export default class JsonRpcTransport extends EventEmitter {
try {
log.silly('Sending message', id, action);
- webSocket.send(JSON.stringify(message));
+ transport.send(JSON.stringify(message));
} catch (error) {
log.error(`Failed sending RPC message "${action}": ${error.message}`);
@@ -272,9 +269,14 @@ export default class JsonRpcTransport extends EventEmitter {
}
}
- _onMessage(message: string) {
- const result = jsonrpc.parse(message);
- const messages = Array.isArray(result) ? result : [result];
+ _onMessage(obj: Object) {
+ let messages = [];
+ try {
+ const message = jsonrpc.parseObject(obj);
+ messages = Array.isArray(message) ? message : [message];
+ } catch (error) {
+ log.error(`Failed to parse JSON-RPC message: ${error} for object`);
+ }
for (const message of messages) {
if (message.type === 'notification') {
@@ -319,3 +321,128 @@ export default class JsonRpcTransport extends EventEmitter {
}
}
}
+
+interface Transport<T> {
+ close(): void;
+ onOpen: (event: Event) => void;
+ onMessage: (Object) => void;
+ onClose: (error: ?Error) => void;
+ send(message: string): void;
+ connect(params: T): void;
+}
+
+export class WebsocketTransport implements Transport<string> {
+ ws: ?WebSocket;
+ onOpen: (event: Event) => void;
+ onMessage: (Object) => void;
+ onClose: (error: ?Error) => void;
+
+ constructor(ws: ?WebSocket) {
+ this.ws = ws;
+ this.onOpen = () => {};
+ this.onMessage = () => {};
+ this.onClose = () => {};
+ }
+
+ close() {
+ if (this.ws) this.ws.close();
+ }
+
+ send(msg: string) {
+ if (this.ws) {
+ this.ws.send(msg);
+ }
+ }
+
+ connect(params: string): void {
+ if (this.ws) {
+ this.ws.close();
+ }
+ this.ws = new WebSocket(params);
+ this.ws.onopen = this.onOpen;
+ this.ws.onmessage = (event) => {
+ try {
+ const data = event.data;
+ if (typeof data === 'string') {
+ const msg = JSON.parse(data);
+ this.onMessage(msg);
+ } else {
+ throw event;
+ }
+ } catch (error) {
+ log.error('Got invalid reply from server: ', error);
+ }
+ };
+
+ this.ws.onclose = (event) => {
+ log.info(`The websocket connection closed with code: ${event.code}`);
+ const error = event.code === 1000 ? null : new WebSocketError(event.code);
+ this.onClose(error);
+ };
+ }
+}
+
+// Given the correct parameters, this transport supports named pipes/unix
+// domain sockets, and also TCP/UDP sockets
+export class SocketTransport implements Transport<{ path: string }> {
+ connection: ?net.Socket;
+ onMessage: (message: Object) => void;
+ onClose: (error: ?Error) => void;
+ onOpen: (event: Event) => void;
+
+ constructor() {
+ this.connection = null;
+ this.onMessage = () => {};
+ this.onClose = () => {};
+ this.onOpen = () => {};
+ }
+
+ _connect(options: { path: string }) {
+ const connection = new net.Socket();
+ connection.on('error', (err) => {
+ this.onClose(err);
+ this.close();
+ });
+
+ connection.on('connect', (event) => {
+ this.connection = connection;
+ this.onOpen(event);
+ });
+
+ const jsonStream = JSONStream.parse();
+
+ connection.pipe(jsonStream);
+
+ jsonStream.on('data', this.onMessage);
+
+ jsonStream.on('error', (err) => {
+ this.onClose(err);
+ this.close();
+ });
+
+ connection.connect(options);
+ }
+
+ close() {
+ try {
+ if (this.connection) {
+ this.connection.end();
+ }
+ } catch (error) {
+ log.error('failed to close the connection: ', error);
+ }
+ this.connection = null;
+ }
+
+ send(msg: string) {
+ if (this.connection) {
+ this.connection.write(msg);
+ } else {
+ throw new TransportError('Socket not connected');
+ }
+ }
+
+ connect(options: { path: string }): void {
+ this._connect(options);
+ }
+}
diff --git a/gui/packages/desktop/test/jsonrpc-transport.spec.js b/gui/packages/desktop/test/jsonrpc-transport.spec.js
index 5e600f2204..ea07e060b8 100644
--- a/gui/packages/desktop/test/jsonrpc-transport.spec.js
+++ b/gui/packages/desktop/test/jsonrpc-transport.spec.js
@@ -1,15 +1,18 @@
// @flow
import jsonrpc from 'jsonrpc-lite';
-import { Server, WebSocket as MockWebSocket } from 'mock-socket';
-import JsonRpcTransport, { TimeOutError } from '../src/renderer/lib/jsonrpc-transport';
+import { Server } from 'mock-socket';
+import JsonRpcClient, {
+ WebsocketTransport,
+ TimeOutError,
+} from '../src/renderer/lib/jsonrpc-client';
describe('JSON RPC transport', () => {
const WEBSOCKET_URL = 'ws://localhost:8080';
- let server: Server, transport: JsonRpcTransport;
+ let server: Server, transport: JsonRpcClient<string>;
beforeEach(() => {
server = new Server(WEBSOCKET_URL);
- transport = new JsonRpcTransport((url) => new MockWebSocket(url));
+ transport = new JsonRpcClient(new WebsocketTransport());
});
afterEach(() => {
diff --git a/gui/yarn.lock b/gui/yarn.lock
index cded790c63..102a3b710e 100644
--- a/gui/yarn.lock
+++ b/gui/yarn.lock
@@ -684,6 +684,13 @@
version "2.3.3"
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.3.tgz#7f226d67d654ec9070e755f46daebf014628e9d9"
+JSONStream@^1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.4.tgz#615bb2adb0cd34c8f4c447b5f6512fa1d8f16a2e"
+ dependencies:
+ jsonparse "^1.2.0"
+ through ">=2.2.7 <3"
+
abab@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f"
@@ -4625,6 +4632,10 @@ jsonify@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+jsonparse@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
+
jsonrpc-lite@^1.2.3:
version "1.3.1"
resolved "https://registry.yarnpkg.com/jsonrpc-lite/-/jsonrpc-lite-1.3.1.tgz#5c33086071793a0806e6c96e7c1ae92f4460ac50"
@@ -7251,7 +7262,7 @@ through2@~0.2.3:
readable-stream "~1.1.9"
xtend "~2.1.1"
-through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1:
+through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -7728,12 +7739,6 @@ window-size@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
-"windows-security@https://github.com/mullvad/windows-security.git#0.0.5":
- version "0.0.5"
- resolved "https://github.com/mullvad/windows-security.git#311fcfeb137e9e10b25929b0615f9188e118004f"
- dependencies:
- node-pre-gyp "^0.10.0"
-
wordwrap@^1.0.0, wordwrap@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"