From 9eab75cf88bc180ec2a94f91b156a1acea5274e4 Mon Sep 17 00:00:00 2001 From: Michal Bajer Date: Wed, 14 Jun 2023 13:00:13 +0000 Subject: [PATCH] feat(cactus-plugin-ledger-connector-cdl-socketio): add new connector plugin - Add new plugin for connecting with Fujitsu CDL. - Add test script for checking connector operations manually. Closes: #2455 Signed-off-by: Michal Bajer --- jest.config.js | 1 + .../Dockerfile | 13 + .../README.md | 81 ++++ .../package.json | 81 ++++ .../sample-config/default.yaml | 11 + .../src/main/typescript/common/core/app.ts | 53 +++ .../main/typescript/common/core/bin/www.ts | 176 ++++++++ .../src/main/typescript/common/core/config.ts | 37 ++ .../main/typescript/connector/ServerPlugin.ts | 228 ++++++++++ .../connector/ValidatorAuthentication.ts | 31 ++ .../main/typescript/connector/cdl-request.ts | 82 ++++ .../src/main/typescript/index.ts | 1 + .../src/main/typescript/public-api.ts | 1 + .../integration/cdl-connector-manual.test.ts | 419 ++++++++++++++++++ .../tsconfig.json | 27 ++ tsconfig.json | 10 +- yarn.lock | 144 +++++- 17 files changed, 1383 insertions(+), 13 deletions(-) create mode 100644 packages/cactus-plugin-ledger-connector-cdl-socketio/Dockerfile create mode 100644 packages/cactus-plugin-ledger-connector-cdl-socketio/README.md create mode 100644 packages/cactus-plugin-ledger-connector-cdl-socketio/package.json create mode 100644 packages/cactus-plugin-ledger-connector-cdl-socketio/sample-config/default.yaml create mode 100644 packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/common/core/app.ts create mode 100644 packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/common/core/bin/www.ts create mode 100644 packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/common/core/config.ts create mode 100644 packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/connector/ServerPlugin.ts create mode 100644 packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/connector/ValidatorAuthentication.ts create mode 100644 packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/connector/cdl-request.ts create mode 100644 packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/index.ts create mode 100644 packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/public-api.ts create mode 100644 packages/cactus-plugin-ledger-connector-cdl-socketio/src/test/typescript/integration/cdl-connector-manual.test.ts create mode 100644 packages/cactus-plugin-ledger-connector-cdl-socketio/tsconfig.json diff --git a/jest.config.js b/jest.config.js index bf6e047e68b..e28ffcdb75f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -97,6 +97,7 @@ module.exports = { `./packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/openapi/openapi-validation-go.test.ts`, `./packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/openapi/openapi-validation.test.ts`, `./packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/unit/identity-internal-crypto-utils.test.ts`, + `./packages/cactus-plugin-ledger-connector-cdl-socketio/src/test/typescript/integration/cdl-connector-manual.test.ts`, `./packages/cactus-plugin-keychain-vault/src/test/typescript/integration/cactus-keychain-vault-server.test.ts`, `./packages/cactus-plugin-keychain-vault/src/test/typescript/integration/plugin-keychain-vault.test.ts`, `./packages/cactus-plugin-keychain-vault/src/test/typescript/integration/openapi/openapi-validation.test.ts`, diff --git a/packages/cactus-plugin-ledger-connector-cdl-socketio/Dockerfile b/packages/cactus-plugin-ledger-connector-cdl-socketio/Dockerfile new file mode 100644 index 00000000000..b1a951028e9 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-cdl-socketio/Dockerfile @@ -0,0 +1,13 @@ +# TODO +# Install connector as yarn package like in @hyperledger/cactus-plugin-ledger-connector-besu + +FROM node:18 + +WORKDIR /root/cactus/ + +COPY ./dist ./dist/ +COPY ./dist/yarn.lock ./package.json ./ +RUN yarn install --production --ignore-engines --non-interactive --cache-folder ./.yarnCache; rm -rf ./.yarnCache + +EXPOSE 5061 +CMD [ "npm", "run", "start" ] diff --git a/packages/cactus-plugin-ledger-connector-cdl-socketio/README.md b/packages/cactus-plugin-ledger-connector-cdl-socketio/README.md new file mode 100644 index 00000000000..1e44c8e66fc --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-cdl-socketio/README.md @@ -0,0 +1,81 @@ +# `@hyperledger/cactus-plugin-ledger-connector-cdl-socketio` + +This plugin provides `Cacti` a way to interact with Fujitsu CDL networks. Using this we can perform: + +- `sendSyncRequest`: Send sync-typed requests to the API. +- `sendAsyncRequest`: Send async-typed requests to the API. + +## Getting started + +### Required software components + +- OS: Linux (recommended Ubuntu20.04,18.04 or CentOS7) +- Docker (recommend: v17.06.2-ce or greater) +- node.js v12 (recommend: v12.20.2 or greater) + +## Boot methods + +### Common setup + +1. Always run configure command first, from the project root directory: + + ```bash + pushd ../.. + npm run configure + popd + ``` + +1. Copy default configuration + +- **Remember to replace default CA and to adjust the `default.yaml` configuration on production deployments!** + ```bash + mkdir -p /etc/cactus/connector-cdl-socketio + rm -r /etc/cactus/connector-cdl-socketio/* + cp -rf ./sample-config/* /etc/cactus/connector-cdl-socketio/ + ``` + +#### Configuring CDL API Gateway Access + +- Set the base URL of GW service in `cdlApiGateway.url`. Do not include `api/v1`, just the base URL. (example: `"http://localhost:3000"`). +- If the service certificate is signed with a known CA (node uses Mozilla DB), then you can skip the next steps. +- If the service is signed with unknown CA, you can specify the gateway certificate to trust manually: + - Set `cdlApiGateway.caPath` to path of API Gateway certificate (in PEM format). (example: `"/etc/cactus/connector-cdl-socketio/CA/cdl-api-gateway-ca.pem"`) + - (optional) If server name in cert doesn't match the one in `cdlApiGateway.url`, you can overwrite it in `cdlApiGateway.serverName` +- (not recommended - only for development): To ignore certificate rejection (e.g. use self-signed certificate) set `cdlApiGateway.skipCertCheck` to `true`. + +### Docker + +- Docker build process will use artifacts from the latest build. Make sure `./dist` contains the version you want to dockerize. + +``` +# Build +DOCKER_BUILDKIT=1 docker build ./packages/cactus-plugin-ledger-connector-cdl-socketio -t cactus-plugin-ledger-connector-cdl-socketio + +# Run +docker run -v/etc/cactus/:/etc/cactus -p 5061:5061 cactus-plugin-ledger-connector-cdl-socketio +``` + +### Manual + +``` +npm run start +``` + +## Configuration + +- Validator can be configured in `/etc/cactus/connector-cdl-socketio/default.yaml` (see [sample-config](./sample-config/default.yaml) for details). + +## Manual Tests + +- `cdl-connector-manual.test` contains a Jest test script that will check every implemented operation on running CDL instance. +- Script can be used as a quick reference for using this connector plugin. + +## Contributing + +We welcome contributions to Hyperledger Cacti in many forms, and there's always plenty to do! + +Please review [CONTIRBUTING.md](../../CONTRIBUTING.md) to get started. + +## License + +This distribution is published under the Apache License Version 2.0 found in the [LICENSE](../../LICENSE) file. diff --git a/packages/cactus-plugin-ledger-connector-cdl-socketio/package.json b/packages/cactus-plugin-ledger-connector-cdl-socketio/package.json new file mode 100644 index 00000000000..066da575eaa --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-cdl-socketio/package.json @@ -0,0 +1,81 @@ +{ + "name": "@hyperledger/cactus-plugin-ledger-connector-cdl-socketio", + "version": "2.0.0-alpha.1", + "description": "Allows Cacti nodes to connect to Fujitsu CDL.", + "keywords": [ + "Hyperledger", + "Cacti", + "Integration", + "Blockchain", + "Distributed Ledger Technology" + ], + "homepage": "https://github.com/hyperledger/cacti#readme", + "bugs": { + "url": "https://github.com/hyperledger/cacti/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hyperledger/cacti.git" + }, + "license": "Apache-2.0", + "author": { + "name": "Hyperledger Cacti Contributors", + "email": "cacti@lists.hyperledger.org", + "url": "https://www.hyperledger.org/use/cacti" + }, + "contributors": [ + { + "name": "Please add yourself to the list of contributors", + "email": "your.name@example.com", + "url": "https://example.com" + }, + { + "name": "Michal Bajer", + "email": "michal.bajer@fujitsu.com", + "url": "https://www.fujitsu.com/global/" + } + ], + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/*" + ], + "scripts": { + "build": "npm run build-ts && npm run build:dev:backend:postbuild", + "build-ts": "tsc", + "build:dev:backend:postbuild": "npm run init-cdl", + "debug": "nodemon --inspect ./dist/common/core/bin/www.js", + "init-cdl": "cp -af ../../yarn.lock ./dist/yarn.lock", + "start": "cd ./dist && node common/core/bin/www.js" + }, + "dependencies": { + "axios": "0.27.2", + "body-parser": "1.17.2", + "config": "3.3.7", + "cookie-parser": "1.4.6", + "express": "4.15.5", + "fast-safe-stringify": "2.1.1", + "http-errors": "1.6.3", + "js-yaml": "3.14.1", + "jsonwebtoken": "8.5.1", + "log4js": "6.4.1", + "sanitize-html": "2.7.0", + "socket.io": "4.4.1" + }, + "devDependencies": { + "@types/config": "0.0.41", + "@types/node": "14.17.32", + "@types/sanitize-html": "2.6.2", + "jest-extended": "0.11.5", + "uuid": "8.3.2" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "publishConfig": { + "access": "public" + }, + "watch": {} +} diff --git a/packages/cactus-plugin-ledger-connector-cdl-socketio/sample-config/default.yaml b/packages/cactus-plugin-ledger-connector-cdl-socketio/sample-config/default.yaml new file mode 100644 index 00000000000..15a82aa28df --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-cdl-socketio/sample-config/default.yaml @@ -0,0 +1,11 @@ +sslParam: + port: 5061 + key: "/etc/cactus/connector-cdl-socketio/CA/connector.priv" + cert: "/etc/cactus/connector-cdl-socketio/CA/connector.crt" +logLevel: "debug" +userAgent: "CactiCDLConnector" +cdlApiGateway: + url: "http://localhost:3000" + #skipCertCheck: true # Set to true to ignore self-signed and other rejected certificates + #caPath: "/etc/cactus/connector-cdl-socketio/CA/cdl-api-gateway-ca.pem" # CA of CDL API gateway server in PEM format to use + #serverName: "cdl.fujitsu" # Overwrite server name from cdlApiGateway.url to match one specified in CA diff --git a/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/common/core/app.ts b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/common/core/app.ts new file mode 100644 index 00000000000..14da5c56ca6 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/common/core/app.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Hyperledger Cacti Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * app.js + */ + +/* Summary: + * + */ + +import { NextFunction, Request, Response } from "express"; +import createError from "http-errors"; +import express from "express"; +import cookieParser from "cookie-parser"; +import bodyParser from "body-parser"; + +const app: express.Express = express(); + +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(cookieParser()); + +// catch 404 and forward to error handler +app.use((req: Request, res: Response, next: NextFunction) => { + next(createError(404)); +}); + +// error handler +app.use( + ( + err: { message: string; status?: number }, + req: Request, + res: Response, + next: NextFunction, + ) => { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get("env") === "development" ? err : {}; + + // set erreor response + const errorResponse: {} = { + statusCode: err.status || 500, + message: err.message, + }; + + // render the error page + res.status(err.status || 500); + res.send(errorResponse); + }, +); + +export default app; diff --git a/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/common/core/bin/www.ts b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/common/core/bin/www.ts new file mode 100644 index 00000000000..181ed024a42 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/common/core/bin/www.ts @@ -0,0 +1,176 @@ +#!/usr/bin/env node + +import app from "../app"; +import { read as configRead } from "../config"; + +import axios from "axios"; +import https from "https"; +import fs from "fs"; +import { Server } from "socket.io"; +import safeStringify from "fast-safe-stringify"; +import sanitizeHtml from "sanitize-html"; + +// Log settings +import { getLogger } from "log4js"; +const logger = getLogger("connector_main[" + process.pid + "]"); +logger.level = configRead("logLevel", "info"); + +// implementation class of a part dependent of end-chains (server plugin) +import { ServerPlugin } from "../../../connector/ServerPlugin"; +import { ValidatorAuthentication } from "../../../connector/ValidatorAuthentication"; + +// Normalize a port into a number, string, or false. +function normalizePort(val: string) { + const port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Return secure string representation of error from the input. + * Handles circular structures and removes HTML. + * + * @param error Any object to return as an error, preferable `Error` + * @returns Safe string representation of an error. + */ +export function safeStringifyException(error: unknown): string { + if (axios.isAxiosError(error)) { + return safeStringify(error.toJSON()); + } + + if (error instanceof Error) { + return sanitizeHtml(error.stack || error.message); + } + + return sanitizeHtml(safeStringify(error)); +} + +export async function startCDLSocketIOConnector() { + const Splug = new ServerPlugin(); + + // Get port from environment and store in Express. + const sslport = normalizePort( + process.env.PORT || configRead("sslParam.port"), + ); + app.set("port", sslport); + + // Specify private key and certificate + let keyString: string; + let certString: string; + try { + keyString = configRead("sslParam.keyValue"); + certString = configRead("sslParam.certValue"); + } catch { + keyString = fs.readFileSync(configRead("sslParam.key"), "ascii"); + certString = fs.readFileSync(configRead("sslParam.cert"), "ascii"); + } + + // Create HTTPS server. + const server = https.createServer( + { + key: keyString, + cert: certString, + }, + app, + ); // Start as an https server. + const io = new Server(server); + + // Event listener for HTTPS server "error" event. + server.on("error", (error: any) => { + if (error.syscall !== "listen") { + throw error; + } + + const bind = + typeof sslport === "string" ? "Pipe " + sslport : "Port " + sslport; + + // handle specific listen errors with friendly messages + switch (error.code) { + case "EACCES": + console.error(bind + " requires elevated privileges"); + process.exit(1); + break; + case "EADDRINUSE": + console.error(bind + " is already in use"); + process.exit(1); + break; + default: + throw error; + } + }); + + io.on("connection", (client) => { + logger.info("Client " + client.id + " connected."); + + client.on("request2", async (data) => { + try { + const result = await Splug.executeFunction({ + method: data.method, + args: data.args, + reqID: data.reqID, + }); + const response = { + resObj: { + status: 200, + data: ValidatorAuthentication.sign({ result }), + }, + id: data.reqID, + }; + logger.info("Client ID :" + client.id); + logger.info("Response:", JSON.stringify(response)); + client.emit("response", response); + } catch (error: unknown) { + const errorObj = { + resObj: { + status: 504, + errorDetail: safeStringifyException(error), + }, + id: data.reqID, + }; + logger.error("request2 connector_error:", JSON.stringify(errorObj)); + client.emit("connector_error", errorObj); + } + }); + + client.on("disconnect", function (reason) { + // Unexpected disconnect as well as explicit disconnect request can be received here + logger.info("Client", client.id, "disconnected."); + logger.info("Reason :", reason); + }); + }); + + // Listen on provided port, on all network interfaces. + return new Promise((resolve) => + server.listen(sslport, () => resolve(server)), + ); +} + +if (require.main === module) { + // When this file executed as a script, not loaded as module - run the connector + startCDLSocketIOConnector() + .then((server) => { + const addr = server.address(); + + if (!addr) { + logger.error("Could not get running server address - exit."); + process.exit(1); + } + + const bind = + typeof addr === "string" ? "pipe " + addr : "port " + addr.port; + logger.debug("Listening on " + bind); + }) + .catch((err) => { + logger.error("Could not start cdl-socketio connector:", err); + }); +} diff --git a/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/common/core/config.ts b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/common/core/config.ts new file mode 100644 index 00000000000..ffada9f71c4 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/common/core/config.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * config.js + */ + +// TODO - Common + +export const DEFAULT_NODE_CONFIG_DIR = "/etc/cactus/connector-cdl-socketio/"; +if (!process.env["NODE_CONFIG_DIR"]) { + // Must be set before import config + process.env["NODE_CONFIG_DIR"] = DEFAULT_NODE_CONFIG_DIR; +} + +import config from "config"; + +/** + * Get configuration entry (uses node-config setup) + * + * @param key : Key to retrieve + * @param defaultValue : Value to return if key is not present in the config. + * @returns : Configuration value + */ +export function read(key: string, defaultValue?: T): T { + if (config.has(key)) { + return config.get(key); + } + + if (typeof defaultValue !== "undefined") { + return defaultValue; + } + + throw Error( + `Missing configuration entry '${key}', config dir = ${process.env["NODE_CONFIG_DIR"]}`, + ); +} diff --git a/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/connector/ServerPlugin.ts b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/connector/ServerPlugin.ts new file mode 100644 index 00000000000..2bd613c1a35 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/connector/ServerPlugin.ts @@ -0,0 +1,228 @@ +import { read as configRead } from "../common/core/config"; +import { cdlRequest } from "./cdl-request"; +import sanitizeHtml from "sanitize-html"; +import safeStringify from "fast-safe-stringify"; + +// Log settings +import { getLogger } from "log4js"; +const logger = getLogger("ServerPlugin[" + process.pid + "]"); +logger.level = configRead("logLevel", "info"); + +type SupportedFunctions = + | "registerHistoryData" + | "getLineage" + | "searchByHeader" + | "searchByGlobalData" + | "status"; + +type FunctionArgsType = { + method: { + type: SupportedFunctions; + accessToken?: string; + trustAgentId?: string; + }; + args: any; + reqID?: string; +}; + +/* + * ServerPlugin + * Class definition for server plugins + */ +export class ServerPlugin { + /** + * Dispatch function that runs method with name from `args.method.type` with `args` arguments. + */ + async executeFunction(args: FunctionArgsType): Promise { + switch (args.method.type) { + case "registerHistoryData": + return this.registerHistoryData(args); + case "getLineage": + return this.getLineage(args); + case "searchByHeader": + return this.searchByHeader(args); + case "searchByGlobalData": + return this.searchByGlobalData(args); + case "status": + return this.status(args); + default: + const _unknownMethod: never = args.method.type; + throw new Error(`Unknown CDL ServerPlugin method: ${_unknownMethod}`); + } + } + + /** + * Throws if any property in an object starts with `cdl:` (not allowed by the API) + * @param properties object with string fields. + */ + private checkPropertyNames(properties?: Record) { + const invalidProps = Object.keys(properties ?? {}).filter((k) => + k.startsWith("cdl:"), + ); + if (invalidProps.length > 0) { + throw new Error( + `Properties can't start with 'cdl:'. Invalid properties provided: ${invalidProps}`, + ); + } + } + + /** + * Send request to `trail_registration` CDL endpoint. + */ + async registerHistoryData(args: FunctionArgsType): Promise { + logger.debug( + "ServerPlugin:registerHistoryData() args:", + JSON.stringify(args), + ); + + // Check args + const typedArgs = args.args as { + eventId?: string; + lineageId?: string; + tags?: Record; + properties?: Record; + }; + this.checkPropertyNames(typedArgs.tags); + this.checkPropertyNames(typedArgs.properties); + + const responseData = await cdlRequest( + `trail_registration`, + getAccessTokenOrThrow(args), + {}, + { + "cdl:EventId": typedArgs.eventId ?? "", + "cdl:LineageId": typedArgs.lineageId ?? "", + "cdl:Tags": typedArgs.tags, + ...typedArgs.properties, + }, + ); + + if (responseData.result !== "OK") { + throw new Error(sanitizeHtml(safeStringify(responseData))); + } + + logger.debug("registerHistoryData results:", responseData); + return responseData; + } + + /** + * Get data from `trail_acquisition` CDL endpoint. + */ + async getLineage(args: FunctionArgsType): Promise { + logger.debug("ServerPlugin:getLineage() args:", JSON.stringify(args)); + + // Check args + const typedArgs = args.args as { + eventId: string; + direction?: "backward" | "forward" | "both"; + depth?: string; + }; + + if (!typedArgs.eventId) { + throw new Error("Missing eventId in getLineage args!"); + } + const direction = (typedArgs.direction ?? "backward").toUpperCase(); + let depth = parseInt(typedArgs.depth ?? "-1", 10); + if (isNaN(depth)) { + logger.warn( + "Could not parse depth from the argument, using default (-1). Wrong input:", + typedArgs.depth, + ); + depth = -1; + } + + const responseData = await cdlRequest( + `trail_acquisition/${sanitizeHtml(typedArgs.eventId)}`, + getAccessTokenOrThrow(args), + { + direction, + depth, + }, + ); + + if (responseData.result !== "OK") { + throw new Error(sanitizeHtml(safeStringify(responseData))); + } + logger.debug("getLineage results:", responseData); + + return responseData; + } + + /** + * Search data using `trail_search_headers` CDL endpoint. + */ + async searchByHeader(args: FunctionArgsType): Promise { + logger.debug("ServerPlugin:searchByHeader() args:", JSON.stringify(args)); + return await searchRequest("trail_search_headers", args); + } + + /** + * Search data using `trail_search_globaldata` CDL endpoint. + */ + async searchByGlobalData(args: FunctionArgsType): Promise { + logger.debug( + "ServerPlugin:searchByGlobalData() args:", + JSON.stringify(args), + ); + return await searchRequest("trail_search_globaldata", args); + } + + /** + * Simple method to get current status of the connector plugin. + */ + async status(args: FunctionArgsType): Promise { + logger.debug("ServerPlugin:status() args:", JSON.stringify(args)); + return { + status: "OK.", + }; + } +} + +/** + * Common logic for sending trail search requests + */ +async function searchRequest( + searchType: "trail_search_headers" | "trail_search_globaldata", + args: FunctionArgsType, +) { + // Check args + const typedArgs = args.args as { + searchType: "exactmatch" | "partialmatch" | "regexpmatch"; + fields: Record; + }; + + if (!typedArgs.searchType || !typedArgs.fields) { + throw new Error("Missing required searchByHeader args!"); + } + + const responseData = await cdlRequest( + searchType, + getAccessTokenOrThrow(args), + {}, + { + searchType: typedArgs.searchType, + body: typedArgs.fields, + }, + ); + + if (responseData.result !== "OK") { + throw new Error(sanitizeHtml(safeStringify(responseData))); + } + + return responseData; +} + +function getAccessTokenOrThrow(args: FunctionArgsType): [string, string] { + const accessToken = args?.method?.accessToken; + const trustAgentId = args?.method?.trustAgentId; + + if (!accessToken) { + throw new Error("Missing CDL accessToken"); + } + + if (!trustAgentId) { + throw new Error("Missing CDL trustAgentId"); + } + + return [accessToken, trustAgentId]; +} diff --git a/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/connector/ValidatorAuthentication.ts b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/connector/ValidatorAuthentication.ts new file mode 100644 index 00000000000..86179338f88 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/connector/ValidatorAuthentication.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Hyperledger Cacti Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * ValidatorAuthentication.ts + */ + +import * as config from "../common/core/config"; + +import fs from "fs"; +import path from "path"; +import jwt from "jsonwebtoken"; + +import { getLogger } from "log4js"; +const logger = getLogger("ValidatorAuthentication[" + process.pid + "]"); +logger.level = config.read("logLevel", "info"); + +const privateKey = fs.readFileSync( + path.resolve(__dirname, config.read("sslParam.key")), +); + +export class ValidatorAuthentication { + static sign(payload: object): string { + const signature: string = jwt.sign(payload, privateKey, { + algorithm: "ES256", + expiresIn: "10000", + }); + logger.debug(`signature: OK`); + return signature; + } +} diff --git a/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/connector/cdl-request.ts b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/connector/cdl-request.ts new file mode 100644 index 00000000000..95b4dd04b5a --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/connector/cdl-request.ts @@ -0,0 +1,82 @@ +import { read as configRead } from "../common/core/config"; +import axios from "axios"; +import https from "https"; + +// Log settings +import { getLogger } from "log4js"; +import { readFileSync } from "fs"; +const logger = getLogger("cdl-request[" + process.pid + "]"); +logger.level = configRead("logLevel", "info"); + +function getHttpsAgent() { + const agentOptions: https.AgentOptions = {}; + + const skipCertCheck = configRead( + "cdlApiGateway.skipCertCheck", + false, + ); + if (skipCertCheck) { + logger.info( + `Allowing self signed CDL API GW certificates (skipCertCheck=${skipCertCheck})`, + ); + agentOptions.rejectUnauthorized = false; + } + + const caPath = configRead("cdlApiGateway.caPath", ""); + if (caPath) { + logger.info(`Using CDL API GW CA ${caPath}`); + const gatewayCAString = readFileSync(caPath, "ascii"); + logger.debug("CDL Gateway certificate read:", gatewayCAString); + agentOptions.ca = gatewayCAString; + } + + const serverName = configRead("cdlApiGateway.serverName", ""); + if (serverName) { + logger.info(`Overwrite CDL API GW server name with '${serverName}'`); + agentOptions.servername = serverName; + } + + return new https.Agent(agentOptions); +} + +const COMMON_HTTPS_AGENT = getHttpsAgent(); + +export async function cdlRequest( + url: string, + accessToken: [string, string], + queryParams?: any, + dataPayload?: any, +) { + let httpMethod = "get"; + if (dataPayload) { + httpMethod = "post"; + } + + logger.debug(`cdlRequest ${httpMethod} ${url} executed`); + + try { + const requestResponse = await axios({ + httpsAgent: COMMON_HTTPS_AGENT, + method: httpMethod, + baseURL: configRead("cdlApiGateway.url"), + url: `api/v1/${url}`, + responseType: "json", + headers: { + "User-Agent": configRead("userAgent", "CactiCDLConnector"), + Authorization: `Bearer ${accessToken[0]}`, + "Trust-Agent-Id": accessToken[1], + "Content-Type": "application/json;charset=UTF-8", + }, + params: queryParams, + data: dataPayload, + }); + + return requestResponse.data; + } catch (error: any) { + if ("toJSON" in error) { + logger.error("CDL API request failed:", error.toJSON()); + } + + throw error; + } +} diff --git a/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/index.ts b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/index.ts new file mode 100644 index 00000000000..87cb558397c --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/index.ts @@ -0,0 +1 @@ +export * from "./public-api"; diff --git a/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/public-api.ts b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/public-api.ts new file mode 100644 index 00000000000..136baee1640 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/main/typescript/public-api.ts @@ -0,0 +1 @@ +export { startCDLSocketIOConnector } from "./common/core/bin/www"; diff --git a/packages/cactus-plugin-ledger-connector-cdl-socketio/src/test/typescript/integration/cdl-connector-manual.test.ts b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/test/typescript/integration/cdl-connector-manual.test.ts new file mode 100644 index 00000000000..621de2b133e --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-cdl-socketio/src/test/typescript/integration/cdl-connector-manual.test.ts @@ -0,0 +1,419 @@ +/** + * Manual tests for CDL connector. + * Must be exectued on Azure environment with access to CDL service. + * Check out CDL connector readme for instructions on how to run these tests. + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +const ACCESS_TOKEN = "_____FILL_TOKEN_HERE_____" +const TOKEN_AGENT_ID = "_____FILL_AGENT_ID_HERE_____" +const VALIDATOR_KEY_PATH = "_____FILL_KEY_PATH_HERE_____" + +const testLogLevel: LogLevelDesc = "info"; +const sutLogLevel: LogLevelDesc = "info"; +const setupTimeout = 1000 * 60; // 1 minute timeout for setup + +// ApiClient settings +const syncReqTimeout = 1000 * 10; // 10 seconds + +import { + LogLevelDesc, + LoggerProvider, + Logger, +} from "@hyperledger/cactus-common"; +import { SocketIOApiClient } from "@hyperledger/cactus-api-client"; + +import "jest-extended"; +import { Server as HttpsServer } from "https"; +import { v4 as uuidv4 } from "uuid"; + +import * as cdlConnector from "../../../main/typescript/index"; + +// Logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "cdl-connector-manual.test", + level: testLogLevel, +}); + +describe("CDL Connector manual tests", () => { + let connectorServer: HttpsServer; + let apiClient: SocketIOApiClient; + + ////////////////////////////////// + // Helper Methods + ////////////////////////////////// + + async function registerHistoryDataOnCDL(args: any = {}) { + const response = await apiClient.sendSyncRequest( + {}, + { + type: "registerHistoryData", + accessToken: ACCESS_TOKEN, + trustAgentId: TOKEN_AGENT_ID, + }, + args, + ); + + log.debug("registerHistoryData response:", JSON.stringify(response)); + + expect(response.status).toEqual(200); + expect(response.data.result).toEqual("OK"); + const event = response.data.detail; + expect(event).toBeTruthy(); + expect(event["cdl:Lineage"]).toBeTruthy(); + expect(event["cdl:Lineage"]["cdl:EventId"]).toBeTruthy(); + expect(event["cdl:Lineage"]["cdl:LineageId"]).toBeTruthy(); + expect(event["cdl:Tags"]).toBeTruthy(); + expect(event["cdl:Verification"]).toBeTruthy(); + return event; + } + + async function getLineageFromCDL(args: any = {}): Promise { + const response = await apiClient.sendSyncRequest( + {}, + { + type: "getLineage", + accessToken: ACCESS_TOKEN, + trustAgentId: TOKEN_AGENT_ID, + }, + args, + ); + + log.debug("getLineage response:", JSON.stringify(response)); + + expect(response.status).toEqual(200); + expect(response.data.result).toEqual("OK"); + const eventList = response.data.detail; + expect(eventList).toBeTruthy(); + expect(eventList.length).toBeGreaterThan(0); + return eventList; + } + + async function searchByHeaderOnCDL(args: any = {}): Promise { + const response = await apiClient.sendSyncRequest( + {}, + { + type: "searchByHeader", + accessToken: ACCESS_TOKEN, + trustAgentId: TOKEN_AGENT_ID, + }, + args, + ); + + log.debug("searchByHeaderOnCDL response:", JSON.stringify(response)); + + expect(response.status).toEqual(200); + expect(response.data.result).toEqual("OK"); + const eventList = response.data.detail; + expect(eventList).toBeTruthy(); + return eventList; + } + + async function searchByGlobalDataOnCDL(args: any = {}): Promise { + const response = await apiClient.sendSyncRequest( + {}, + { + type: "searchByGlobalData", + accessToken: ACCESS_TOKEN, + trustAgentId: TOKEN_AGENT_ID, + }, + args, + ); + + log.debug("searchByGlobalData response:", JSON.stringify(response)); + + expect(response.status).toEqual(200); + expect(response.data.result).toEqual("OK"); + const eventList = response.data.detail; + expect(eventList).toBeTruthy(); + return eventList; + } + + ////////////////////////////////// + // Environment Setup + ////////////////////////////////// + + beforeAll(async () => { + // Run the connector + connectorServer = await cdlConnector.startCDLSocketIOConnector(); + expect(connectorServer).toBeTruthy(); + const connectorAddress = connectorServer.address(); + if (!connectorAddress || typeof connectorAddress === "string") { + throw new Error("Unexpected CDL connector AddressInfo type"); + } + log.info( + "CDL-SocketIO Connector started on:", + `${connectorAddress.address}:${connectorAddress.port}`, + ); + + // Create ApiClient instance + const apiConfigOptions = { + validatorID: "cdl-connector-manual.test", + validatorURL: `https://localhost:${connectorAddress.port}`, + validatorKeyPath: VALIDATOR_KEY_PATH, + logLevel: sutLogLevel, + maxCounterRequestID: 1000, + syncFunctionTimeoutMillisecond: syncReqTimeout, + socketOptions: { + rejectUnauthorized: false, + reconnection: false, + timeout: syncReqTimeout * 2, + }, + }; + log.debug("ApiClient config:", apiConfigOptions); + apiClient = new SocketIOApiClient(apiConfigOptions); + }, setupTimeout); + + afterAll(async () => { + log.info("FINISHING THE TESTS"); + + if (apiClient) { + log.info("Close ApiClient connection..."); + apiClient.close(); + } + + if (connectorServer) { + log.info("Stop the CDL connector..."); + await new Promise((resolve) => + connectorServer.close(() => resolve()), + ); + } + + // SocketIOApiClient has timeout running for each request which is not cancellable at the moment. + // Wait timeout amount of seconds to make sure all handles are closed. + await new Promise((resolve) => setTimeout(resolve, syncReqTimeout)); + }, setupTimeout); + + ////////////////////////////////// + // Tests + ////////////////////////////////// + + /** + * Test if connector was started correctly and communication is possible + */ + test("Get connector status", async () => { + const argsParam = { + args: ["test1", "test2"], + }; + + const response = await apiClient.sendSyncRequest( + {}, + { + type: "status", + }, + argsParam, + ); + + log.debug("Status response:", response); + expect(response.status).toEqual(200); + expect(response.data.status).toEqual("OK."); + }); + + test("Register single history data", async () => { + const newEvent = await registerHistoryDataOnCDL({ + eventId: "", + lineageId: "", + tags: { + test: "abc", + }, + properties: { + prop1: "abc", + prop2: "cba", + }, + }); + + // Check custom properties and tags + expect(newEvent["cdl:Event"]["prop1"]).toEqual("abc"); + expect(newEvent["cdl:Event"]["prop2"]).toEqual("cba"); + expect(newEvent["cdl:Tags"]["test"]).toEqual("abc"); + }); + + test("Register history data in signle lineage", async () => { + const firstEvent = await registerHistoryDataOnCDL(); + const firstEventId = firstEvent["cdl:Lineage"]["cdl:EventId"]; + + const secondEvent = await registerHistoryDataOnCDL({ + lineageId: firstEventId, + }); + + // Check if two events belong to same lineage + expect(secondEvent["cdl:Lineage"]["cdl:LineageId"]).toEqual(firstEventId); + }); + + /** + * Tests for getLineage endpoint + */ + describe("Get lineage tests", () => { + const eventsInLineage: string[] = []; + + beforeAll(async () => { + const firstEvent = await registerHistoryDataOnCDL(); + const firstEventId = firstEvent["cdl:Lineage"]["cdl:EventId"]; + log.info("First eventId (lineageId):", firstEventId); + eventsInLineage.push(firstEventId); + + for (let i = 0; i < 2; i++) { + const event = await registerHistoryDataOnCDL({ + lineageId: firstEventId, + }); + eventsInLineage.push(event["cdl:Lineage"]["cdl:EventId"]); + } + + log.info("Events in test lineage:", eventsInLineage); + }); + + // both middle + test("Get lineage forward all (default) on the first event (lineageId)", async () => { + const eventList = await getLineageFromCDL({ + eventId: eventsInLineage[0], + direction: "forward", + depth: "-1", + }); + + // Forward from first should return all events in lineage + expect(eventList.length).toEqual(eventsInLineage.length); + }); + + test("Get lineage forward all (default) on the last event", async () => { + const eventList = await getLineageFromCDL({ + eventId: eventsInLineage[eventsInLineage.length - 1], + direction: "forward", + depth: "-1", + }); + + // Forward from last should return only one event + expect(eventList.length).toEqual(1); + }); + + test("Get lineage backward all on the last event", async () => { + const eventList = await getLineageFromCDL({ + eventId: eventsInLineage[eventsInLineage.length - 1], + direction: "backward", + depth: "-1", + }); + + // Backward from last should return all events in lineage + expect(eventList.length).toEqual(eventsInLineage.length); + }); + + test("Get lineage both all on the middle event", async () => { + const eventList = await getLineageFromCDL({ + eventId: eventsInLineage[1], + direction: "both", + depth: "-1", + }); + + // Both on middle event should return all events in lineage + expect(eventList.length).toEqual(eventsInLineage.length); + }); + }); + + /** + * Tests for searchByHeader and searchByGlobalData endpoints + */ + describe("Search data tests", () => { + const privateTagValue = uuidv4(); + const customEventPropValue = uuidv4(); + let searchedEvent: any; + let searchedEventTimestamp: string; + + beforeAll(async () => { + searchedEvent = await registerHistoryDataOnCDL({ + tags: { + privateTag: privateTagValue, + }, + properties: { + customEventProp: customEventPropValue, + }, + }); + log.info("Event to search for:", searchedEvent); + + searchedEventTimestamp = + searchedEvent["cdl:Lineage"]["cdl:DataRegistrationTimeStamp"]; + }); + + test("Search header data using exact match", async () => { + log.info( + "Search for events with exact timestamp:", + searchedEventTimestamp, + ); + const events = await searchByHeaderOnCDL({ + searchType: "exactmatch", + fields: { + "cdl:DataRegistrationTimeStamp": searchedEventTimestamp, + }, + }); + + expect(events).toBeTruthy(); + expect(events.length).toBeGreaterThan(0); + for (const e of events) { + expect(e["cdl:Lineage"]["cdl:DataRegistrationTimeStamp"]).toEqual( + searchedEventTimestamp, + ); + } + }); + + test("Search header data using partial match", async () => { + const datePart = searchedEventTimestamp.split("T")[0]; + log.info("Search for events with partialmatch (date):", datePart); + + const events = await searchByHeaderOnCDL({ + searchType: "partialmatch", + fields: { + "cdl:DataRegistrationTimeStamp": datePart, + }, + }); + + expect(events).toBeTruthy(); + expect(events.length).toBeGreaterThan(0); + for (const e of events) { + expect(e["cdl:Lineage"]["cdl:DataRegistrationTimeStamp"]).toMatch( + datePart, + ); + } + }); + + test("Search header data using regex match", async () => { + const datePart = searchedEventTimestamp.split("T")[0]; + const dateRegex = `^${datePart}.*$`; + log.info("Search for events with regexpmatch:", dateRegex); + + const events = await searchByHeaderOnCDL({ + searchType: "regexpmatch", + fields: { + "cdl:DataRegistrationTimeStamp": dateRegex, + }, + }); + + expect(events).toBeTruthy(); + expect(events.length).toBeGreaterThan(0); + for (const e of events) { + expect(e["cdl:Lineage"]["cdl:DataRegistrationTimeStamp"]).toMatch( + datePart, + ); + } + }); + + test("Search global data using exact match", async () => { + log.info( + "Search for events with exact customEventProp:", + customEventPropValue, + ); + const events = await searchByGlobalDataOnCDL({ + searchType: "exactmatch", + fields: { + customEventProp: customEventPropValue, + }, + }); + + expect(events).toBeTruthy(); + expect(events.length).toEqual(1); + for (const e of events) { + expect(e["cdl:Event"]["customEventProp"]).toEqual(customEventPropValue); + } + }); + }); +}); diff --git a/packages/cactus-plugin-ledger-connector-cdl-socketio/tsconfig.json b/packages/cactus-plugin-ledger-connector-cdl-socketio/tsconfig.json new file mode 100644 index 00000000000..8781e839810 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-cdl-socketio/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src/main/typescript", + "composite": true, + "outDir": "./dist", + "sourceMap": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "tsBuildInfoFile": "../../.build-cache/cactus-plugin-ledger-connector-cdl-socketio.tsbuildinfo" + }, + "include": [ + "./src/main/typescript/common/core/*.ts", + "./src/main/typescript/common/core/bin/*.ts", + "./src/main/typescript/common/core/config/*.ts", + "./src/main/typescript/connector/*.ts", + "./src/main/typescript/*.ts" + ], + "references": [ + { + "path": "../cactus-common/tsconfig.json" + }, + { + "path": "../cactus-api-client/tsconfig.json" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json index e50f5124de9..014ea2c6bf7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -64,6 +64,9 @@ { "path": "./packages/cactus-plugin-ledger-connector-go-ethereum-socketio/tsconfig.json" }, + { + "path": "./packages/cactus-plugin-ledger-connector-cdl-socketio/tsconfig.json" + }, { "path": "./packages/cactus-plugin-ledger-connector-iroha/tsconfig.json" }, @@ -160,7 +163,7 @@ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - "declaration": true, /* Generates corresponding '.d.ts' file. */ + "declaration": true /* Generates corresponding '.d.ts' file. */, // "declarationDir": "dist/lib", // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ @@ -194,10 +197,7 @@ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - "typeRoots": [ - "./node_modules/@types", - "./typings" - ], + "typeRoots": ["./node_modules/@types", "./typings"], "esModuleInterop": true, "inlineSourceMap": true, "forceConsistentCasingInFileNames": true diff --git a/yarn.lock b/yarn.lock index 30ee3dd3227..9038189e7d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8134,7 +8134,7 @@ abstract-leveldown@~6.2.1, abstract-leveldown@~6.2.3: level-supports "~1.0.0" xtend "~4.0.0" -accepts@^1.3.5, accepts@~1.3.8: +accepts@^1.3.5, accepts@~1.3.3, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -9207,6 +9207,14 @@ axios@0.24.0: dependencies: follow-redirects "^1.14.4" +axios@0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + axios@1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.4.tgz#6555dd955d2efa9b8f4cb4cb0b3371b7b243537a" @@ -12871,7 +12879,7 @@ depd@2.0.0, depd@~2.0.0: resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -depd@^1.1.2, depd@~1.1.0, depd@~1.1.2: +depd@^1.1.2, depd@~1.1.0, depd@~1.1.1, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= @@ -13416,7 +13424,7 @@ enabled@2.0.x: resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== -encodeurl@^1.0.2, encodeurl@~1.0.2: +encodeurl@^1.0.2, encodeurl@~1.0.1, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= @@ -14519,7 +14527,7 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -etag@~1.8.1: +etag@~1.8.0, etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= @@ -14995,6 +15003,40 @@ express-unless@^2.1.3: resolved "https://registry.yarnpkg.com/express-unless/-/express-unless-2.1.3.tgz#f951c6cca52a24da3de32d42cfd4db57bc0f9a2e" integrity sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ== +express@4.15.5: + version "4.15.5" + resolved "https://registry.yarnpkg.com/express/-/express-4.15.5.tgz#670235ca9598890a5ae8170b83db722b842ed927" + integrity sha512-E5kemI7MqPbkLstU1xLG3VB7TgSRyJu4N765mQinL9agWiD5HzpK5IZTUYuqd8m0M8A8ReM23xBqcwiSzrDRGQ== + dependencies: + accepts "~1.3.3" + array-flatten "1.1.1" + content-disposition "0.5.2" + content-type "~1.0.2" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.1" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.0" + finalhandler "~1.0.6" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.1" + path-to-regexp "0.1.7" + proxy-addr "~1.1.5" + qs "6.5.0" + range-parser "~1.2.0" + send "0.15.6" + serve-static "1.12.6" + setprototypeof "1.0.3" + statuses "~1.3.1" + type-is "~1.6.15" + utils-merge "1.0.0" + vary "~1.1.1" + express@4.16.4: version "4.16.4" resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" @@ -15720,6 +15762,19 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" +finalhandler@~1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.6.tgz#007aea33d1a4d3e42017f624848ad58d212f814f" + integrity sha512-immlyyYCPWG2tajlYBhZ6cjLAv1QAclU8tKS0d27ZtPqm/+iddy16GT3xLExg+V4lIETLpPwaYQAlZHNE//dPA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.1" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.3.1" + unpipe "~1.0.0" + find-cache-dir@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9" @@ -15850,7 +15905,7 @@ follow-redirects@^1.0.0, follow-redirects@^1.12.1, follow-redirects@^1.14.0, fol resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== -follow-redirects@^1.10.0, follow-redirects@^1.15.0: +follow-redirects@^1.10.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -15956,6 +16011,11 @@ forwarded@0.2.0: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== +forwarded@~0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha512-Ua9xNhH0b8pwE3yRbFfXJvfdWF0UHNCdeyb2sbi9Ul/M+r3PTdrz7Cv4SCfZRMjmzEM9PhraqfZFbGTIg3OMyA== + fp-ts@1.19.3: version "1.19.3" resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-1.19.3.tgz#261a60d1088fbff01f91256f91d21d0caaaaa96f" @@ -18097,6 +18157,11 @@ ip@^1.1.0, ip@^1.1.5: resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= +ipaddr.js@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0" + integrity sha512-RbrsPoo4IkisyHhS9VDa3ybxnu0wOo0uTAhaELmwxq244p18X7Dk0fQoJvh/QTkIUO296fbjgvMqK3ry84eVVA== + ipaddr.js@1.9.1, ipaddr.js@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -21260,6 +21325,7 @@ level-supports@^4.0.0: integrity sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA== level-supports@~1.0.0, level-transcoder@^1.0.1: + name level-transcoder version "1.0.1" resolved "https://registry.yarnpkg.com/level-transcoder/-/level-transcoder-1.0.1.tgz#f8cef5990c4f1283d4c86d949e73631b0bc8ba9c" integrity sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w== @@ -22414,6 +22480,11 @@ mime-types@^2.1.12, mime-types@^2.1.16, mime-types@^2.1.27, mime-types@^2.1.31, dependencies: mime-db "1.52.0" +mime@1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + integrity sha512-sAaYXszED5ALBt665F0wMQCUXpGuZsGdopoqcHPdL39ZYdi7uHoZlhrfZfhv8WzivhBzr/oXwaj+yiK5wY8MXQ== + mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" @@ -24765,7 +24836,7 @@ parseuri@0.0.6: resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a" integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow== -parseurl@^1.3.3, parseurl@~1.3.2, parseurl@~1.3.3: +parseurl@^1.3.3, parseurl@~1.3.1, parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -26247,6 +26318,14 @@ protractor@7.0.0: webdriver-manager "^12.1.7" yargs "^15.3.1" +proxy-addr@~1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918" + integrity sha512-av1MQ5vwTiMICwU75KSf/vJ6a+AXP0MtP+aYBqm2RFlire7BP6sWlfOLc8+6wIQrywycqSpJWm5zNkYFkRARWA== + dependencies: + forwarded "~0.1.0" + ipaddr.js "1.4.0" + proxy-addr@~2.0.4, proxy-addr@~2.0.5, proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -26371,6 +26450,11 @@ qs@6.4.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" integrity sha1-E+JtKK1rD/qpExLNO/cI7TUecjM= +qs@6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49" + integrity sha512-fjVFjW9yhqMhVGwRExCXLhJKrLlkYSaxNWdyc9rmHlrVZbk35YHH312dFd7191uQeXkI3mKLZTIbSvIeFwFemg== + qs@6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -27901,6 +27985,25 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semve resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +send@0.15.6: + version "0.15.6" + resolved "https://registry.yarnpkg.com/send/-/send-0.15.6.tgz#20f23a9c925b762ab82705fe2f9db252ace47e34" + integrity sha512-e1/758VJ+GsPg8vE+Z/xE7R36IWogUl8rrrs53CsfHrT2IyZyPggfvbHT8HTV3yBNKrUHYUTsBQ9pXQYkcB4YQ== + dependencies: + debug "2.6.9" + depd "~1.1.1" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.3.4" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.1" + send@0.16.2: version "0.16.2" resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" @@ -28030,6 +28133,16 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" +serve-static@1.12.6: + version "1.12.6" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.6.tgz#b973773f63449934da54e5beba5e31d9f4211577" + integrity sha512-M2CMkmVnR22x7taVSeYGdfIhnh/mhanPZqqBCSRAvVZNkrhaOpOLlwimKpJR+NhFpD/8Dr9G0YpAtkDkCcAVJQ== + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.2" + send "0.15.6" + serve-static@1.13.2: version "1.13.2" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" @@ -28106,6 +28219,11 @@ setimmediate@^1.0.4, setimmediate@^1.0.5: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + integrity sha512-9jphSf3UbIgpOX/RKvX02iw/rN2TKdusnsPpGfO/rkcsrd+IRqgHZb4VGnmL0Cynps8Nj2hN45wsi30BzrHDIw== + setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -29021,6 +29139,11 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +statuses@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + integrity sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg== + statuses@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" @@ -31198,6 +31321,11 @@ util@^0.12.5: is-typed-array "^1.1.3" which-typed-array "^1.1.2" +utils-merge@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" + integrity sha512-HwU9SLQEtyo+0uoKXd1nkLqigUWLB+QuNQR4OcmB73eWqksM5ovuqcycks2x043W8XVb75rG1HQ0h93TMXkzQQ== + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -31313,7 +31441,7 @@ varint@^6.0.0: resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0" integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg== -vary@^1, vary@~1.1.2: +vary@^1, vary@~1.1.1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= @@ -33498,7 +33626,7 @@ yargs-parser@20.2.4: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== -yargs-parser@^13.1.0: +yargs-parser@^13.1.0, yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==