import * as http from 'http'; import * as url from 'url'; import * as open from 'opn'; import * as logger from 'winston'; import * as path from 'path'; import * as fs from 'fs-extra-promise'; import * as lodash from 'lodash'; import { CoError } from '../co-error'; import * as Promise from 'bluebird'; const SUCCESS_FILENAME = 'index.html'; const errorMessages = { title: 'The error occurred during your attempt to login to Cobalt CLI', message: 'Please, check the entered parameters and try again', info: 'You can close this window now.' } const RESOURCES_DIRECTORY = path.join(__dirname, '../../../resources/browser-callback-server'); const ignoredPaths = ['robots.txt', 'sitemap.xml']; export default class BrowserCallbackServer { public createCallbackUrl(port: number): string { return 'http://localhost:' + port; } public start(requestUrl: string, redirectPort: number, messages: any, onRequest: (url: url.Url) => void, onError: (error: Error) => void): void { const server: http.Server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => { const parsedUrl = url.parse('' + req.url, true); const pathName = parsedUrl.pathname || ''; if(this.isPageLoaded(parsedUrl)) { res.end(); server.close(); } else { try { onRequest(parsedUrl); return this.respondForServer(server, req, res, 200, pathName, messages); } catch (error) { onError(error); return this.respondForServer(server, req, res, 400, pathName, errorMessages); } } }); server.listen(redirectPort, () => { open(requestUrl, { wait: false }) }); server.on('connection', (socket) => { socket.unref(); }); } private isPageLoaded(parsedUrl: url.Url): boolean { return parsedUrl.pathname === '/page-loaded'; } private respondForServer(server: http.Server, req: http.IncomingMessage, res: http.ServerResponse, statusCode: number, filename: string, messages: any) { return this.respondWithFile(req, res, 200, filename, messages) .catch((error: Error) => { logger.error(error.message); }) } private respondWithFile(req: http.IncomingMessage, res: http.ServerResponse, statusCode: number, filename: string, messages: any): Promise { const fileFullPath = this.getFullPath(filename); const sendBody = Promise.promisify(res.end).bind(res); if (this.isIgnoredPath(filename)) { return sendBody() } return this.isDirectory(fileFullPath).then(isDirectory => { const filePath = isDirectory ? this.getFullPath(SUCCESS_FILENAME) : fileFullPath; return fs.readFileAsync(filePath) .then((response) => { let content = response; if (this.isHtmlFile(filePath)) { content = this.setMessages(content.toString(), messages); } res.writeHead(statusCode, { 'Content-Length': content.length }); return sendBody(content); }) .catch((error) => { throw new CoError("BROWSER_CALLBACK_FILE", filename, error.message); }); }) } private isIgnoredPath(pathName: string): boolean { return ignoredPaths.some(path => pathName.indexOf(path) > 0) } private getFullPath(inputPath: string): string { return path.join(RESOURCES_DIRECTORY, inputPath); } private isHtmlFile(filePath: string): boolean { return path.extname(filePath) === '.html'; } private setMessages(content: string, messages: any): Buffer { const resultContent = lodash.template(content)(messages); return Buffer.alloc(Buffer.byteLength(resultContent), resultContent); } private isDirectory(inputPath: string = ''): Promise { return fs.statAsync(inputPath).then((stat: fs.Stats) => stat.isDirectory()); } }