'use strict'; // ////////////////////////////////////////////////////////////////////////////// // / DISCLAIMER // / // / Copyright 2016 ArangoDB GmbH, Cologne, Germany // / // / Licensed under the Apache License, Version 2.0 (the "License") // / you may not use this file except in compliance with the License. // / You may obtain a copy of the License at // / // / http://www.apache.org/licenses/LICENSE-2.0 // / // / Unless required by applicable law or agreed to in writing, software // / distributed under the License is distributed on an "AS IS" BASIS, // / WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // / See the License for the specific language governing permissions and // / limitations under the License. // / // / Copyright holder is ArangoDB GmbH, Cologne, Germany // / // / @author Alan Plum // ////////////////////////////////////////////////////////////////////////////// const _ = require('lodash'); const fs = require('fs'); const vary = require('vary'); const httperr = require('http-errors'); const statuses = require('statuses'); const mediaTyper = require('media-typer'); const mimeTypes = require('mime-types'); const typeIs = require('type-is'); const contentDisposition = require('content-disposition'); const actions = require('@arangodb/actions'); const crypto = require('@arangodb/crypto'); const MIME_BINARY = 'application/octet-stream'; const MIME_JSON = 'application/json; charset=utf-8'; module.exports = class SyntheticResponse { constructor (res, context) { this._raw = res; this._responses = new Map(); this.context = context; } _PRINT (ctx) { ctx.output += '[OutgoingResponse]'; } // Node compat get headers () { if (!this._raw.headers) { this._raw.headers = {}; } return this._raw.headers; } set headers (headers) { this._raw.headers = headers; if (!headers) { return; } for (const name of Object.keys(headers)) { if (name.toLowerCase() === 'content-type') { this._raw.contentType = headers[name]; } } } get statusCode () { return this._raw.responseCode; } set statusCode (value) { this._raw.responseCode = value; } get body () { return this._raw.body; } set body (data) { if (data === null || data === undefined) { delete this._raw.body; return; } if (typeof data === 'string' || data instanceof Buffer) { this._raw.body = data; } else if (typeof data === 'object') { if (this.context.isDevelopment) { this._raw.body = JSON.stringify(data, null, 2); } else { this._raw.body = JSON.stringify(data); } } else { this._raw.body = String(data); } } getHeader (name) { name = name.toLowerCase(); if (name === 'content-type') { return this._raw.contentType; } return this.headers[name]; } removeHeader (name) { name = name.toLowerCase(); if (name === 'content-type') { delete this._raw.contentType; } delete this.headers[name]; return this; } setHeader (name, value) { if (!name) { return this; } name = name.toLowerCase(); if (name === 'content-type') { this._raw.contentType = value; } else { this.headers[name] = value; } return this; } write (data) { const bodyIsBuffer = this._raw.body instanceof Buffer; const dataIsBuffer = data instanceof Buffer; if (data === null || data === undefined) { return this; } if (!dataIsBuffer) { if (typeof data === 'object') { if (this.context.isDevelopment) { data = JSON.stringify(data, null, 2); } else { data = JSON.stringify(data); } } else { data = String(data); } } if (!this._raw.body) { this._raw.body = data; } else if (bodyIsBuffer || dataIsBuffer) { this._raw.body = Buffer.concat([ bodyIsBuffer ? this._raw.body : new Buffer(this._raw.body), dataIsBuffer ? data : new Buffer(data) ]); } else { this._raw.body += data; } return this; } // Express compat attachment (filename) { this.headers['content-disposition'] = contentDisposition(filename); if (filename && !this._raw.contentType) { this._raw.contentType = mimeTypes.lookup(filename) || MIME_BINARY; } return this; } download (path, filename) { this.attachment(filename || path); this.sendFile(path); return this; } json (value, pretty) { if (!this._raw.contentType) { this._raw.contentType = MIME_JSON; } if (pretty || this.context.isDevelopment) { this._raw.body = JSON.stringify(value, null, 2); } else { this._raw.body = JSON.stringify(value); } return this; } redirect (status, path) { if (!path) { path = status; status = undefined; } if (status === 'permanent') { status = 301; } if (status || !this.statusCode) { this.statusCode = status || 302; } this.setHeader('location', path); return this; } sendFile (filename, opts) { if (typeof opts === 'boolean') { opts = {lastModified: opts}; } if (!opts) { opts = {}; } this._raw.body = fs.readFileSync(filename); if (opts.lastModified || ( opts.lastModified !== false && !this.headers['last-modified'] )) { const lastModified = new Date(fs.mtime(filename) * 1000); this.headers['last-modified'] = lastModified.toUTCString(); } if (!this._raw.contentType) { this._raw.contentType = mimeTypes.lookup(filename) || MIME_BINARY; } return this; } sendStatus (status) { if (typeof status === 'string') { status = statuses(status); } const message = String(statuses[status] || status); this.statusCode = status; this.body = message; return this; } set (name, value) { if (name && typeof name === 'object') { _.each(name, (v, k) => this.set(k, v)); } else { this.setHeader(name, value); } return this; } status (status) { if (typeof status === 'string') { status = statuses(status); } this.statusCode = status; return this; } vary (values) { let header = this.getHeader('vary') || ''; if (!Array.isArray(values)) { values = Array.prototype.slice.call(arguments); } for (const value of values) { header = vary.append(header, value); } this.setHeader('vary', header); return this; } // idiosyncratic cookie (name, value, opts) { if (!opts) { opts = {}; } else if (typeof opts === 'string') { opts = {secret: opts}; } else if (typeof opts === 'number') { opts = {ttl: opts}; } const ttl = ( (typeof opts.ttl === 'number' && opts.ttl !== Infinity) ? opts.ttl : undefined ); actions.addCookie( this._raw, name, value, ttl, opts.path, opts.domain, opts.secure, opts.httpOnly ); if (opts.secret) { const signature = crypto.hmac(opts.secret, value, opts.algorithm); actions.addCookie( this._raw, `${name}.sig`, signature, ttl, opts.path, opts.domain, opts.secure, opts.httpOnly ); } return this; } throw (status, reason, options) { if (typeof status === 'string') { status = statuses(status); } if (reason instanceof Error) { const err = reason; reason = err.message; options = Object.assign({ cause: err, errorNum: err.errorNum }, options); } if (reason && typeof reason === 'object') { options = reason; reason = undefined; } throw Object.assign( httperr(status, reason), {statusCode: status, status}, options ); } send (body, type) { if (body && body.isArangoResultSet) { body = body.toArray(); } if (!type) { type = 'auto'; } let contentType; const status = this.statusCode || 200; const response = this._responses.get(status); if (response) { if (response.model && response.model.forClient) { if (response.multiple && Array.isArray(body)) { body = body.map((item) => response.model.forClient(item)); } else { body = response.model.forClient(body); } } if (type === 'auto' && response.contentTypes) { type = response.contentTypes[0]; contentType = type; } } if (type === 'auto') { if (body instanceof Buffer) { type = MIME_BINARY; } else if (body && typeof body === 'object') { type = 'json'; } else { type = 'html'; } } type = mimeTypes.lookup(type) || type; let handler; for (const entry of this.context.service.types.entries()) { const key = entry[0]; const value = entry[1]; let match; if (key instanceof RegExp) { match = type.test(key); } else if (typeof key === 'function') { match = key(type); } else { match = typeIs.is(key, type); } if (match && value.forClient) { contentType = key; handler = value; break; } } if (handler) { const result = handler.forClient(body, this, mediaTyper.parse(contentType)); if (result.headers || result.data) { contentType = result.headers['content-type'] || contentType; this.set(result.headers); body = result.data; } else { body = result; } } this._raw.body = body; if (contentType) { this._raw.contentType = contentType; } return this; } type (type) { if (type) { this._raw.contentType = mimeTypes.lookup(type) || type; } return this._raw.contentType; } };