2017-02-06 13:40:28 -05:00
|
|
|
// Imports
|
|
|
|
import * as express from 'express';
|
|
|
|
import * as http from 'http';
|
2017-02-28 14:10:46 -05:00
|
|
|
import {GithubPullRequests} from '../common/github-pull-requests';
|
2017-03-01 17:04:03 -05:00
|
|
|
import {assertNotMissingOrEmpty} from '../common/utils';
|
2017-02-06 13:40:28 -05:00
|
|
|
import {BuildCreator} from './build-creator';
|
2017-06-18 18:15:07 -04:00
|
|
|
import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
|
|
|
|
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from './build-verifier';
|
2017-02-06 13:40:28 -05:00
|
|
|
import {UploadError} from './upload-error';
|
|
|
|
|
|
|
|
// Constants
|
2017-02-28 14:10:46 -05:00
|
|
|
const AUTHORIZATION_HEADER = 'AUTHORIZATION';
|
2017-02-06 13:40:28 -05:00
|
|
|
const X_FILE_HEADER = 'X-FILE';
|
|
|
|
|
2017-02-28 14:10:46 -05:00
|
|
|
// Interfaces - Types
|
|
|
|
interface UploadServerConfig {
|
|
|
|
buildsDir: string;
|
2017-03-01 17:04:03 -05:00
|
|
|
domainName: string;
|
2017-02-28 14:10:46 -05:00
|
|
|
githubOrganization: string;
|
|
|
|
githubTeamSlugs: string[];
|
|
|
|
githubToken: string;
|
|
|
|
repoSlug: string;
|
|
|
|
secret: string;
|
2017-06-18 18:15:07 -04:00
|
|
|
trustedPrLabel: string;
|
2017-02-28 14:10:46 -05:00
|
|
|
}
|
|
|
|
|
2017-02-06 13:40:28 -05:00
|
|
|
// Classes
|
|
|
|
class UploadServerFactory {
|
|
|
|
// Methods - Public
|
2017-02-28 14:10:46 -05:00
|
|
|
public create({
|
|
|
|
buildsDir,
|
2017-03-01 17:04:03 -05:00
|
|
|
domainName,
|
2017-02-28 14:10:46 -05:00
|
|
|
githubOrganization,
|
|
|
|
githubTeamSlugs,
|
|
|
|
githubToken,
|
|
|
|
repoSlug,
|
|
|
|
secret,
|
2017-06-18 18:15:07 -04:00
|
|
|
trustedPrLabel,
|
2017-02-28 14:10:46 -05:00
|
|
|
}: UploadServerConfig): http.Server {
|
2017-03-01 17:04:03 -05:00
|
|
|
assertNotMissingOrEmpty('domainName', domainName);
|
|
|
|
|
2017-06-18 18:15:07 -04:00
|
|
|
const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, githubOrganization, githubTeamSlugs,
|
|
|
|
trustedPrLabel);
|
2017-03-01 17:04:03 -05:00
|
|
|
const buildCreator = this.createBuildCreator(buildsDir, githubToken, repoSlug, domainName);
|
2017-02-06 13:40:28 -05:00
|
|
|
|
2017-02-28 14:10:46 -05:00
|
|
|
const middleware = this.createMiddleware(buildVerifier, buildCreator);
|
2017-06-17 14:03:10 -04:00
|
|
|
const httpServer = http.createServer(middleware as any);
|
2017-02-06 13:40:28 -05:00
|
|
|
|
|
|
|
httpServer.on('listening', () => {
|
|
|
|
const info = httpServer.address();
|
|
|
|
console.info(`Up and running (and listening on ${info.address}:${info.port})...`);
|
|
|
|
});
|
|
|
|
|
|
|
|
return httpServer;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Methods - Protected
|
2017-03-01 17:04:03 -05:00
|
|
|
protected createBuildCreator(buildsDir: string, githubToken: string, repoSlug: string,
|
|
|
|
domainName: string): BuildCreator {
|
2017-02-28 14:10:46 -05:00
|
|
|
const buildCreator = new BuildCreator(buildsDir);
|
|
|
|
const githubPullRequests = new GithubPullRequests(githubToken, repoSlug);
|
2017-06-18 18:15:07 -04:00
|
|
|
const postPreviewsComment = (pr: number, shas: string[]) => {
|
|
|
|
const body = shas.
|
|
|
|
map(sha => `You can preview ${sha} at https://pr${pr}-${sha}.${domainName}/.`).
|
|
|
|
join('\n');
|
2017-02-28 14:10:46 -05:00
|
|
|
|
2017-06-18 18:15:07 -04:00
|
|
|
return githubPullRequests.addComment(pr, body);
|
|
|
|
};
|
2017-02-28 14:10:46 -05:00
|
|
|
|
2017-06-18 18:15:07 -04:00
|
|
|
buildCreator.on(CreatedBuildEvent.type, ({pr, sha, isPublic}: CreatedBuildEvent) => {
|
|
|
|
if (isPublic) {
|
|
|
|
postPreviewsComment(pr, [sha]);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
buildCreator.on(ChangedPrVisibilityEvent.type, ({pr, shas, isPublic}: ChangedPrVisibilityEvent) => {
|
|
|
|
if (isPublic && shas.length) {
|
|
|
|
postPreviewsComment(pr, shas);
|
|
|
|
}
|
2017-02-28 14:10:46 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
return buildCreator;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express {
|
2017-02-06 13:40:28 -05:00
|
|
|
const middleware = express();
|
|
|
|
|
|
|
|
middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => {
|
|
|
|
const pr = req.params[0];
|
|
|
|
const sha = req.params[1];
|
|
|
|
const archive = req.header(X_FILE_HEADER);
|
2017-02-28 14:10:46 -05:00
|
|
|
const authHeader = req.header(AUTHORIZATION_HEADER);
|
2017-02-06 13:40:28 -05:00
|
|
|
|
2017-02-28 14:10:46 -05:00
|
|
|
if (!authHeader) {
|
|
|
|
this.throwRequestError(401, `Missing or empty '${AUTHORIZATION_HEADER}' header`, req);
|
|
|
|
} else if (!archive) {
|
2017-02-06 13:40:28 -05:00
|
|
|
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
|
2017-06-17 14:03:10 -04:00
|
|
|
} else {
|
|
|
|
buildVerifier.
|
|
|
|
verify(+pr, authHeader).
|
2017-06-18 18:15:07 -04:00
|
|
|
then(verStatus => verStatus === BUILD_VERIFICATION_STATUS.verifiedAndTrusted).
|
|
|
|
then(isPublic => buildCreator.create(pr, sha, archive, isPublic).
|
|
|
|
then(() => res.sendStatus(isPublic ? 201 : 202))).
|
2017-06-17 14:03:10 -04:00
|
|
|
catch(err => this.respondWithError(res, err));
|
2017-02-06 13:40:28 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
|
2017-06-27 13:14:41 -04:00
|
|
|
middleware.all('*', req => this.throwRequestError(404, 'Unknown resource', req));
|
2017-02-06 13:40:28 -05:00
|
|
|
middleware.use((err: any, _req: any, res: express.Response, _next: any) => this.respondWithError(res, err));
|
|
|
|
|
|
|
|
return middleware;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected respondWithError(res: express.Response, err: any) {
|
|
|
|
if (!(err instanceof UploadError)) {
|
|
|
|
err = new UploadError(500, String((err && err.message) || err));
|
|
|
|
}
|
|
|
|
|
|
|
|
const statusText = http.STATUS_CODES[err.status] || '???';
|
|
|
|
console.error(`Upload error: ${err.status} - ${statusText}`);
|
|
|
|
console.error(err.message);
|
|
|
|
|
|
|
|
res.status(err.status).end(err.message);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected throwRequestError(status: number, error: string, req: express.Request) {
|
|
|
|
throw new UploadError(status, `${error} in request: ${req.method} ${req.originalUrl}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Exports
|
|
|
|
export const uploadServerFactory = new UploadServerFactory();
|