Skip to content

Commit

Permalink
WIP - web service plugins #2
Browse files Browse the repository at this point in the history
Signed-off-by: Peter Somogyvari <[email protected]>
  • Loading branch information
petermetz committed May 14, 2020
1 parent 0e5f3d9 commit 530389b
Show file tree
Hide file tree
Showing 17 changed files with 266 additions and 137 deletions.
224 changes: 112 additions & 112 deletions packages/bif-cmd-api-server/src/main/typescript/api-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,23 @@ import { OpenApiValidator } from 'express-openapi-validator';
import compression from 'compression';
import bodyParser from 'body-parser';
import cors, { CorsOptions } from 'cors';
import { IPluginKVStorage, PluginFactory } from '@hyperledger-labs/bif-core-api';
import { IPluginKVStorage, PluginFactory, ICactusPlugin, PluginAspect } from '@hyperledger-labs/bif-core-api';
import { CreateConsortiumEndpointV1 } from './consortium/routes/create-consortium-endpoint-v1';
import { IBifApiServerOptions, ConfigService } from './config/config-service';
import { BIF_OPEN_API_JSON } from './openapi-spec';
import { Logger, LoggerProvider } from '@hyperledger-labs/bif-common';
import { Servers } from './common/servers';

export interface IApiServerConstructorOptions {
plugins: ICactusPlugin[];
config: Config<IBifApiServerOptions>;
}

export class ApiServer {

private readonly log: Logger;
private httpServerApi: Server | null = null;
private httpServerFile: Server | null = null;
private httpServerCockpit: Server | null = null;

constructor(public readonly options: IApiServerConstructorOptions) {
if (!options) {
Expand All @@ -43,141 +45,139 @@ export class ApiServer {
}
}

public shutdown(): Promise<any> {

const apiServerShutdown = new Promise<void>((resolve, reject) => {
if (this.httpServerApi) {
this.httpServerApi.close((err: any) => {
if (err) {
reject(err);
} else {
resolve();
}
});
}
});
public getHttpServerApi(): Server | null {
return this.httpServerApi;
}

const fileServerShutdown = new Promise<void>((resolve, reject) => {
if (this.httpServerFile) {
this.httpServerFile.close((err: any) => {
if (err) {
reject(err);
} else {
resolve();
}
});
}
});
public getHttpServerCockpit(): Server | null {
return this.httpServerCockpit;
}

return Promise.all([apiServerShutdown, fileServerShutdown]);
public async shutdown(): Promise<void> {

if (this.httpServerApi) {
this.log.info(`Closing HTTP server of the API...`);
await Servers.shutdown(this.httpServerApi);
this.log.info(`Close HTTP server of the API OK`);
}

if (this.httpServerCockpit) {
this.log.info(`Closing HTTP server of the cockpit ...`);
await Servers.shutdown(this.httpServerCockpit);
this.log.info(`Close HTTP server of the cockpit OK`);
}
}

async startCockpitFileServer(): Promise<void> {
const cockpitWwwRoot = this.options.config.get('cockpitWwwRoot');
this.log.info(`wwwRoot: ${cockpitWwwRoot}`);
async startCockpitFileServer(): Promise < void> {
const cockpitWwwRoot = this.options.config.get('cockpitWwwRoot');
this.log.info(`wwwRoot: ${cockpitWwwRoot}`);

const resolvedWwwRoot = path.resolve(process.cwd(), cockpitWwwRoot);
this.log.info(`resolvedWwwRoot: ${resolvedWwwRoot}`);
const resolvedWwwRoot = path.resolve(process.cwd(), cockpitWwwRoot);
this.log.info(`resolvedWwwRoot: ${resolvedWwwRoot}`);

const resolvedIndexHtml = path.resolve(resolvedWwwRoot + '/index.html');
this.log.info(`resolvedIndexHtml: ${resolvedIndexHtml}`);
const resolvedIndexHtml = path.resolve(resolvedWwwRoot + '/index.html');
this.log.info(`resolvedIndexHtml: ${resolvedIndexHtml}`);

const app: Express = express();
app.use(compression());
app.use(express.static(resolvedWwwRoot));
app.get('/*', (_, res) => res.sendFile(resolvedIndexHtml));
const app: Express = express();
app.use(compression());
app.use(express.static(resolvedWwwRoot));
app.get('/*', (_, res) => res.sendFile(resolvedIndexHtml));

const cockpitPort: number = this.options.config.get('cockpitPort');
const cockpitHost: string = this.options.config.get('cockpitHost');
const cockpitPort: number = this.options.config.get('cockpitPort');
const cockpitHost: string = this.options.config.get('cockpitHost');

await new Promise<any>((resolve, reject) => {
this.httpServerApi = app.listen(cockpitPort, cockpitHost, () => {
// tslint:disable-next-line: no-console
console.log(`BIF Cockpit UI reachable on port ${cockpitPort}`);
resolve({ cockpitPort });
});
this.httpServerApi.on('error', (err: any) => reject(err));
await new Promise<any>((resolve, reject) => {
this.httpServerCockpit = app.listen(cockpitPort, cockpitHost, () => {
this.log.info(`Cactus Cockpit UI reachable on port http://${cockpitHost}:${cockpitPort}`);
resolve({ cockpitPort });
});
}
this.httpServerCockpit.on('error', (err: any) => reject(err));
});
}

async startApiServer(): Promise<void> {
const app: Application = express();
app.use(compression());
async startApiServer(): Promise < void> {
const app: Application = express();
app.use(compression());

const corsMiddleware = this.createCorsMiddleware()
const corsMiddleware = this.createCorsMiddleware()
app.use(corsMiddleware);

app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.json({ limit: '50mb' }));

const openApiValidator = this.createOpenApiValidator();
await openApiValidator.install(app);
const openApiValidator = this.createOpenApiValidator();
await openApiValidator.install(app);

app.get('/healthcheck', (req: Request, res: Response, next: NextFunction) => {
res.json({ 'success': true, timestamp: new Date() });
});
app.get('/healthcheck', (req: Request, res: Response, next: NextFunction) => {
res.json({ 'success': true, timestamp: new Date() });
});

const storage: IPluginKVStorage = await this.createStoragePlugin();
const configService = new ConfigService();
const config = configService.getOrCreate();
const storage: IPluginKVStorage = await this.createStoragePlugin();
const configService = new ConfigService();
const config = configService.getOrCreate();
{
const endpoint = new CreateConsortiumEndpointV1({ storage, config });
app.post(endpoint.getPath(), endpoint.handleRequest.bind(endpoint));
}
const endpoint = new CreateConsortiumEndpointV1({ storage, config });
app.post(endpoint.getPath(), endpoint.handleRequest.bind(endpoint));
}

// FIXME
// app.get('/api/v1/consortium/:consortiumId', (req: Request, res: Response, next: NextFunction) => {
// res.json({ swagger: 'TODO' });
// });

const apiPort: number = this.options.config.get('apiPort');
const apiHost: string = this.options.config.get('apiHost');
this.log.info(`Binding Cactus API to port ${apiPort}...`);
await new Promise<any>((resolve, reject) => {
const httpServer = app.listen(apiPort, apiHost, () => {
const address: any = httpServer.address();
this.log.info(`Successfully bound API to port ${apiPort}`, { address });
if (address && address.port) {
resolve({ port: address.port });
} else {
resolve({ port: apiPort });
}
});
httpServer.on('error', (err) => reject(err));
});
// FIXME
// app.get('/api/v1/consortium/:consortiumId', (req: Request, res: Response, next: NextFunction) => {
// res.json({ swagger: 'TODO' });
// });

const apiPort: number = this.options.config.get('apiPort');
const apiHost: string = this.options.config.get('apiHost');
this.log.info(`Binding Cactus API to port ${apiPort}...`);
await new Promise<any>((resolve, reject) => {
const httpServerApi = app.listen(apiPort, apiHost, () => {
const address: any = httpServerApi.address();
this.log.info(`Successfully bound API to port ${apiPort}`, { address });
if (address && address.port) {
resolve({ port: address.port });
} else {
resolve({ port: apiPort });
}
});
this.httpServerApi = httpServerApi;
this.httpServerApi.on('error', (err) => reject(err));
});
}

createOpenApiValidator(): OpenApiValidator {
return new OpenApiValidator({
apiSpec: BIF_OPEN_API_JSON,
validateRequests: true,
validateResponses: false
});
}
createOpenApiValidator(): OpenApiValidator {
return new OpenApiValidator({
apiSpec: BIF_OPEN_API_JSON,
validateRequests: true,
validateResponses: false
});
}

async createStoragePlugin(): Promise<IPluginKVStorage> {
const storagePluginPackage = this.options.config.get('storagePluginPackage');
const { PluginFactoryKVStorage } = await import(storagePluginPackage);
const storagePluginOptionsJson = this.options.config.get('storagePluginOptionsJson');
const storagePluginOptions = JSON.parse(storagePluginOptionsJson);
const pluginFactory: PluginFactory<IPluginKVStorage, unknown> = new PluginFactoryKVStorage();
const plugin = await pluginFactory.create(storagePluginOptions);
return plugin;
async createStoragePlugin(): Promise < IPluginKVStorage > {
const kvStoragePlugin = this.options.plugins.find((p) => p.getAspect() === PluginAspect.KV_STORAGE);
if(kvStoragePlugin) {
return kvStoragePlugin as IPluginKVStorage;
}
const storagePluginPackage = this.options.config.get('storagePluginPackage');
const { PluginFactoryKVStorage } = await import(storagePluginPackage);
const storagePluginOptionsJson = this.options.config.get('storagePluginOptionsJson');
const storagePluginOptions = JSON.parse(storagePluginOptionsJson);
const pluginFactory: PluginFactory<IPluginKVStorage, unknown> = new PluginFactoryKVStorage();
const plugin = await pluginFactory.create(storagePluginOptions);
return plugin;
}

createCorsMiddleware(): RequestHandler {
const apiCorsDomainCsv = this.options.config.get('apiCorsDomainCsv');
const allowedDomains = apiCorsDomainCsv.split(',');
const allDomainsAllowed = allowedDomains.includes('*');

const corsOptions: CorsOptions = {
origin: (origin: string | undefined, callback) => {
if (allDomainsAllowed || origin && allowedDomains.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error(`CORS not allowed for Origin "${origin}".`));
}
createCorsMiddleware(): RequestHandler {
const apiCorsDomainCsv = this.options.config.get('apiCorsDomainCsv');
const allowedDomains = apiCorsDomainCsv.split(',');
const allDomainsAllowed = allowedDomains.includes('*');

const corsOptions: CorsOptions = {
origin: (origin: string | undefined, callback) => {
if (allDomainsAllowed || origin && allowedDomains.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error(`CORS not allowed for Origin "${origin}".`));
}
}
return cors(corsOptions);
}
return cors(corsOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const main = async () => {
} else {
const configService = new ConfigService();
const config = configService.getOrCreate();
const apiServer = new ApiServer({ config });
const apiServer = new ApiServer({ config, plugins: [] });
await apiServer.start();
}
};
Expand Down
29 changes: 29 additions & 0 deletions packages/bif-cmd-api-server/src/main/typescript/common/servers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Server } from 'http';
import { Server as SecureServer } from 'https';

/**
* Utility class for handling common tasks for NodeJS HTTP/S server objects.
*/
export class Servers {

/**
* Returns with a promise that resolves when the server has been shut down. Rejects if anything goes wrong of if the
* parameters are invalid.
*
* @param server The server object that will be shut down.
*/
public static async shutdown(server: Server | SecureServer): Promise<void> {
if (!server) {
throw new TypeError(`Servers#shutdown() server was falsy. Need object.`);
}
return new Promise<void>((resolve, reject) => {
server.close((err: any) => {
if (err) {
reject(new Error(`Servers#shutdown() Failed to shut down server: ${err.stack}`));
} else {
resolve();
}
});
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,9 @@ export class ConfigService {
}, {});
}

public newExampleConfigConvict(): Config<IBifApiServerOptions> {
const env = this.newExampleConfigEnv();
public newExampleConfigConvict(bifApiServerOptions?: IBifApiServerOptions): Config<IBifApiServerOptions> {
bifApiServerOptions = bifApiServerOptions || this.newExampleConfig();
const env = this.newExampleConfigEnv(bifApiServerOptions);
return this.getOrCreate({ env });
}

Expand Down
4 changes: 2 additions & 2 deletions packages/bif-cmd-api-server/src/main/typescript/public-api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { ApiServer } from './api-server';
export { ConfigService } from './config/config-service';
export { ApiServer, IApiServerConstructorOptions } from './api-server';
export { ConfigService, IBifApiServerOptions } from './config/config-service';
export { CreateConsortiumEndpointV1, ICreateConsortiumEndpointOptions } from './consortium/routes/create-consortium-endpoint-v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { PluginAspect } from "./plugin-aspect";

/**
* This is the common base for all other plugin interface definitions to have as a parent.
*
*/
export interface ICactusPlugin {

/**
* Returns the ID of the plugin which is a string uniquely identifying the plugin among other plugins so that they can
* be managed separately without conflicts or runtime errors.
* Important: This is not just uniqely identifying the plugin aspect, but the implementation as well.
* For example a plugin aspect would we `ledger-connector` or `storage` and implementations are the ones within those
* aspects such as `plugin-ledger-connector-besu` or `plugin-storage-kv-in-memory`.
*/
getId(): string;

/**
* Returns the aspect of which this plugin implementation belongs to such as the aspect of `ledger-connector` or
* `storage` for example.
* There can be any number of plugin implementations for each aspect.
*/
getAspect(): PluginAspect;
}

export function isICactusPlugin(pluginInstance: ICactusPlugin): pluginInstance is ICactusPlugin {
return typeof pluginInstance.getId === 'function';
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ICactusPlugin } from "../i-cactus-plugin";

/**
* Common interface to be implemented by plugins which are implementing the connection to ledgers.
*/
export interface IPluginLedgerConnector<T,K> {
export interface IPluginLedgerConnector<T, K> extends ICactusPlugin {

/**
* Deploys the BIF build-in smart contract written for this ledger to manage the validator's public keys.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const enum PluginAspect {
KEYCHAIN = 'KEYCHAIN',
LEDGER_CONNECTOR = 'LEDGER_CONNECTOR',
KV_STORAGE = 'KV_STORAGE',
WEB_SERVICE = 'WEB_SERVICE',
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export interface IPluginKVStorage {
import { ICactusPlugin } from "../../i-cactus-plugin";

export interface IPluginKVStorage extends ICactusPlugin {
has(key: string): Promise<boolean>;
get<T>(key: string): Promise<T>;
set<T>(key: string, value: T): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IWebServiceEndpoint } from "./i-web-service-endpoint";
import { ICactusPlugin } from "../i-cactus-plugin";

export interface IPluginWebService {
export interface IPluginWebService extends ICactusPlugin {
installWebService(expressApp: any): IWebServiceEndpoint[];
}

Expand Down
Loading

0 comments on commit 530389b

Please sign in to comment.