Skip to content

Commit

Permalink
Use the server's new REST API, where available
Browse files Browse the repository at this point in the history
  • Loading branch information
pimterry committed Jul 10, 2023
1 parent 4defa08 commit 79a8bdd
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 205 deletions.
1 change: 0 additions & 1 deletion src/model/interception/interceptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { ServerInterceptor } from "../../services/server-api";
import {
versionSatisfies,
DETAILED_CONFIG_RANGE,
DOCKER_INTERCEPTION_RANGE,
WEBRTC_GLOBALLY_ENABLED
} from "../../services/service-versions";
import { IconProps, SourceIcons } from "../../icons";
Expand Down
24 changes: 24 additions & 0 deletions src/services/server-api-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NetworkInterfaceInfo } from 'os';
import { ProxySetting } from 'mockttp';

export interface ServerInterceptor {
id: string;
version: string;
isActivable: boolean;
isActive: boolean;
metadata?: any;
}

export interface NetworkInterfaces {
[index: string]: NetworkInterfaceInfo[];
}

export interface ServerConfig {
certificatePath: string;
certificateContent?: string;
certificateFingerprint?: string;
networkInterfaces: NetworkInterfaces;
systemProxy: ProxySetting | undefined;
dnsServers: string[];
ruleParameterKeys: string[];
}
239 changes: 36 additions & 203 deletions src/services/server-api.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import { NetworkInterfaceInfo } from 'os';
import * as _ from 'lodash';
import * as localForage from 'localforage';
import { ProxySetting } from 'mockttp';

import { RUNNING_IN_WORKER } from '../util';
import { getDeferred } from '../util/promise';
import {
serverVersion,
versionSatisfies,
DETAILED_CONFIG_RANGE,
INTERCEPTOR_METADATA,
DETAILED_METADATA,
PROXY_CONFIG_RANGE,
DNS_AND_RULE_PARAM_CONFIG_RANGE
REST_API_SUPPORTED
} from './service-versions';

import type { ServerConfig, NetworkInterfaces, ServerInterceptor } from './server-api-types';
export { ServerConfig, NetworkInterfaces, ServerInterceptor };

import { GraphQLApiClient } from './server-graphql-api';
import { RestApiClient } from './server-rest-api';

const authTokenPromise = !RUNNING_IN_WORKER
// Main UI gets given the auth token directly in its URL:
? Promise.resolve(new URLSearchParams(window.location.search).get('authToken'))
? Promise.resolve(new URLSearchParams(window.location.search).get('authToken') ?? undefined)
// For workers, the new (March 2020) UI shares the auth token with SW via IDB:
: localForage.getItem<string>('latest-auth-token')
.then((authToken) => {
Expand All @@ -27,234 +25,69 @@ const authTokenPromise = !RUNNING_IN_WORKER
const workerParams = new URLSearchParams(
(self as unknown as WorkerGlobalScope).location.search
);
return workerParams.get('authToken');
return workerParams.get('authToken') ?? undefined;

// Pre-Jan 2020 UI doesn't share auth token - ok with old desktop, fails with 0.1.18+.
});

const authHeaders = authTokenPromise.then((authToken): Record<string, string> =>
authToken
? { 'Authorization': `Bearer ${authToken}` }
: {}
);

const graphql = async <T extends {}>(operationName: string, query: string, variables: unknown) => {
const response = await fetch('http://127.0.0.1:45457', {
method: 'POST',
headers: {
...await authHeaders,
'content-type': 'application/json'
},
body: JSON.stringify({
operationName,
query,
variables
})
});

if (!response.ok) {
console.error(response);
throw new Error(
`Server XHR error during ${operationName}, status ${response.status} ${response.statusText}`
);
}

const { data, errors } = await response.json() as { data: T, errors?: GraphQLError[] };

if (errors && errors.length) {
console.error(errors);

const errorCount = errors.length > 1 ? `s (${errors.length})` : '';

throw new Error(
`Server error${errorCount} during ${operationName}: ${errors.map(e =>
`${e.message} at ${e.path.join('.')}`
).join(', ')}`
);
}

return data;
}

export interface ServerInterceptor {
id: string;
version: string;
isActivable: boolean;
isActive: boolean;
metadata?: any;
}

interface GraphQLError {
locations: Array<{ line: number, column: number }>;
message: string;
path: Array<string>
}

const serverReady = getDeferred();
export const announceServerReady = () => serverReady.resolve();
export const waitUntilServerReady = () => serverReady.promise;

// We initially default to the GQL API. If at the first version lookup we discover that the REST
// API is supported, this is swapped out for the REST client instead. Both work, but REST is the
// goal long-term so should be preferred where available.
let apiClient: Promise<GraphQLApiClient | RestApiClient> = authTokenPromise
.then((authToken) => new GraphQLApiClient(authToken));
export async function getServerVersion(): Promise<string> {
const response = await graphql<{ version: string }>('getVersion', `
query getVersion {
version
}
`, {});

return response.version;
}
const client = await apiClient;
const version = await client.getServerVersion();

export type NetworkInterfaces = { [index: string]: NetworkInterfaceInfo[] };

export async function getConfig(proxyPort: number): Promise<{
certificatePath: string;
certificateContent?: string;
certificateFingerprint?: string;
networkInterfaces: NetworkInterfaces;
systemProxy: ProxySetting | undefined;
dnsServers: string[];
ruleParameterKeys: string[];
}> {
const response = await graphql<{
config: {
certificatePath: string;
certificateContent?: string;
certificateFingerprint?: string;
}
networkInterfaces?: NetworkInterfaces;
systemProxy?: ProxySetting;
dnsServers?: string[];
ruleParameterKeys?: string[];
}>('getConfig', `
${versionSatisfies(await serverVersion, DNS_AND_RULE_PARAM_CONFIG_RANGE)
? `query getConfig($proxyPort: Int!) {`
: 'query getConfig {'
}
config {
certificatePath
${versionSatisfies(await serverVersion, DETAILED_CONFIG_RANGE)
? `
certificateContent
certificateFingerprint
` : ''}
}
${versionSatisfies(await serverVersion, DETAILED_CONFIG_RANGE)
? `networkInterfaces`
: ''}
${versionSatisfies(await serverVersion, PROXY_CONFIG_RANGE)
? `systemProxy {
proxyUrl
noProxy
}` : ''}
// Swap to the REST client if we receive a version where it's supported:
if (versionSatisfies(version, REST_API_SUPPORTED) && client instanceof GraphQLApiClient) {
apiClient = authTokenPromise
.then((authToken) => new RestApiClient(authToken));
}

${versionSatisfies(await serverVersion, DNS_AND_RULE_PARAM_CONFIG_RANGE)
? `
dnsServers(proxyPort: $proxyPort)
ruleParameterKeys
`
: ''}
}
`, { proxyPort: proxyPort });
return version;
}

return {
...response.config,
networkInterfaces: response.networkInterfaces || {},
systemProxy: response.systemProxy,
dnsServers: response.dnsServers || [],
ruleParameterKeys: response.ruleParameterKeys || []
}
export async function getConfig(proxyPort: number): Promise<ServerConfig> {
return (await apiClient).getConfig(proxyPort);
}

export async function getNetworkInterfaces(): Promise<NetworkInterfaces> {
if (!versionSatisfies(await serverVersion, DETAILED_CONFIG_RANGE)) return {};

const response = await graphql<{
networkInterfaces: NetworkInterfaces
}>('getNetworkInterfaces', `
query getNetworkInterfaces {
networkInterfaces
}
`, {});

return response.networkInterfaces;
return (await apiClient).getNetworkInterfaces();
}

export async function getInterceptors(proxyPort: number): Promise<ServerInterceptor[]> {
const response = await graphql<{
interceptors: ServerInterceptor[]
}>('getInterceptors', `
query getInterceptors($proxyPort: Int!) {
interceptors {
id
version
isActive(proxyPort: $proxyPort)
isActivable
${versionSatisfies(await serverVersion, INTERCEPTOR_METADATA)
? 'metadata'
: ''
}
}
}
`, { proxyPort });

return response.interceptors;
return (await apiClient).getInterceptors(proxyPort);
}

export async function getDetailedInterceptorMetadata<M extends unknown>(id: string): Promise<M | undefined> {
if (!versionSatisfies(await serverVersion, DETAILED_METADATA)) return undefined;

const response = await graphql<{
interceptor: { metadata: M }
}>('getDetailedInterceptorMetadata', `
query getDetailedInterceptorMetadata($id: ID!) {
interceptor(id: $id) {
metadata(type: DETAILED)
}
}
`, { id });

return response.interceptor.metadata;
return (await apiClient).getDetailedInterceptorMetadata(id);
}

export async function activateInterceptor(id: string, proxyPort: number, options?: any): Promise<unknown> {
const result = await graphql<{
activateInterceptor: boolean | { success: boolean, metadata: unknown }
}>('Activate', `
mutation Activate($id: ID!, $proxyPort: Int!, $options: Json) {
activateInterceptor(id: $id, proxyPort: $proxyPort, options: $options)
}
`, { id, proxyPort, options });
const result = await (await apiClient).activateInterceptor(id, proxyPort, options);

if (result.activateInterceptor === true) {
// Backward compat for a < v0.1.28 server that returns booleans:
return undefined;
} else if (result.activateInterceptor && result.activateInterceptor.success) {
// Modern server that return an object with a success prop:
return result.activateInterceptor.metadata;
if (result.success) {
return result.metadata;
} else {
// Some kind of falsey failure:
// Some kind of failure:
console.log('Activation result', JSON.stringify(result));

const error = Object.assign(
new Error(`Failed to activate interceptor ${id}`),
result.activateInterceptor && result.activateInterceptor.metadata
? { metadata: result.activateInterceptor.metadata }
: {}
result
);

throw error;
}
}

export async function triggerServerUpdate() {
await graphql<{}>('TriggerUpdate', `
mutation TriggerUpdate {
triggerUpdate
}
`, { })
// We ignore all errors, this trigger is just advisory
.catch(console.log);
return (await apiClient).triggerServerUpdate()
// We ignore all errors, this trigger is just advisory
.catch(console.log);
}
Loading

0 comments on commit 79a8bdd

Please sign in to comment.