diff --git a/README.md b/README.md index c475302..c0e0010 100644 --- a/README.md +++ b/README.md @@ -206,3 +206,13 @@ The configurable retry settings are: Setting `RETRY_CONDITION` to `""` disables retries. Setting `RETRY_MAX_ATTEMPTS` to `-1` causes it to retry indefinitely. Note, the token connector will make a total of `RETRY_MAX_ATTEMPTS` + 1 calls for a given retryable call (1 original attempt and `RETRY_MAX_ATTEMPTS` retries) + +## TLS + +Mutual TLS can be enabled by providing three environment variables: + +- `TLS_CA` +- `TLS_CERT` +- `TLS_KEY` + +Each should be a path to a file on disk. Providing all three environment variables will result in a token connector running with TLS enabled, and requiring all clients to provide client certificates signed by the certificate authority. diff --git a/src/event-stream/event-stream.service.ts b/src/event-stream/event-stream.service.ts index b83fb67..85655b4 100644 --- a/src/event-stream/event-stream.service.ts +++ b/src/event-stream/event-stream.service.ts @@ -22,7 +22,7 @@ import WebSocket from 'ws'; import { FFRequestIDHeader } from '../request-context/constants'; import { Context } from '../request-context/request-context.decorator'; import { IAbiMethod } from '../tokens/tokens.interfaces'; -import { basicAuth } from '../utils'; +import { getHttpRequestOptions, getWebsocketOptions } from '../utils'; import { Event, EventBatch, @@ -58,9 +58,7 @@ export class EventStreamSocket { this.disconnectDetected = false; this.closeRequested = false; - const auth = - this.username && this.password ? { auth: `${this.username}:${this.password}` } : undefined; - this.ws = new WebSocket(this.url, auth); + this.ws = new WebSocket(this.url, getWebsocketOptions(this.username, this.password)); this.ws .on('open', () => { if (this.disconnectDetected) { @@ -173,7 +171,7 @@ export class EventStreamService { } } headers[FFRequestIDHeader] = ctx.requestId; - const config = basicAuth(this.username, this.password); + const config = getHttpRequestOptions(this.username, this.password); config.headers = headers; return config; } diff --git a/src/health/health.controller.ts b/src/health/health.controller.ts index dc399d1..55ca86f 100644 --- a/src/health/health.controller.ts +++ b/src/health/health.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get } from '@nestjs/common'; import { HealthCheckService, HealthCheck, HttpHealthIndicator } from '@nestjs/terminus'; import { BlockchainConnectorService } from '../tokens/blockchain.service'; -import { basicAuth } from '../utils'; +import { getHttpRequestOptions } from '../utils'; @Controller('health') export class HealthController { @@ -25,7 +25,7 @@ export class HealthController { this.http.pingCheck( 'ethconnect', `${this.blockchain.baseUrl}/status`, - basicAuth(this.blockchain.username, this.blockchain.password), + getHttpRequestOptions(this.blockchain.username, this.blockchain.password), ), ]); } diff --git a/src/main.ts b/src/main.ts index f5e9844..308b816 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ShutdownSignal, ValidationPipe } from '@nestjs/common'; +import { NestApplicationOptions, ShutdownSignal, ValidationPipe } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; import { WsAdapter } from '@nestjs/platform-ws'; @@ -36,6 +36,7 @@ import { import { TokensService } from './tokens/tokens.service'; import { newContext } from './request-context/request-context.decorator'; import { AbiMapperService } from './tokens/abimapper.service'; +import { getNestOptions } from './utils'; const API_DESCRIPTION = `
All POST APIs are asynchronous. Listen for websocket notifications on /api/ws
.
@@ -50,7 +51,8 @@ export function getApiConfig() {
}
async function bootstrap() {
- const app = await NestFactory.create(AppModule);
+ const app = await NestFactory.create(AppModule, getNestOptions());
+
app.setGlobalPrefix('api/v1');
app.useWebSocketAdapter(new WsAdapter(app));
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
diff --git a/src/tokens/blockchain.service.ts b/src/tokens/blockchain.service.ts
index 3d3ce30..e321c4d 100644
--- a/src/tokens/blockchain.service.ts
+++ b/src/tokens/blockchain.service.ts
@@ -25,7 +25,7 @@ import {
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { lastValueFrom } from 'rxjs';
import { EventStreamReply } from '../event-stream/event-stream.interfaces';
-import { basicAuth } from '../utils';
+import { getHttpRequestOptions } from '../utils';
import { Context } from '../request-context/request-context.decorator';
import { FFRequestIDHeader } from '../request-context/constants';
import { EthConnectAsyncResponse, EthConnectReturn, IAbiMethod } from './tokens.interfaces';
@@ -80,7 +80,7 @@ export class BlockchainConnectorService {
}
}
headers[FFRequestIDHeader] = ctx.requestId;
- const config = basicAuth(this.username, this.password);
+ const config = getHttpRequestOptions(this.username, this.password);
config.headers = headers;
return config;
}
diff --git a/src/utils.ts b/src/utils.ts
index 06f2876..2aa5db6 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,6 +1,52 @@
+import * as fs from 'fs';
+import * as https from 'https';
+import { NestApplicationOptions } from '@nestjs/common';
import { AxiosRequestConfig } from 'axios';
+import { ClientOptions } from 'ws';
-export const basicAuth = (username: string, password: string) => {
+interface Certificates {
+ key: string;
+ cert: string;
+ ca: string;
+}
+
+const getCertificates = (): Certificates | undefined => {
+ let key, cert, ca;
+ if (
+ process.env['TLS_KEY'] === undefined ||
+ process.env['TLS_CERT'] === undefined ||
+ process.env['TLS_CA'] === undefined
+ ) {
+ return undefined;
+ }
+ try {
+ key = fs.readFileSync(process.env['TLS_KEY']).toString();
+ cert = fs.readFileSync(process.env['TLS_CERT']).toString();
+ ca = fs.readFileSync(process.env['TLS_CA']).toString();
+ } catch (error) {
+ console.error(`Error reading certificates: ${error}`);
+ process.exit(-1);
+ }
+ return { key, cert, ca };
+};
+
+export const getWebsocketOptions = (username: string, password: string): ClientOptions => {
+ const requestOptions: ClientOptions = {};
+ if (username && username !== '' && password && password !== '') {
+ requestOptions.headers = {
+ Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
+ };
+ }
+ const certs = getCertificates();
+ if (certs) {
+ requestOptions.ca = certs.ca;
+ requestOptions.cert = certs.cert;
+ requestOptions.key = certs.key;
+ }
+ return requestOptions;
+};
+
+export const getHttpRequestOptions = (username: string, password: string) => {
const requestOptions: AxiosRequestConfig = {};
if (username !== '' && password !== '') {
requestOptions.auth = {
@@ -8,5 +54,18 @@ export const basicAuth = (username: string, password: string) => {
password: password,
};
}
+ const certs = getCertificates();
+ if (certs) {
+ requestOptions.httpsAgent = new https.Agent({ ...certs, requestCert: true });
+ }
return requestOptions;
};
+
+export const getNestOptions = (): NestApplicationOptions => {
+ const options: NestApplicationOptions = {};
+ const certs = getCertificates();
+ if (certs) {
+ options.httpsOptions = certs;
+ }
+ return options;
+};