UNPKG

@atomist/automation-client

Version:

Atomist API for software low-level client

357 lines (316 loc) • 13.8 kB
import * as GitHubApi from "@octokit/rest"; import * as bodyParser from "body-parser"; import * as express from "express"; import * as passport from "passport"; import * as http from "passport-http"; import * as bearer from "passport-http-bearer"; import * as tokenHeader from "passport-http-header-token"; import { Configuration, ExpressCustomizer, } from "../../../configuration"; import * as globals from "../../../globals"; import { AutomationContextAware } from "../../../HandlerContext"; import { noEventHandlersWereFound } from "../../../server/AbstractAutomationServer"; import { AutomationServer } from "../../../server/AutomationServer"; import { GraphClient } from "../../../spi/graph/GraphClient"; import { MessageClient } from "../../../spi/message/MessageClient"; import { logger } from "../../../util/logger"; import { scanFreePort } from "../../../util/port"; import { health, HealthStatus, } from "../../util/health"; import { info } from "../../util/info"; import { gc, heapDump, mtrace, } from "../../util/memory"; import { metrics } from "../../util/metric"; import { guid } from "../../util/string"; import { RequestProcessor } from "../RequestProcessor"; import { prepareRegistration } from "../websocket/payloads"; /** * Registers an endpoint for every automation and exposes * metadataFromInstance at root. Responsible for marshalling into the appropriate structure */ export class ExpressServer { private readonly exp: express.Express; constructor(private readonly automations: AutomationServer, private readonly configuration: Configuration, private readonly handler: RequestProcessor) { this.exp = express(); this.exp.use(bodyParser.json(this.configuration.http.bodyParser?.options)); this.exp.use(require("helmet")()); this.exp.use(passport.initialize()); // Enable cors for all endpoints const cors = require("cors"); this.setupAuthentication(); // Set up routes this.exp.options(`${ApiBase}/health`, cors()); this.exp.get(`${ApiBase}/health`, cors(), (req, res) => { const h = health(); if (h.status !== HealthStatus.Up) { logger.warn(`Health status: ${JSON.stringify(h)}`); } res.status(h.status === HealthStatus.Up ? 200 : 500).json(h); }); this.exp.options(`${ApiBase}/info`, cors()); this.exp.get(`${ApiBase}/info`, cors(), this.adminRoute, this.authenticate, (req, res) => { res.json(info(automations.automations)); }); this.exp.options(`${ApiBase}/registration`, cors()); this.exp.get(`${ApiBase}/registration`, cors(), this.adminRoute, this.authenticate, (req, res) => { res.json(prepareRegistration(automations.automations)); }); this.exp.options(`${ApiBase}/metrics`, cors()); this.exp.get(`${ApiBase}/metrics`, cors(), this.adminRoute, this.authenticate, (req, res) => { res.json(metrics()); }); this.exp.options(`${ApiBase}/memory/gc`, cors()); this.exp.put(`${ApiBase}/memory/gc`, cors(), this.adminRoute, this.authenticate, (req, res) => { gc(); res.sendStatus(201); }); this.exp.options(`${ApiBase}/memory/heapdump`, cors()); this.exp.put(`${ApiBase}/memory/heapdump`, cors(), this.adminRoute, this.authenticate, (req, res) => { heapDump(); res.sendStatus(201); }); this.exp.options(`${ApiBase}/memory/mtrace`, cors()); this.exp.put(`${ApiBase}/memory/mtrace`, cors(), this.adminRoute, this.authenticate, (req, res) => { mtrace(); res.sendStatus(201); }); this.exp.options(`${ApiBase}/log/events`, cors()); this.exp.get(`${ApiBase}/log/events`, cors(), this.adminRoute, this.authenticate, (req, res) => { res.json(globals.eventStore().events(req.query.from)); }); this.exp.options(`${ApiBase}/log/commands`, cors()); this.exp.get(`${ApiBase}/log/commands`, cors(), this.adminRoute, this.authenticate, (req, res) => { res.json(globals.eventStore().commands(req.query.from)); }); this.exp.options(`${ApiBase}/log/messages`, cors()); this.exp.get(`${ApiBase}/log/messages`, cors(), this.adminRoute, this.authenticate, (req, res) => { res.json(globals.eventStore().messages(req.query.from)); }); this.exp.options(`${ApiBase}/series/events`, cors()); this.exp.get(`${ApiBase}/series/events`, cors(), this.adminRoute, this.authenticate, (req, res) => { res.json(globals.eventStore().eventSeries()); }); this.exp.options(`${ApiBase}/series/commands`, cors()); this.exp.get(`${ApiBase}/series/commands`, cors(), this.adminRoute, this.authenticate, (req, res) => { res.json(globals.eventStore().commandSeries()); }); this.exposeCommandHandlerInvocationRoute(this.exp, `${ApiBase}/command`, cors, (req, res, result) => { if (result.redirect && !req.get("x-atomist-no-redirect")) { res.redirect(result.redirect); } else { res.status(result.code === 0 ? 200 : 500).json(result); } }); this.exposeEventHandlerInvocationRoute(this.exp, `${ApiBase}/event`, cors, (req, res, result) => { const results = Array.isArray(result) ? result : [result]; const code = noEventHandlersWereFound(result) ? 404 : results.some(r => r.code !== 0) ? 500 : 200; res.status(code).json(result); }); if (this.configuration.http.customizers.length > 0) { logger.debug("Invoking http server customizers"); this.configuration.http.customizers.forEach(c => c(this.exp, this.authenticate)); } } public run(): Promise<boolean> { let portPromise; if (!this.configuration.http.port) { portPromise = scanFreePort(); } else { portPromise = Promise.resolve(this.configuration.http.port); } return portPromise .then(port => { this.configuration.http.port = port; const hostname = this.configuration.http.host || "0.0.0.0"; this.exp.listen(port, hostname, () => { logger.debug( `Atomist automation client api running at 'http://${hostname}:${port}'`); return true; }).on("error", err => { logger.error(`Failed to start automation client api: ${err.message}`); return false; }); }); } private exposeCommandHandlerInvocationRoute(exp: express.Express, url: string, cors, handle: (req, res, result) => any) { exp.post(url, cors(), this.authenticate, (req, res) => { this.handler.processCommand(req.body, result => { result.then(r => handle(req, res, r)); }); }); } private exposeEventHandlerInvocationRoute(exp: express.Express, url: string, cors, handle: (req, res, result) => any) { exp.post(url, cors(), this.authenticate, (req, res) => { this.handler.processEvent(req.body, result => { result.then(r => handle(req, res, r)); }); }); } private setupAuthentication() { if (this.configuration.http.auth && this.configuration.http.auth.basic && this.configuration.http.auth.basic.enabled) { const user: string = this.configuration.http.auth.basic.username ? this.configuration.http.auth.basic.username : "admin"; const pwd: string = this.configuration.http.auth.basic.password ? this.configuration.http.auth.basic.password : guid(); passport.use("basic", new http.BasicStrategy( (username, password, done) => { if (user === username && pwd === password) { done(null, { user: username }); } else { done(null, false); } }, )); if (!this.configuration.http.auth.basic.password) { logger.debug(`Auto-generated credentials for web endpoints are user '${user}' and password '${pwd}'`); } } if (this.configuration.http.auth && this.configuration.http.auth.bearer && this.configuration.http.auth.bearer.enabled) { const org = this.configuration.http.auth.bearer.org; const adminOrg = this.configuration.http.auth.bearer.adminOrg; passport.use("bearer", new bearer.Strategy({ passReqToCallback: true, } as bearer.IStrategyOptions, (req, token, done) => { const api = new GitHubApi(); api.authenticate({ type: "token", token }); api.users.getAuthenticated({}) .then(user => { if (adminOrg && req.__admin === true) { return api.orgs.checkMembership({ username: user.data.login, org: adminOrg, }) .then(() => { return user.data; }); } else if (org) { return api.orgs.checkMembership({ username: user.data.login, org, }) .then(() => { return user.data; }); } else { return user.data; } }) .then(user => { return done(null, { token, user }); }) .catch(err => { console.log(err); return done(null, false); }); }, )); } if (this.configuration.http.auth && this.configuration.http.auth.token && this.configuration.http.auth.token.enabled) { const cb = this.configuration.http.auth.token.verify || (token => Promise.resolve(false)); passport.use("token", new tokenHeader.Strategy( (token, done) => { cb(token) .then(valid => { if (valid) { return done(null, { user: token }); } else { return done(null, false); } }) .catch(err => { console.log(err); return done(null, false); }); }, )); } } private readonly adminRoute = (req, res, next) => { req.__admin = true; next(); } private readonly authenticate = (req, res, next) => { if (this.configuration.http.auth) { const strategies = []; if (this.configuration.http.auth.bearer && this.configuration.http.auth.bearer.enabled === true) { strategies.push("bearer"); } if (this.configuration.http.auth.basic && this.configuration.http.auth.basic.enabled === true) { strategies.push("basic"); } if (this.configuration.http.auth.token && this.configuration.http.auth.token.enabled === true) { strategies.push("token"); } if (strategies.length > 0) { passport.authenticate(strategies, { session: false })(req, res, next); } else { next(); } } else { next(); } } } const ApiBase = ""; export interface ExpressServerOptions { port: number; host?: string; customizers?: ExpressCustomizer[]; bodyParser?: { options?: bodyParser.Options }, auth?: { basic?: { enabled?: boolean; username?: string; password?: string; }, bearer?: { enabled?: boolean; org?: string; adminOrg?: string; }, token?: { enabled?: boolean; verify?: (token: string) => Promise<boolean>; }, }; endpoint: { graphql: string; }; messageClientFactory?: (aca: AutomationContextAware) => MessageClient; graphClientFactory?: (aca: AutomationContextAware) => GraphClient; }