mirror of
https://github.com/OutlineFoundation/outline-server.git
synced 2026-05-13 05:52:04 +00:00
Refactor
This commit is contained in:
parent
41ae0f6dbb
commit
263974e1ae
19 changed files with 177 additions and 158 deletions
4
.npmrc
4
.npmrc
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
// 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.
|
||||
|
||||
// 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.
|
||||
|
||||
// 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.
|
||||
|
||||
export interface HttpRequest {
|
||||
url: string;
|
||||
method: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
export interface HttpResponse {
|
||||
status: number;
|
||||
body?: string;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
67
src/server_manager/infrastructure/path_api.spec.ts
Normal file
67
src/server_manager/infrastructure/path_api.spec.ts
Normal file
|
|
@ -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"}',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -12,39 +12,31 @@
|
|||
// 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.
|
||||
|
||||
import * as errors from './errors';
|
||||
|
||||
export interface HttpRequest {
|
||||
url: string;
|
||||
method: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
export interface HttpResponse {
|
||||
status: number;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
// A Fetcher provides the HTTP client functionality for PathApi.
|
||||
export type Fetcher = typeof fetchWrapper;
|
||||
|
||||
/**
|
||||
* @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));
|
||||
}
|
||||
export type Fetcher = (request: HttpRequest) => Promise<HttpResponse>;
|
||||
|
||||
/**
|
||||
* Provides access to an HTTP API of the kind exposed by the Shadowbox server.
|
||||
4
src/server_manager/types/preload.d.ts
vendored
4
src/server_manager/types/preload.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
28
src/server_manager/web_app/fetcher.spec.ts
Normal file
28
src/server_manager/web_app/fetcher.spec.ts
Normal file
|
|
@ -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
src/server_manager/web_app/fetcher.ts
Normal file
43
src/server_manager/web_app/fetcher.ts
Normal file
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
// 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, PathApiClient} from './path_api';
|
||||
|
||||
describe('PathApi', () => {
|
||||
describe('local', () => {
|
||||
// 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"}',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// These tests rely on the fetch() function, which is not available in Node (yet).
|
||||
if (!process?.versions?.node) {
|
||||
describe('remote', () => {
|
||||
const api = makePathApiClient('https://api.github.com/repos/Jigsaw-Code/');
|
||||
|
||||
it('GET', async () => {
|
||||
const response = await api.request<{name: string}>('outline-server');
|
||||
expect(response.name).toEqual('outline-server');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
import * as semver from 'semver';
|
||||
|
||||
import * as server from '../model/server';
|
||||
import {PathApiClient} from './path_api';
|
||||
import {PathApiClient} from '../infrastructure/path_api';
|
||||
|
||||
interface AccessKeyJson {
|
||||
id: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue