| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | // Imports
 | 
					
						
							| 
									
										
										
										
											2017-06-27 19:43:02 +03:00
										 |  |  | import * as bodyParser from 'body-parser'; | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | import * as express from 'express'; | 
					
						
							|  |  |  | import * as http from 'http'; | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  | import {GithubPullRequests} from '../common/github-pull-requests'; | 
					
						
							| 
									
										
										
										
											2017-03-02 00:04:03 +02:00
										 |  |  | import {assertNotMissingOrEmpty} from '../common/utils'; | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | import {BuildCreator} from './build-creator'; | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  | import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events'; | 
					
						
							|  |  |  | import {BUILD_VERIFICATION_STATUS, BuildVerifier} from './build-verifier'; | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | import {UploadError} from './upload-error'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Constants
 | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  | const AUTHORIZATION_HEADER = 'AUTHORIZATION'; | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | const X_FILE_HEADER = 'X-FILE'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  | // Interfaces - Types
 | 
					
						
							|  |  |  | interface UploadServerConfig { | 
					
						
							|  |  |  |   buildsDir: string; | 
					
						
							| 
									
										
										
										
											2017-03-02 00:04:03 +02:00
										 |  |  |   domainName: string; | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  |   githubOrganization: string; | 
					
						
							|  |  |  |   githubTeamSlugs: string[]; | 
					
						
							|  |  |  |   githubToken: string; | 
					
						
							|  |  |  |   repoSlug: string; | 
					
						
							|  |  |  |   secret: string; | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |   trustedPrLabel: string; | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | // Classes
 | 
					
						
							|  |  |  | class UploadServerFactory { | 
					
						
							|  |  |  |   // Methods - Public
 | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  |   public create({ | 
					
						
							|  |  |  |     buildsDir, | 
					
						
							| 
									
										
										
										
											2017-03-02 00:04:03 +02:00
										 |  |  |     domainName, | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  |     githubOrganization, | 
					
						
							|  |  |  |     githubTeamSlugs, | 
					
						
							|  |  |  |     githubToken, | 
					
						
							|  |  |  |     repoSlug, | 
					
						
							|  |  |  |     secret, | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |     trustedPrLabel, | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  |   }: UploadServerConfig): http.Server { | 
					
						
							| 
									
										
										
										
											2017-03-02 00:04:03 +02:00
										 |  |  |     assertNotMissingOrEmpty('domainName', domainName); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |     const buildVerifier = new BuildVerifier(secret, githubToken, repoSlug, githubOrganization, githubTeamSlugs, | 
					
						
							|  |  |  |                                             trustedPrLabel); | 
					
						
							| 
									
										
										
										
											2017-03-02 00:04:03 +02:00
										 |  |  |     const buildCreator = this.createBuildCreator(buildsDir, githubToken, repoSlug, domainName); | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  |     const middleware = this.createMiddleware(buildVerifier, buildCreator); | 
					
						
							| 
									
										
										
										
											2017-06-17 21:03:10 +03:00
										 |  |  |     const httpServer = http.createServer(middleware as any); | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02: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-02 00:04:03 +02:00
										 |  |  |   protected createBuildCreator(buildsDir: string, githubToken: string, repoSlug: string, | 
					
						
							|  |  |  |                                domainName: string): BuildCreator { | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  |     const buildCreator = new BuildCreator(buildsDir); | 
					
						
							|  |  |  |     const githubPullRequests = new GithubPullRequests(githubToken, repoSlug); | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03: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 21:10:46 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |       return githubPullRequests.addComment(pr, body); | 
					
						
							|  |  |  |     }; | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03: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 21:10:46 +02:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return buildCreator; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express { | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  |     const middleware = express(); | 
					
						
							| 
									
										
										
										
											2017-06-27 19:43:02 +03:00
										 |  |  |     const jsonParser = bodyParser.json(); | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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 21:10:46 +02:00
										 |  |  |       const authHeader = req.header(AUTHORIZATION_HEADER); | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  |       if (!authHeader) { | 
					
						
							|  |  |  |         this.throwRequestError(401, `Missing or empty '${AUTHORIZATION_HEADER}' header`, req); | 
					
						
							|  |  |  |       } else if (!archive) { | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  |         this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req); | 
					
						
							| 
									
										
										
										
											2017-06-17 21:03:10 +03:00
										 |  |  |       } else { | 
					
						
							| 
									
										
										
										
											2017-06-27 19:43:02 +03:00
										 |  |  |         Promise.resolve(). | 
					
						
							|  |  |  |           then(() => buildVerifier.verify(+pr, authHeader)). | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03: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 21:03:10 +03:00
										 |  |  |           catch(err => this.respondWithError(res, err)); | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200)); | 
					
						
							| 
									
										
										
										
											2017-06-27 19:43:02 +03:00
										 |  |  |     middleware.post(/^\/pr-updated\/?$/, jsonParser, (req, res) => { | 
					
						
							|  |  |  |       const {action, number: prNo}: {action?: string, number?: number} = req.body; | 
					
						
							|  |  |  |       const visMayHaveChanged = !action || (action === 'labeled') || (action === 'unlabeled'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (!visMayHaveChanged) { | 
					
						
							|  |  |  |         res.sendStatus(200); | 
					
						
							|  |  |  |       } else if (!prNo) { | 
					
						
							|  |  |  |         this.throwRequestError(400, `Missing or empty 'number' field`, req); | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         Promise.resolve(). | 
					
						
							|  |  |  |           then(() => buildVerifier.getPrIsTrusted(prNo)). | 
					
						
							|  |  |  |           then(isPublic => buildCreator.updatePrVisibility(String(prNo), isPublic)). | 
					
						
							|  |  |  |           then(() => res.sendStatus(200)). | 
					
						
							|  |  |  |           catch(err => this.respondWithError(res, err)); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2017-06-27 20:14:41 +03:00
										 |  |  |     middleware.all('*', req => this.throwRequestError(404, 'Unknown resource', req)); | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02: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) { | 
					
						
							| 
									
										
										
										
											2017-06-27 19:43:02 +03:00
										 |  |  |     const message = `${error} in request: ${req.method} ${req.originalUrl}` + | 
					
						
							|  |  |  |                     (!req.body ? '' : ` ${JSON.stringify(req.body)}`); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     throw new UploadError(status, message); | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Exports
 | 
					
						
							|  |  |  | export const uploadServerFactory = new UploadServerFactory(); |