Skip to content
This repository has been archived by the owner on Jun 20, 2022. It is now read-only.

Commit

Permalink
feat: support system proxies via Electron's net module
Browse files Browse the repository at this point in the history
  • Loading branch information
arielsvg committed May 1, 2020
1 parent d7d7a30 commit 91eb637
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 80 deletions.
70 changes: 35 additions & 35 deletions app/javascripts/main/networking.ts
@@ -1,12 +1,20 @@
import { IncomingMessage, net } from 'electron';
import fs from 'fs';
import { IncomingMessage } from 'http';
import https from 'https';
import path from 'path';
import { pipeline as pipelineFn } from 'stream';
import { promisify } from 'util';
import { MessageType } from '../../../test/TestIpcMessage';
import { ensureDirectoryExists } from './fileUtils';
import { handle } from './testing';
import { isTesting } from './utils';

const pipeline = promisify(pipelineFn);

if (isTesting()) {
handle(MessageType.GetJSON, getJSON);
handle(MessageType.DownloadFile, downloadFile);
}

/**
* Downloads a file to the specified destination.
* @param filePath path to the saved file (will be created if it does
Expand All @@ -15,56 +23,48 @@ const pipeline = promisify(pipelineFn);
export async function downloadFile(url: string, filePath: string) {
await ensureDirectoryExists(path.dirname(filePath));
const response = await get(url);
await pipeline(response, fs.createWriteStream(filePath));
await pipeline(
/**
* IncomingMessage doesn't implement *every* property of ReadableStream
* but still all the ones that pipeline needs
* @see https://www.electronjs.org/docs/api/incoming-message
*/
response as any,
fs.createWriteStream(filePath)
);
}

export async function getJSON<T>(url: string): Promise<T> {
const response = await get(url);
response.setEncoding('utf-8');
let data = '';
return new Promise((resolve, reject) => {
response
.on('data', (chunk) => {
data += chunk;
})
.on('error', reject)
.on('close', () => {
.on('end', () => {
resolve(JSON.parse(data));
});
});
}

/**
* Performs an HTTPS GET request, following redirects.
* DOES NOT handle compressed responses.
* @param {string} url the url of the file to get
*/
export async function get(
url: string,
maxRedirects = 3
): Promise<IncomingMessage> {
let redirects = 0;
let response = await promiseGet(url);
while (
response.statusCode &&
response.statusCode >= 300 &&
response.statusCode < 400 &&
response.headers.location &&
redirects < maxRedirects
) {
redirects += 1;
response = await promiseGet(response.headers.location);
export function get(url: string) {
const enum Method {
Get = 'GET',
}
const enum RedirectMode {
Follow = 'follow',
}
return response;
}

/**
* The https module's get function, promisified.
* @param {string} url
* @returns The response stream.
*/
function promiseGet(url: string): Promise<IncomingMessage> {
return new Promise((resolve, reject) => {
https.get(url, resolve).on('error', reject);
return new Promise<IncomingMessage>((resolve, reject) => {
const request = net.request({
url,
method: Method.Get,
redirect: RedirectMode.Follow,
});
request.on('response', resolve);
request.on('error', reject);
request.end();
});
}
10 changes: 6 additions & 4 deletions app/javascripts/main/updateManager.ts
Expand Up @@ -194,10 +194,12 @@ export function createUpdateManager(window: BrowserWindow): UpdateManager {
}
} catch (error) {
logError(error);
dialog.showMessageBox({
title: str().finishedChecking.title,
message: str().finishedChecking.error(JSON.stringify(error)),
});
if (userTriggered) {
dialog.showMessageBox({
title: str().finishedChecking.title,
message: str().finishedChecking.error(JSON.stringify(error)),
});
}
} finally {
checkingForUpdate = false;
triggerMenuReload();
Expand Down
2 changes: 2 additions & 0 deletions test/TestIpcMessage.ts
Expand Up @@ -33,4 +33,6 @@ export enum MessageType {
Relaunch,
DataArchive,
WindowLoaded,
GetJSON,
DownloadFile,
}
6 changes: 6 additions & 0 deletions test/driver.ts
Expand Up @@ -165,6 +165,12 @@ class Driver {
this.send(MessageType.UpdateManagerTriggeredMenuReload),
};

readonly net = {
getJSON: (url: string) => this.send(MessageType.GetJSON, url),
downloadFile: (url: string, filePath: string) =>
this.send(MessageType.DownloadFile, url, filePath),
};

stop = async () => {
this.appProcess.kill();

Expand Down
59 changes: 37 additions & 22 deletions test/extServer.spec.ts
@@ -1,11 +1,11 @@
import anyTest, { TestInterface } from 'ava';
import { promises as fs } from 'fs';
import http, { IncomingMessage } from 'http';
import http from 'http';
import { AddressInfo } from 'net';
import path from 'path';
import proxyquire from 'proxyquire';
import { ensureDirectoryExists } from '../app/javascripts/main/fileUtils';
import { initializeStrings } from '../app/javascripts/main/strings';
import { AddressInfo } from 'net';
import { createTmpDir } from './testUtils';

const test = anyTest as TestInterface<{
Expand Down Expand Up @@ -36,10 +36,6 @@ const { createExtensionsServer, normalizeFilePath } = proxyquire(
}
);

const { getJSON, get } = proxyquire('../app/javascripts/main/networking', {
https: http,
});

const extensionsDir = path.join(tmpDir.path, 'Extensions');

initializeStrings('en');
Expand Down Expand Up @@ -83,7 +79,7 @@ test.after(
}
);

test('serves the files in the Extensions directory over HTTP', async (t) => {
test('serves the files in the Extensions directory over HTTP', (t) => {
const data = {
name: 'Boxes',
meter: {
Expand All @@ -97,25 +93,44 @@ test('serves the files in the Extensions directory over HTTP', async (t) => {
{ name: 'Piano', type: 'Electric' },
],
};
await fs.writeFile(
path.join(extensionsDir, 'file.json'),
JSON.stringify(data)
);
t.deepEqual(await getJSON(t.context.host + 'Extensions/file.json'), data);

return fs
.writeFile(path.join(extensionsDir, 'file.json'), JSON.stringify(data))
.then(
() =>
new Promise((resolve) => {
let serverData = '';
http
.get(t.context.host + 'Extensions/file.json')
.on('response', (response) => {
response
.setEncoding('utf-8')
.on('data', (chunk) => {
serverData += chunk;
})
.on('end', () => {
t.deepEqual(data, JSON.parse(serverData));
resolve();
});
});
})
);
});

test('does not serve files outside the Extensions directory', async (t) => {
const response: IncomingMessage = await get(
t.context.host + 'Extensions/../../../package.json'
);
t.is(response.statusCode, 500);
test.cb('does not serve files outside the Extensions directory', (t) => {
http
.get(t.context.host + 'Extensions/../../../package.json')
.on('response', (response) => {
t.is(response.statusCode, 500);
t.end();
});
});

test('returns a 404 for files that are not present', async (t) => {
const response: IncomingMessage = await get(
t.context.host + 'Extensions/nothing'
);
t.is(response.statusCode, 404);
test.cb('returns a 404 for files that are not present', (t) => {
http.get(t.context.host + 'Extensions/nothing').on('response', (response) => {
t.is(response.statusCode, 404);
t.end();
});
});

test('normalizes file paths to always point somewhere in the Extensions directory', (t) => {
Expand Down
35 changes: 16 additions & 19 deletions test/networking.spec.ts
@@ -1,20 +1,14 @@
import test from 'ava';
import anyTest, { TestInterface } from 'ava';
import { promises as fs } from 'fs';
import http from 'http';
import { AddressInfo } from 'net';
import path from 'path';
import proxyquire from 'proxyquire';
import { createDriver, Driver } from './driver';
import { createTmpDir } from './testUtils';
const test = anyTest as TestInterface<Driver>;

const tmpDir = createTmpDir(__filename);

const { getJSON, downloadFile } = proxyquire(
'../app/javascripts/main/networking',
{
https: http,
}
);

const sampleData = {
title: 'Diamond Dove',
meter: {
Expand All @@ -27,8 +21,8 @@ let server: http.Server;
let serverAddress: string;

test.before(
(): Promise<any> => {
return Promise.all([
(): Promise<any> =>
Promise.all([
tmpDir.make(),
new Promise((resolve) => {
server = http.createServer((_req, res) => {
Expand All @@ -41,26 +35,29 @@ test.before(
resolve();
});
}),
]);
}
])
);

test.after(
(): Promise<any> => {
return Promise.all([
(): Promise<any> =>
Promise.all([
tmpDir.clean(),
new Promise((resolve) => server.close(resolve)),
]);
}
])
);

test.beforeEach(async (t) => {
t.context = await createDriver();
});
test.afterEach((t) => t.context.stop());

test('downloads a JSON file', async (t) => {
t.deepEqual(await getJSON(serverAddress), sampleData);
t.deepEqual(await t.context.net.getJSON(serverAddress), sampleData);
});

test('downloads a folder to the specified location', async (t) => {
const filePath = path.join(tmpDir.path, 'fileName.json');
await downloadFile(serverAddress + '/file', filePath);
await t.context.net.downloadFile(serverAddress + '/file', filePath);
const fileContents = await fs.readFile(filePath, 'utf8');
t.is(JSON.stringify(sampleData), fileContents);
});

0 comments on commit 91eb637

Please sign in to comment.