From 178a0eab2cf567ff67e2f6f26ab4056f3264c023 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Fri, 4 Jun 2021 20:13:22 -0400 Subject: [PATCH] Introduce new features to enhance portability --- README.md | 40 ++++++++++++++++--------- data/member-a/config.json | 12 ++++++-- data/member-b/config.json | 14 ++++++--- package-lock.json | 17 +++++++++-- package.json | 4 ++- src/app.ts | 17 ++++++----- src/custom.d.ts | 1 + src/lib/cert.ts | 3 ++ src/lib/interfaces.ts | 21 +++++++++---- src/lib/utils.ts | 33 ++++++++++++++++++++- src/routers/api.ts | 32 +++++++++++++------- src/routers/p2p.ts | 4 +-- src/schemas/config.json | 38 ++++++++++++++++++++---- src/swagger.yaml | 62 +++++++++++++++++++++++++++------------ 14 files changed, 222 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 8a9b117..f47f335 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,18 @@ Create `config.json` in the data directory and set its content to: ``` { "$schema": "../../src/schemas/config.json", - "apiPort": 3000, - "p2pPort": 3001, + "api": { + "hostname": "localhost", + "port": 3000 + }, + "p2p": { + "hostname": "localhost", + "port": 3001 + }, "apiKey": "xxxxx", "peers": [ { - "name": "org-b", + "id": "org-b", "endpoint": "https://localhost:4001" } ] @@ -35,9 +41,9 @@ Create `config.json` in the data directory and set its content to: ``` Based on this configuration: -- Port 3000 will be used to access the API -- Port 3001 will be used for P2P communications -- The API key will be set to `xxxxx` +- API will be accessed via localhost:3000 +- P2P communications will use localhost:3001 +- The API key will be set to `xxxxx` (this is optional) - There is one peer named `org-b` whose P2P endpoint is `https://localhost:4001` #### Generate certificate @@ -63,22 +69,28 @@ export LOG_LEVEL=info ``` { "$schema": "../../src/schemas/config.json", - "apiPort": 4000, - "p2pPort": 4001, - "apiKey": "yyyyy", + "api": { + "hostname": "localhost", + "port": 4000 + }, + "p2p": { + "hostname": "localhost", + "port": 4001 + }, + "apiKey": "xxxxx", "peers": [ { - "name": "org-b", - "endpoint": "https://localhost:4001" + "id": "org-b", + "endpoint": "https://localhost:3001" } ] } ``` Based on this configuration: -- Port 4000 will be used to access the API -- Port 4001 will be used for P2P communications -- The API key will be set to `yyyyy` +- API will be accessed via localhost:4000 +- P2P communications will use localhost:4001 +- The API key will be set to `xxxxx` (this is optional) - There is one peer named `org-a` whose P2P endpoint is `https://localhost:3001` diff --git a/data/member-a/config.json b/data/member-a/config.json index 55ae27e..8fdfbf1 100644 --- a/data/member-a/config.json +++ b/data/member-a/config.json @@ -1,11 +1,17 @@ { "$schema": "../../src/schemas/config.json", - "apiPort": 3000, - "p2pPort": 3001, + "api": { + "hostname": "localhost", + "port": 3000 + }, + "p2p": { + "hostname": "localhost", + "port": 3001 + }, "apiKey": "xxxxx", "peers": [ { - "name": "org-b", + "id": "org-b", "endpoint": "https://localhost:4001" } ] diff --git a/data/member-b/config.json b/data/member-b/config.json index f7f9970..4b4701c 100644 --- a/data/member-b/config.json +++ b/data/member-b/config.json @@ -1,11 +1,17 @@ { "$schema": "../../src/schemas/config.json", - "apiPort": 4000, - "p2pPort": 4001, - "apiKey": "yyyyy", + "api": { + "hostname": "localhost", + "port": 4000 + }, + "p2p": { + "hostname": "localhost", + "port": 4001 + }, + "apiKey": "xxxxx", "peers": [ { - "name": "org-a", + "id": "org-b", "endpoint": "https://localhost:3001" } ] diff --git a/package-lock.json b/package-lock.json index 010ad7e..545f039 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,12 @@ "@types/range-parser": "*" } }, + "@types/jsrsasign": { + "version": "8.0.12", + "resolved": "https://registry.npmjs.org/@types/jsrsasign/-/jsrsasign-8.0.12.tgz", + "integrity": "sha512-FLXKbwbB+4fsJECYOpIiYX2GSqSHYnkO/UnrFqlZn6crpyyOtk4LRab+G1HC7dTbT1NB7spkHecZRQGXoCWiJQ==", + "dev": true + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -133,9 +139,9 @@ } }, "ajv": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.4.0.tgz", - "integrity": "sha512-7QD2l6+KBSLwf+7MuYocbWvRPdOu63/trReTLu2KFwkgctnub1auoF+Y1WYcm09CTM7quuscrzqmASaLHC/K4Q==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.5.0.tgz", + "integrity": "sha512-Y2l399Tt1AguU3BPRP9Fn4eN+Or+StUGWCUpbnFyXSo8NZ9S4uj+AG2pjs5apK+ZMOwYOz1+a+VKvKH7CudXgQ==", "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -483,6 +489,11 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "jsrsasign": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.3.0.tgz", + "integrity": "sha512-irDIKKFW++EAELgP3fjFi5/Fn0XEyfuQTTgpbeFwCGkV6tRIYZl3uraRea2HTXWCstcSZuDaCbdAhU1n+075Bg==" + }, "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", diff --git a/package.json b/package.json index badfa5a..22ebc40 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,13 @@ "author": "", "license": "ISC", "dependencies": { - "ajv": "^8.4.0", + "ajv": "^8.5.0", "axios": "^0.21.1", "bunyan": "^1.8.15", "busboy": "^0.3.1", "express": "^4.17.1", "form-data": "^4.0.0", + "jsrsasign": "^10.3.0", "swagger-ui-express": "^4.1.6", "ts-node": "^9.1.1", "typescript": "^4.2.4", @@ -31,6 +32,7 @@ "@types/bunyan": "^1.8.6", "@types/busboy": "^0.2.3", "@types/express": "^4.17.11", + "@types/jsrsasign": "^8.0.12", "@types/node": "^15.0.3", "@types/swagger-ui-express": "^4.1.2", "@types/ws": "^7.4.4", diff --git a/src/app.ts b/src/app.ts index 37b8061..bd0daab 100644 --- a/src/app.ts +++ b/src/app.ts @@ -67,13 +67,13 @@ export const start = async () => { p2pEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); blobsEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); messagesEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); - + eventsHandler.eventEmitter.addListener('event', event => { - if(delegatedWebSocket !== undefined) { + if (delegatedWebSocket !== undefined) { delegatedWebSocket.send(JSON.stringify(event)); } }); - + const assignWebSocketDelegate = (webSocket: WebSocket) => { delegatedWebSocket = webSocket; const event = eventsHandler.getCurrentEvent(); @@ -96,7 +96,7 @@ export const start = async () => { }; wss.on('connection', (webSocket: WebSocket) => { - if(delegatedWebSocket === undefined) { + if (delegatedWebSocket === undefined) { assignWebSocketDelegate(webSocket); } }); @@ -107,7 +107,7 @@ export const start = async () => { if (req.path === '/') { res.redirect('/swagger'); } else { - if (req.headers['x-api-key'] !== config.apiKey) { + if (config.apiKey !== undefined && req.headers['x-api-key'] !== config.apiKey) { next(new RequestError('Unauthorized', 401)); } else { next(); @@ -123,9 +123,10 @@ export const start = async () => { p2pApp.use('/api/v1', p2pRouter); p2pApp.use(errorHandler); - const apiServerPromise = new Promise(resolve => apiServer.listen(config.apiPort, () => resolve())); - const p2pServerPromise = new Promise(resolve => p2pServer.listen(config.p2pPort, () => resolve())); + const apiServerPromise = new Promise(resolve => apiServer.listen(config.api.port, config.api.hostname, () => resolve())); + const p2pServerPromise = new Promise(resolve => p2pServer.listen(config.p2p.port, config.p2p.hostname, () => resolve())); await Promise.all([apiServerPromise, p2pServerPromise]); - log.info(`Data exchange listening on ports ${config.apiPort} (API) and ${config.p2pPort} (P2P) - log level "${utils.constants.LOG_LEVEL}"`); + log.info(`Data exchange running on http://${config.api.hostname}:${config.api.port} (API) and ` + + `https://${config.p2p.hostname}:${config.p2p.port} (P2P) - log level "${utils.constants.LOG_LEVEL}"`); }; diff --git a/src/custom.d.ts b/src/custom.d.ts index c391773..1ba0bf2 100644 --- a/src/custom.d.ts +++ b/src/custom.d.ts @@ -29,6 +29,7 @@ declare global{ getPeerCertificate: () => { issuer: { O: string + OU: string } } } diff --git a/src/lib/cert.ts b/src/lib/cert.ts index 53bf6b1..03a23f7 100644 --- a/src/lib/cert.ts +++ b/src/lib/cert.ts @@ -24,10 +24,13 @@ const log = createLogger({ name: 'lib/certs.ts', level: utils.constants.LOG_LEVE export let key: string; export let cert: string; export let ca: string[] = []; +export let peerID: string; export const init = async () => { key = (await fs.readFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.KEY_FILE))).toString(); cert = (await fs.readFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.CERT_FILE))).toString(); + const certData = utils.getCertData(cert); + peerID = utils.getPeerID(certData.organization, certData.organizationUnit); await loadCAs(); }; diff --git a/src/lib/interfaces.ts b/src/lib/interfaces.ts index f879b59..cc72c50 100644 --- a/src/lib/interfaces.ts +++ b/src/lib/interfaces.ts @@ -15,11 +15,17 @@ // limitations under the License. export interface IConfig { - apiPort: number - p2pPort: number - apiKey: string + api: { + hostname: string + port: number + } + p2p: { + hostname: string + port: number + } + apiKey?: string peers: { - name: string + id: string endpoint: string }[] } @@ -107,7 +113,12 @@ export type BlobTask = { export interface IStatus { messageQueueSize: number peers: { - name: string + id: string available: boolean }[] } + +export interface ICertData { + organization?: string + organizationUnit?: string +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 9f3c56a..78983e4 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -16,11 +16,12 @@ import { Request } from 'express'; import { promises as fs } from 'fs'; -import { IFile } from './interfaces'; +import { ICertData, IFile } from './interfaces'; import RequestError from './request-error'; import Busboy from 'busboy'; import axios, { AxiosRequestConfig } from 'axios'; import { createLogger, LogLevelString } from 'bunyan'; +import { X509 } from 'jsrsasign'; export const constants = { LOG_LEVEL: process.env.LOG_LEVEL || 'info', @@ -115,3 +116,33 @@ export const axiosWithRetry = async (config: AxiosRequestConfig) => { } throw currentError; }; + +export const getPeerID = (organization: string | undefined, organizationUnit: string | undefined) => { + if(organization !== undefined) { + if(organizationUnit !== undefined) { + return `${organization}-${organizationUnit}`; + } else { + return organization; + } + } else if(organizationUnit !== undefined) { + return organizationUnit; + } else { + throw new Error('Invalid peer'); + } +}; + +export const getCertData = (cert: string): ICertData => { + const x509 = new X509(); + x509.readCertPEM(cert); + const subject = x509.getSubjectString(); + const o = subject.match(/O=(.+[^/])/); + let certData: ICertData = {}; + if(o !== null) { + certData.organization = o[1]; + } + const ou = subject.match(/OU=(.+[^/])/); + if(ou !== null) { + certData.organizationUnit = ou[1]; + } + return certData; +}; \ No newline at end of file diff --git a/src/routers/api.ts b/src/routers/api.ts index d279afa..80cd669 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -22,13 +22,25 @@ import RequestError from '../lib/request-error'; import { config, persistConfig } from '../lib/config'; import { IStatus } from '../lib/interfaces'; import https from 'https'; -import { key, cert, ca, loadCAs } from '../lib/cert'; +import { key, cert, ca, loadCAs, peerID } from '../lib/cert'; import * as eventsHandler from '../handlers/events'; import { promises as fs } from 'fs'; import path from 'path'; export const router = Router(); +router.get('/id', async (_req, res, next) => { + try { + res.send({ + id: peerID, + endpoint: `https://${config.p2p.hostname}:${config.p2p.port}`, + cert + }); + } catch (err) { + next(err); + } +}); + router.get('/status', async (_req, res, next) => { try { let status: IStatus = { @@ -48,7 +60,7 @@ router.get('/status', async (_req, res, next) => { let i = 0; for (const peer of config.peers) { status.peers.push({ - name: peer.name, + id: peer.id, available: responses[i++].status === 'fulfilled' }) } @@ -62,7 +74,7 @@ router.get('/peers', (_req, res) => { res.send(config.peers); }); -router.put('/peers/:name', async (req, res, next) => { +router.put('/peers/:id', async (req, res, next) => { try { if (req.body.endpoint === undefined) { throw new RequestError('Missing endpoint', 400); @@ -70,10 +82,10 @@ router.put('/peers/:name', async (req, res, next) => { if (req.body.certificate !== undefined) { await fs.writeFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.PEER_CERTS_SUBDIRECTORY, `${req.params.name}.pem`), req.body.certificate); } - let peer = config.peers.find(peer => peer.name === req.params.name); + let peer = config.peers.find(peer => peer.id === req.params.id); if (peer === undefined) { peer = { - name: req.params.name, + id: req.params.id, endpoint: req.body.endpoint }; config.peers.push(peer); @@ -86,9 +98,9 @@ router.put('/peers/:name', async (req, res, next) => { } }); -router.delete('/peers/:name', async (req, res, next) => { +router.delete('/peers/:id', async (req, res, next) => { try { - if (!config.peers.some(peer => peer.name === req.params.name)) { + if (!config.peers.some(peer => peer.id === req.params.id)) { throw new RequestError('Peer not found', 404); } try { @@ -98,7 +110,7 @@ router.delete('/peers/:name', async (req, res, next) => { throw new RequestError(`Failed to remove peer certificate`); } } - config.peers = config.peers.filter(peer => peer.name !== req.params.name); + config.peers = config.peers.filter(peer => peer.id !== req.params.id); await persistConfig(); await loadCAs(); res.send({ status: 'removed' }); @@ -115,7 +127,7 @@ router.post('/messages', async (req, res, next) => { if (req.body.recipient === undefined) { throw new RequestError('Missing recipient', 400); } - let recipientURL = config.peers.find(peer => peer.name === req.body.recipient)?.endpoint; + let recipientURL = config.peers.find(peer => peer.id === req.body.recipient)?.endpoint; if (recipientURL === undefined) { throw new RequestError(`Unknown recipient`, 400); } @@ -169,7 +181,7 @@ router.post('/transfers', async (req, res, next) => { if (req.body.recipient === undefined) { throw new RequestError('Missing recipient', 400); } - let recipientURL = config.peers.find(peer => peer.name === req.body.recipient)?.endpoint; + let recipientURL = config.peers.find(peer => peer.id === req.body.recipient)?.endpoint; if (recipientURL === undefined) { throw new RequestError(`Unknown recipient`, 400); } diff --git a/src/routers/p2p.ts b/src/routers/p2p.ts index be68c4d..8d43cea 100644 --- a/src/routers/p2p.ts +++ b/src/routers/p2p.ts @@ -31,7 +31,7 @@ router.head('/ping', (_req, res) => { router.post('/messages', async (req, res, next) => { try { const cert = req.client.getPeerCertificate(); - const sender = cert.issuer.O; + const sender = cert.issuer.O + cert.issuer.OU; const message = await utils.extractMessageFromMultipartForm(req); eventEmitter.emit('event', { type: 'message-received', @@ -47,7 +47,7 @@ router.post('/messages', async (req, res, next) => { router.put('/blobs/*', async (req, res, next) => { try { const cert = req.client.getPeerCertificate(); - const sender = cert.issuer.O; + const sender = cert.issuer.O + cert.issuer.OU; const file = await utils.extractFileFromMultipartForm(req); const blobPath = path.join(utils.constants.RECEIVED_BLOBS_SUBDIRECTORY, sender, req.params[0]); const hash = await blobsHandler.storeBlob(file, blobPath); diff --git a/src/schemas/config.json b/src/schemas/config.json index 85e74db..f029537 100644 --- a/src/schemas/config.json +++ b/src/schemas/config.json @@ -2,14 +2,40 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": [ - "apiPort", - "p2pPort", - "apiKey", + "api", + "p2p", "peers" ], "properties": { - "port": { - "type": "integer" + "api": { + "type": "object", + "required": [ + "hostname", + "port" + ], + "properties": { + "hostname": { + "type": "string" + }, + "port": { + "type": "integer" + } + } + }, + "p2p": { + "type": "object", + "required": [ + "hostname", + "port" + ], + "properties": { + "hostname": { + "type": "string" + }, + "port": { + "type": "integer" + } + } }, "apiKey": { "type": "string" @@ -19,7 +45,7 @@ "items": { "type": "object", "required": [ - "name", + "id", "endpoint" ], "properties": { diff --git a/src/swagger.yaml b/src/swagger.yaml index c21984b..08b6bb1 100644 --- a/src/swagger.yaml +++ b/src/swagger.yaml @@ -7,6 +7,24 @@ servers: - url: /api/v1 paths: + /id: + get: + tags: + - ID + description: Peer information + responses: + '200': + description: Peer information + content: + application/json: + schema: + $ref: '#/components/schemas/PeerInformation' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /status: get: tags: @@ -43,18 +61,18 @@ application/json: schema: $ref: '#/components/schemas/Error' - /peers/{name}: + /peers/{id}: + parameters: + - in: path + name: id + required: true + schema: + type: string + description: Peer id put: tags: - Peers description: Add peer - parameters: - - in: path - name: name - required: true - schema: - type: string - description: Peer name responses: '200': description: Peer added @@ -78,13 +96,6 @@ tags: - Peers description: Remove peer - parameters: - - in: path - name: name - required: true - schema: - type: string - description: Peer name responses: '200': description: Peer removed @@ -250,6 +261,19 @@ in: header name: X-API-KEY schemas: + PeerInformation: + type: object + required: + - id + - endpoint + - cert + properties: + id: + type: string + endpoint: + type: string + cert: + type: string Status: type: object required: @@ -263,20 +287,20 @@ items: type: object required: - - name + - id - available properties: - name: + id: type: string available: type: boolean Peer: type: object required: - - name + - id - endpoint properties: - name: + id: type: string endpoint: type: string