Skip to content

Commit

Permalink
Refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben Schwartz committed May 13, 2022
1 parent 41ae0f6 commit 263974e
Show file tree
Hide file tree
Showing 19 changed files with 175 additions and 156 deletions.
4 changes: 4 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
; Enforces that the user is `npm install`ing with the correct node version.
engine-strict=true

; Workaround for conflict between the default location(s) of node-forge and the
; location expected by Typescript, Jasmine, and Electron.
prefer-dedupe=true
2 changes: 1 addition & 1 deletion src/server_manager/electron_app/build.action.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ tsc -p src/server_manager/electron_app/tsconfig.json --outDir build/server_manag
readonly STATIC_DIR="${OUT_DIR}/static"
mkdir -p "${STATIC_DIR}"
mkdir -p "${STATIC_DIR}/server_manager"
cp -r "${OUT_DIR}/js/"* "${STATIC_DIR}"
cp -r "${OUT_DIR}/js/electron_app/"* "${STATIC_DIR}"
cp -r "${BUILD_DIR}/server_manager/web_app/static" "${STATIC_DIR}/server_manager/web_app/"

# Electron requires a package.json file for the app's name, etc.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {urlToHttpOptions} from 'url';

import type {IncomingMessage} from 'http';

import type {HttpRequest, HttpResponse} from './types';
import type {HttpRequest, HttpResponse} from '../infrastructure/path_api';

export const fetchWithPin = async (
req: HttpRequest,
Expand Down
34 changes: 0 additions & 34 deletions src/server_manager/electron_app/http/types.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/server_manager/electron_app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {autoUpdater} from 'electron-updater';
import * as path from 'path';
import {URL, URLSearchParams} from 'url';

import type {HttpRequest, HttpResponse} from './http/types';
import {fetchWithPin} from './http/fetch';
import type {HttpRequest, HttpResponse} from '../infrastructure/path_api';
import {fetchWithPin} from './fetch';
import * as menu from './menu';

const app = electron.app;
Expand Down
2 changes: 1 addition & 1 deletion src/server_manager/electron_app/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {URL} from 'url';

import * as digitalocean_oauth from './digitalocean_oauth';
import * as gcp_oauth from './gcp_oauth';
import {HttpRequest, HttpResponse} from './http/types';
import {HttpRequest, HttpResponse} from '../infrastructure/path_api';
import {redactManagerUrl} from './util';

// This file is run in the renderer process *before* nodeIntegration is disabled.
Expand Down
4 changes: 2 additions & 2 deletions src/server_manager/electron_app/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
"removeComments": false,
"noImplicitAny": true,
"module": "commonjs",
"rootDir": ".",
"rootDir": "..",
"lib": ["dom", "es2021"]
},
"include": ["*.ts", "../types/*.d.ts"],
"include": ["*.ts", "../infrastructure/path_api.ts", "../types/*.d.ts"],
"exclude": ["node_modules"],
"compileOnSave": true
}
2 changes: 1 addition & 1 deletion src/server_manager/infrastructure/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import type {HttpResponse} from '../electron_app/http/types';
import type {HttpResponse} from './path_api';

export class OutlineError extends Error {
constructor(message?: string) {
Expand Down
67 changes: 67 additions & 0 deletions src/server_manager/infrastructure/path_api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2022 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 {PathApiClient} from './path_api';

describe('PathApi', () => {
// Mock fetcher
let lastRequest: HttpRequest;
let nextResponse: Promise<HttpResponse>;

const fetcher = (request: HttpRequest) => {
lastRequest = request;
return nextResponse;
};

beforeEach(() => {
lastRequest = undefined;
nextResponse = undefined;
});

const api = new PathApiClient('https://asdf.test/foo', fetcher);

it('GET', async () => {
const response = {status: 200, body: '{"asdf": true}'};
nextResponse = Promise.resolve(response);
expect(await api.request('bar')).toEqual({asdf: true});
expect(lastRequest).toEqual({
url: 'https://asdf.test/foo/bar',
method: 'GET',
});
});

it('PUT form data', async () => {
const response = {status: 200, body: '{"asdf": true}'};
nextResponse = Promise.resolve(response);
expect(await api.requestForm('bar', 'PUT', {name: 'value'})).toEqual({asdf: true});
expect(lastRequest).toEqual({
url: 'https://asdf.test/foo/bar',
method: 'PUT',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'name=value',
});
});

it('POST JSON data', async () => {
const response = {status: 200, body: '{"asdf": true}'};
nextResponse = Promise.resolve(response);
expect(await api.requestJson('bar', 'POST', {key: 'value'})).toEqual({asdf: true});
expect(lastRequest).toEqual({
url: 'https://asdf.test/foo/bar',
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: '{"key":"value"}',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,32 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import * as errors from '../infrastructure/errors';
import {HttpRequest, HttpResponse} from '../electron_app/http/types';
// This file is imported by both the Electron and Renderer process code,
// so it cannot contain any imports that are not available in both
// environments.

async function fetchWrapper(request: HttpRequest): Promise<HttpResponse> {
const response = await fetch(request.url, request);
return {
status: response.status,
body: await response.text(),
};
}
// These type definitions are designed to bridge the differences between
// the Fetch API and the Node.JS HTTP API, while also being compatible
// with the Structured Clone algorithm so that they can be passed between
// the Electron and Renderer processes.

// A Fetcher provides the HTTP client functionality for PathApi.
export type Fetcher = typeof fetchWrapper;
import * as errors from './errors';

/**
* @param fingerprint A SHA-256 hash of the expected leaf certificate, in binary encoding.
* @returns An HTTP client that enforces `fingerprint`, if set.
*/
function makeFetcher(fingerprint?: string): Fetcher {
if (fingerprint) {
return (request) => fetchWithPin(request, fingerprint);
}
return fetchWrapper;
export interface HttpRequest {
url: string;
method: string;
headers?: Record<string, string>;
body?: string;
}

/**
* @param base A valid URL
* @param fingerprint A SHA-256 hash of the expected leaf certificate, in binary encoding.
* @returns A fully initialized API client.
*/
export function makePathApiClient(base: string, fingerprint?: string): PathApiClient {
return new PathApiClient(base, makeFetcher(fingerprint));
export interface HttpResponse {
status: number;
body?: string;
}

// A Fetcher provides the HTTP client functionality for PathApi.
export type Fetcher = (request: HttpRequest) => Promise<HttpResponse>;

/**
* Provides access to an HTTP API of the kind exposed by the Shadowbox server.
*
Expand Down
4 changes: 2 additions & 2 deletions src/server_manager/types/preload.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

// Functions made available to the renderer process via preload.ts.

type HttpRequest = import('../electron_app/http/types').HttpRequest;
type HttpResponse = import('../electron_app/http/types').HttpResponse;
type HttpRequest = import('../infrastructure/path_api').HttpRequest;
type HttpResponse = import('../infrastructure/path_api').HttpResponse;

declare function fetchWithPin(request: HttpRequest, fingerprint: string): Promise<HttpResponse>;
declare function openImage(basename: string): void;
Expand Down
2 changes: 1 addition & 1 deletion src/server_manager/web_app/digitalocean_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {sleep} from '../infrastructure/sleep';
import {ValueStream} from '../infrastructure/value_stream';
import {Region} from '../model/digitalocean';
import * as server from '../model/server';
import {makePathApiClient} from './path_api';
import {makePathApiClient} from './fetcher';

import {ShadowboxServer} from './shadowbox_server';

Expand Down
28 changes: 28 additions & 0 deletions src/server_manager/web_app/fetcher.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2022 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 {makePathApiClient} from './fetcher';

describe('makePathApiClient', () => {
const api = makePathApiClient('https://api.github.com/repos/Jigsaw-Code/');

if (process?.versions?.node) {
// This test relies on fetch(), which doesn't exist in Node (yet).
return;
}
it('GET', async () => {
const response = await api.request<{name: string}>('outline-server');
expect(response.name).toEqual('outline-server');
});
});
43 changes: 43 additions & 0 deletions src/server_manager/web_app/fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2022 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 {Fetcher, PathApiClient} from '../infrastructure/path_api';

async function fetchWrapper(request: HttpRequest): Promise<HttpResponse> {
const response = await fetch(request.url, request);
return {
status: response.status,
body: await response.text(),
};
}

/**
* @param fingerprint A SHA-256 hash of the expected leaf certificate, in binary encoding.
* @returns An HTTP client that enforces `fingerprint`, if set.
*/
function makeFetcher(fingerprint?: string): Fetcher {
if (fingerprint) {
return (request) => fetchWithPin(request, fingerprint);
}
return fetchWrapper;
}

/**
* @param base A valid URL
* @param fingerprint A SHA-256 hash of the expected leaf certificate, in binary encoding.
* @returns A fully initialized API client.
*/
export function makePathApiClient(base: string, fingerprint?: string): PathApiClient {
return new PathApiClient(base, makeFetcher(fingerprint));
}
2 changes: 1 addition & 1 deletion src/server_manager/web_app/gcp_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {ValueStream} from '../infrastructure/value_stream';
import {Zone} from '../model/gcp';
import * as server from '../model/server';
import {DataAmount, ManagedServerHost, MonetaryCost} from '../model/server';
import {makePathApiClient} from './path_api';
import {makePathApiClient} from './fetcher';

import {ShadowboxServer} from './shadowbox_server';

Expand Down
2 changes: 1 addition & 1 deletion src/server_manager/web_app/manual_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import {hexToString} from '../infrastructure/hex_encoding';
import * as server from '../model/server';
import {makePathApiClient} from './path_api';
import {makePathApiClient} from './fetcher';

import {ShadowboxServer} from './shadowbox_server';

Expand Down
Loading

0 comments on commit 263974e

Please sign in to comment.