UNPKG

scf-service-stack

Version:

A set for developing a cloud function

343 lines (324 loc) 9.27 kB
const cookie = require('cookie'); const fs = require('fs'); const ejs = require('ejs'); const path = require('path'); const jwt = require('jsonwebtoken'); const SECRET = process.env.ENCRYPTSECRET || 'scf-stack'; const TYPES = { '.aac': 'audio/aac', '.abw': 'application/x-abiword', '.arc': 'application/x-freearc', '.avi': 'video/x-msvideo', '.azw': 'application/vnd".amazon".ebook', '.bin': 'application/octet-stream', '.bmp': 'image/bmp', '.bz': 'application/x-bzip', '.bz2': 'application/x-bzip2', '.csh': 'application/x-csh', '.css': 'text/css', '.csv': 'text/csv', '.doc': 'application/msword', '.docx': 'application/vnd".openxmlformats-officedocument".wordprocessingml".document', '.eot': 'application/vnd".ms-fontobject', '.epub': 'application/epub+zip', '.gz': 'application/gzip', '.gif': 'image/gif', '.htm': 'text/html', '.html': 'text/html', '.ico': 'image/vnd".microsoft".icon', '.ics': 'text/calendar', '.jar': 'application/java-archive', '.jpeg': 'image/jpeg', '.jpg': 'image/jpeg', '.js': 'text/javascript', '.json': 'application/json', '.jsonld': 'application/ld+json', '.mid': 'audio/midi audio/x-midi', '.midi': 'audio/midi audio/x-midi', '.mjs': 'text/javascript', '.mp3': 'audio/mpeg', '.mpeg': 'video/mpeg', '.mpkg': 'application/vnd".apple".installer+xml', '.odp': 'application/vnd".oasis".opendocument".presentation', '.ods': 'application/vnd".oasis".opendocument".spreadsheet', '.odt': 'application/vnd".oasis".opendocument".text', '.oga': 'audio/ogg', '.ogv': 'video/ogg', '.ogx': 'application/ogg', '.opus': 'audio/opus', '.otf': 'font/otf', '.png': 'image/png', '.pdf': 'application/pdf', '.php': 'application/x-httpd-php', '.ppt': 'application/vnd".ms-powerpoint', '.pptx': 'application/vnd".openxmlformats-officedocument".presentationml".presentation', '.rar': 'application/vnd".rar', '.rtf': 'application/rtf', '.sh': 'application/x-sh', '.svg': 'image/svg+xml', '.swf': 'application/x-shockwave-flash', '.tar': 'application/x-tar', '.tif': 'image/tiff', '.tiff': 'image/tiff', '.ts': 'video/mp2t', '.ttf': 'font/ttf', '.txt': 'text/plain', '.vsd': 'application/vnd".visio', '.wav': 'audio/wav', '.weba': 'audio/webm', '.webm': 'video/webm', '.webp': 'image/webp', '.woff': 'font/woff', '.woff2': 'font/woff2', '.xhtml': 'application/xhtml+xml', '.xls': 'application/vnd".ms-excel', '.xlsx': 'application/vnd".openxmlformats-officedocument".spreadsheetml".sheet', '.xml': 'application/xml', '.xul': 'application/vnd".mozilla".xul+xml', '.zip': 'application/zip', '.3gp': 'video/3gpp', '.3g2': 'video/3gpp2', '.7z': 'application/x-7z-compressed' }; const resourceNotFound = (path) => { const response = { statusCode: 404, headers: { 'content-type': 'application/html' }, body: '<h1>404 Resource Not Found</h1>' + `<label>${path || ''}</label>` }; return response; }; class Response { constructor(req, options) { this.req = req; this._res = {}; this.headers = {}; this.options = options; this.eventsOnFinish = []; } onFinish(callback) { this.eventsOnFinish.push(callback); } finally(callback) { this.finalEvent = callback; } _setStatus(code) { this.statusCode = code; this._res.statusCode = code; } status(code) { this._setStatus(code); return this; } end(value) { this._end = true; const onFinishEventResults = []; for (let i = 0; i < this.eventsOnFinish.length; i++) { onFinishEventResults.push(this.eventsOnFinish[i](this)); } Promise.all(onFinishEventResults).then(() => { if (typeof value === 'object' && value.statusCode) { value.headers = Object.assign(value.headers, this.headers); } else { value = { statusCode: this.statusCode || 200, headers: { 'content-type': 'application/json', ...this.headers }, body: value }; } if (value.headers) { for (let key in value.headers) { if (value.headers[key] instanceof Array) { if (!value.multiValueHeaders) { value.multiValueHeaders = {}; } value.multiValueHeaders[key] = value.headers[key]; delete value.headers[key]; } } } this.result = value; this.finalEvent(this); }); } json(value) { this.end(value); return this; } file(filePath) { if (fs.existsSync(filePath)) { let contentType; const filename = path.basename(filePath); if (filePath.indexOf('.') === -1) { contentType = 'text/plain'; } else { const names = filename.split('.'); const extension = names[names.length - 1]; contentType = TYPES[`.${extension}`]; } const response = { isBase64Encoded: true, statusCode: 200, headers: { 'content-type': contentType } }; response.body = fs.readFileSync(filePath).toString('base64'); this.end(response); } else { this.end(resourceNotFound(filePath)); } } render(view, data, options, htmlHandler) { const filePath = path.join(this.options.templateFolder, view + '.ejs'); if (fs.existsSync(filePath)) { const tempResponse = { statusCode: this.statusCode || 200, headers: { 'content-type': 'text/html' } }; if (this.options.defaultTemplateData) { data = Object.assign(this.options.defaultTemplateData, data); } const headers = Object.assign(tempResponse.headers, this.headers); const response = { ...tempResponse, headers }; let template = fs.readFileSync(filePath, 'utf-8'); options = options || { views: [this.options.templateFolder] }; response.body = ejs.render(template, data, options, htmlHandler); this.end(response); } else { this.end(resourceNotFound(view)); } } redirect(url) { this.end({ statusCode: this.statusCode || 302, headers: { 'content-type': 'text/html', location: url.replace(/([^\u0000-\u00FF])/g, function ($) { return encodeURI($)}), ...this.headers } }); } cookie(name, value, opts = {}) { let maxAge, signedValue; let cookies = []; let val = typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value); if (opts.maxAge) { maxAge = opts.maxAge - 0; if (isNaN(maxAge)) throw new Error('maxAge should be a Number'); } opts.path = opts.path || '/'; opts.maxAge = opts.maxAge || 7200; const prevCookie = this.headers['Set-Cookie'] || ''; if (opts.signed) { let signOption = opts.maxAge ? { expiresIn: opts.maxAge } : undefined; signedValue = jwt.sign( { data: val }, SECRET, signOption ); signedValue = 's:' + signedValue; } else { signedValue = val; } if (prevCookie) { if (prevCookie instanceof Array) { cookies = prevCookie; } else { cookies.push(prevCookie); } } const latestCookie = cookie.serialize(name, String(signedValue), opts); cookies.push(latestCookie); this.headers['Set-Cookie'] = cookies; return this; } clearCookie(name, options) { const opts = Object.assign({ expires: new Date(1), path: '/' }, options); return this.cookie(name, '', opts); } error(e) { let body, headers, statusCode = this.statusCode || 500; const isJsonRequest = (headers) => { if (headers.referer) { const accepts = headers.accept.split(','); for (const accept of accepts) { if (accept.includes('application/json')) { return true; } } } return false; }; const createResponseBody = () => { if (e instanceof Error) { body = { statusCode: statusCode, message: e.message, stack: e.stack }; } else if (typeof e === 'object') { body = { statusCode: statusCode, data: e }; } else { body = { statusCode: statusCode, message: e }; } headers = { 'content-type': 'application/json' }; }; if (this.req.httpMethod === 'GET') { if (!isJsonRequest(this.req.headers)) { headers = { 'content-type': 'text/plain' }; if (e instanceof Error) { body = `${e.stack}`; } else if (typeof e === 'object') { body = JSON.stringify(e); } else { body = e; } } else { createResponseBody(); } } else { createResponseBody(); } this.end({ statusCode, headers, body }); } notAuthorized(e) { this.end({ statusCode: this.statusCode || 401, headers: { 'content-type': this.req.httpMethod === 'GET' ? 'text/html' : 'application/json' }, body: e }); } text(str) { this.end({ statusCode: this.statusCode || 200, headers: { 'content-type': 'text/plain' }, body: str }); } } module.exports = Response;