@jovian/type-tools
Version:
TypeTools is a Typescript library for providing extensible tooling runtime validations and type helpers.
221 lines (206 loc) • 8.2 kB
text/typescript
/*
* Copyright 2014-2021 Jovian, all rights reserved.
*/
import * as axios from 'axios';
import { DiffieHellmanKeyPair, FourQ } from '@jovian/fourq';
import { v4 as uuidv4 } from 'uuid';
import { initiateSecureChannel, resolveEntry, SecureChannel, SecureHandshake } from './secure-channel';
import { ok, errorResult, promise, Result } from '../../src/common/globals.ix';
import { HTTP } from '../http/http.shim';
import { SecureChannelBaseParams, SecureChannelResponse, TrustedPublicKey } from '../../src/common/security/security.common';
export interface SecureHttpCommConfig extends SecureChannelBaseParams {
user: string;
endpoint: string;
encryptedChannelPath?: string;
encryptedApiPath?: string;
defaultTimeout?: number;
defaultChannelExpire?: number;
revealPath?: boolean;
}
export interface SecureRequest {
id?: string;
method?: string;
path: string;
body?: string;
headers?: {[name: string]: string};
timeout?: number;
revealPath?: boolean;
}
export class SecureHttpComm {
pending: Promise<boolean>;
config: SecureHttpCommConfig;
channel: SecureChannel;
trustResolved?: TrustedPublicKey;
tokenResolved?: string;
channelPublicKeyBase64?: string;
error: Error;
errors: Error[] = [];
constructor(config: SecureHttpCommConfig) {
if (!config.encryptedChannelPath) { config.encryptedChannelPath = HTTP.SHIM.ROOT_API_NEW_CHANNEL; }
if (!config.encryptedApiPath) { config.encryptedApiPath = HTTP.SHIM.ROOT_API_SECURE_API; }
if (!config.defaultTimeout) { config.defaultTimeout = 3600; }
if (!config.defaultChannelExpire && config.defaultChannelExpire !== 0) { config.defaultChannelExpire = 3600; }
this.config = config;
this.pending = this.initialize();
}
pushError(e: Error) {
this.error = e;
this.errors.push(e);
}
getAccessorHeader(token: string, channelPubKeyB64: string, channelExp?: number) {
const headerResult = SecureHandshake.getAccessorHeader(this.config.user, channelPubKeyB64, token, this.config.signing);
return headerResult?.data;
}
initialize() {
const stackTrace = new Error();
const config = this.config;
return promise(async (resolve, reject) => {
const channelInitResult = await initiateSecureChannel({
...config,
initiateContact: async (authHeader: string) => {
try {
const res = await axios.default.get(`${config.endpoint}${config.encryptedChannelPath}`, {
timeout: 7000, headers: { Accessor: authHeader }
});
(res.request as any).stackTrace = stackTrace;
if (res.status !== 200) { return errorResult(new Error(`Not valid stamp`)); }
const trust = this.trustResolved = await resolveEntry('trust', config.trust);
this.tokenResolved = await resolveEntry('token', config.token);
const pubkey = trust ? Buffer.from(trust.publicKey, 'base64') : null;
const secureChannelResponse: SecureChannelResponse = res.data.result;
if (config.trust) {
const validResult = SecureHandshake.verifyStamp(secureChannelResponse, pubkey);
if (!validResult || validResult.bad || !validResult.data) {
return errorResult(new Error(`Not valid stamp`));
}
}
return ok(secureChannelResponse);
} catch (e) {
console.error(e.message, stackTrace);
return errorResult(e);
}
}
});
if (channelInitResult.ok) {
this.channel = channelInitResult.data;
this.channelPublicKeyBase64 = this.channel.localKeyPair.publicKey.toString('base64');
} else {
console.error(channelInitResult)
}
this.pending = null;
resolve(true);
});
}
async waitForChannel() {
if (this.pending) {
const res = await this.pending;
if (!res) { return false; }
}
return true;
}
async get<T = any>(reqObj: SecureRequest) {
reqObj.method = 'GET';
return await this.request<T>(reqObj);
}
async post<T = any>(reqObj: SecureRequest) {
reqObj.method = 'POST';
return await this.request<T>(reqObj);
}
async request<T = any>(reqObj: SecureRequest) {
return promise<T>(async (resolve, reject) => {
if (!(await this.waitForChannel())) {
this.pushError(new Error(`Unable to get channel on endpoint ${this.config.endpoint}`));
return resolve(null);
}
try {
const timeout = reqObj.timeout ? reqObj.timeout : this.config.defaultTimeout;
const encPayload = this.channel.createWrappedPayloadBase64({
id: uuidv4(),
path: reqObj.path,
headers: reqObj.headers,
body: reqObj.body,
});
const revealPath = (reqObj.revealPath || this.config.revealPath);
const reqPath = revealPath ? reqObj.path : this.config.encryptedApiPath;
if (!reqObj.path) { reqObj.path = '/'; }
if (!reqObj.path.startsWith('/')) { reqObj.path = '/' + reqObj.path; }
const authHeader = SecureHandshake.getAccessorHeader(this.config.user, this.channelPublicKeyBase64, this.tokenResolved, this.config.signing)
const encHeaders = { 'Accessor': authHeader.data, 'Encrypted-Api': 'yes' };
const finalPath = `${this.config.endpoint}${reqPath}`;
const stackTrace = new Error();
const attachResolution = (prom: Promise<axios.AxiosResponse<any, any>>) => {
return prom.then(res => {
const result = this.unwrapEncryptedResponse(res, stackTrace);
result.statusCode = res.status;
resolve(result);
})
.catch(e => {
if (e.response) {
const result = errorResult(this.handleEncryptedError(e.response, e, stackTrace));
result.statusCode = e.response.status;
reject(this.handleEncryptedError(e.response, e, stackTrace));
} else {
console.error(e);
reject(e);
}
});
};
switch (reqObj.method) {
case 'GET': {
attachResolution(axios.default.get(finalPath, { timeout, data: encPayload, headers: encHeaders }));
break;
}
case 'PUT': {
attachResolution(axios.default.put(finalPath, encPayload, { timeout, headers: encHeaders }))
break;
}
case 'POST': {
attachResolution(axios.default.post(finalPath, encPayload, { timeout, headers: encHeaders }))
break;
}
case 'PATCH': {
attachResolution(axios.default.patch(finalPath, encPayload, { timeout, headers: encHeaders }))
break;
}
case 'DELETE': {
attachResolution(axios.default.delete(finalPath, { timeout, data: encPayload, headers: encHeaders }))
break;
}
}
} catch (e) {
this.pushError(e);
resolve(null);
}
});
}
private handleEncryptedError(res: axios.AxiosResponse<any, any>, e: Error, stackTrace: Error) {
console.log(res, e);
if (res?.request) {
(res.request as any).stackTrace = stackTrace;
}
if (res?.data?.encrypted) {
const decrypted = this.unwrapEncryptedResponse(res, stackTrace);
return new Error(decrypted.message);
}
return e;
}
private unwrapEncryptedResponse(res: axios.AxiosResponse<any, any>, stackTrace: Error) {
(res.request as any).stackTrace = stackTrace;
if (!res?.data?.encrypted) { return res.data; }
const responseText = this.channel.decryptSecureChannelPayloadIntoString(res.data.payload);
if (res.data.headers) {
for (const headerName of Object.keys(res.data.headers)) {
res.headers[headerName] = res.data.headers[headerName];
}
}
if (!responseText) {
return null;
}
switch (res.data.format) {
case 'json':
return JSON.parse(responseText);
default:
return null;
}
}
}