1 | 'use strict';
|
2 |
|
3 | const debug = require('debug')('grown:conn:response');
|
4 |
|
5 | const statusCodes = require('http').STATUS_CODES;
|
6 | const qs = require('querystring');
|
7 | const url = require('url');
|
8 | const mime = require('mime');
|
9 | const send = require('send');
|
10 | const path = require('path');
|
11 | const https = require('https');
|
12 | const http = require('http');
|
13 | const fs = require('fs');
|
14 |
|
15 | module.exports = (Grown, util) => {
|
16 | function _finishRequest(ctx, body) {
|
17 | return ctx.halt(() => {
|
18 |
|
19 | if (ctx.res.finished) {
|
20 | throw new Error('Already finished');
|
21 | }
|
22 |
|
23 | ctx.res.statusCode = ctx.status_code;
|
24 | ctx.res.statusMessage = statusCodes[ctx.res.statusCode];
|
25 |
|
26 |
|
27 | if (body && typeof body.pipe === 'function') {
|
28 | debug('#%s Done. Reponse is an stream. Sending as %s', ctx.pid, ctx.content_type);
|
29 |
|
30 |
|
31 | if (!ctx.res._header) {
|
32 | ctx.res.setHeader('Content-Type', ctx.content_type);
|
33 | ctx.res.writeHead(ctx.res.statusCode);
|
34 | }
|
35 |
|
36 | return new Promise((resolve, reject) => {
|
37 | body.on('close', resolve);
|
38 | body.on('error', reject);
|
39 | body.pipe(ctx.res);
|
40 | });
|
41 | }
|
42 |
|
43 |
|
44 | if (body !== null && Buffer.isBuffer(body)) {
|
45 | debug('#%s Response is a buffer. Sending as %s', ctx.pid, ctx.content_type);
|
46 |
|
47 |
|
48 | if (!ctx.res._header) {
|
49 | ctx.res.setHeader('Content-Type', ctx.content_type);
|
50 | ctx.res.setHeader('Content-Length', body.length);
|
51 | }
|
52 | } else if (body !== null && typeof body === 'object') {
|
53 | debug('#%s Response is an object. Sending as application/json', ctx.pid);
|
54 |
|
55 | body = JSON.stringify(body);
|
56 | ctx.content_type = 'application/json';
|
57 | }
|
58 |
|
59 |
|
60 | if (!ctx.res._header) {
|
61 | ctx.res.setHeader('Content-Type', `${ctx.content_type}; charset=${ctx.resp_charset}`);
|
62 | ctx.res.setHeader('Content-Length', Buffer.byteLength(body || ''));
|
63 | ctx.res.writeHead(ctx.res.statusCode);
|
64 | }
|
65 |
|
66 | ctx.res.write(body || '');
|
67 | ctx.res.end();
|
68 | });
|
69 | }
|
70 |
|
71 | function _endRequest(ctx, code, message) {
|
72 |
|
73 | if (ctx.res && ctx.res.finished) {
|
74 | throw new Error('Already finished');
|
75 | }
|
76 |
|
77 | let _code = code;
|
78 |
|
79 |
|
80 | if (typeof code === 'string' || code instanceof Buffer) {
|
81 | _code = ctx.status_code;
|
82 | message = code;
|
83 | }
|
84 |
|
85 |
|
86 | if (code instanceof Error) {
|
87 | message = code.message || code.toString();
|
88 | _code = code.statusCode || 500;
|
89 | }
|
90 |
|
91 |
|
92 | if (!ctx.has_body) {
|
93 | ctx.resp_body = message || ctx.resp_body;
|
94 | }
|
95 |
|
96 |
|
97 | if (!ctx.has_status) {
|
98 | ctx.status_code = typeof _code === 'number' ? _code : ctx.status_code;
|
99 | }
|
100 |
|
101 | return ctx.send(ctx.resp_body);
|
102 | }
|
103 |
|
104 | function _cutBody(value) {
|
105 |
|
106 | if (typeof value !== 'string') {
|
107 | value = util.inspect(value);
|
108 | }
|
109 |
|
110 | value = value.replace(/\s+/g, ' ').trim();
|
111 |
|
112 | return value.length > 99
|
113 | ? `${value.substr(0, 100)}...`
|
114 | : value;
|
115 | }
|
116 |
|
117 | function _fixURL(location) {
|
118 | const _uri = url.parse(location);
|
119 |
|
120 | let _query = '';
|
121 |
|
122 |
|
123 | if (_uri.query) {
|
124 | _query = qs.stringify(qs.parse(_uri.query));
|
125 | }
|
126 |
|
127 | return [
|
128 | _uri.protocol ? `${_uri.protocol}//` : '',
|
129 | _uri.hostname ? _uri.hostname : '',
|
130 | _uri.port ? `:${_uri.port}` : '',
|
131 | _uri.pathname && _uri.pathname !== '/' ? _uri.pathname : '',
|
132 | _query ? `?${_query}` : '',
|
133 | ].join('');
|
134 | }
|
135 |
|
136 | return Grown('Conn.Response', {
|
137 | _finishRequest,
|
138 | _endRequest,
|
139 | _cutBody,
|
140 | _fixURL,
|
141 |
|
142 | $before_render(ctx, template) {
|
143 | util.extendValues(template.locals, ctx.state);
|
144 | },
|
145 |
|
146 | $mixins() {
|
147 | const self = this;
|
148 |
|
149 | const _response = {
|
150 | headers: Object.create(null),
|
151 | type: 'text/html',
|
152 | body: null,
|
153 | status: null,
|
154 | charset: 'utf8',
|
155 | };
|
156 |
|
157 | return {
|
158 | props: {
|
159 |
|
160 | has_body: () => _response.body !== null,
|
161 | has_status: () => _response.status !== null,
|
162 |
|
163 | get content_type() {
|
164 | return _response.type;
|
165 | },
|
166 |
|
167 | set content_type(mimeType) {
|
168 |
|
169 | if (!(mimeType && typeof mimeType === 'string')) {
|
170 | throw new Error(`Invalid type: '${mimeType}'`);
|
171 | }
|
172 |
|
173 | _response.type = mimeType;
|
174 | },
|
175 |
|
176 | get status_code() {
|
177 | return _response.status !== null
|
178 | ? _response.status
|
179 | : 200;
|
180 | },
|
181 |
|
182 | set status_code(code) {
|
183 |
|
184 | if (!(code && typeof code === 'number' && statusCodes[code])) {
|
185 | throw new Error(`Invalid status_code: ${code}`);
|
186 | }
|
187 |
|
188 | debug('#%s Set status: %s', this.pid, code);
|
189 |
|
190 | _response.status = code;
|
191 | },
|
192 |
|
193 | get resp_body() {
|
194 | return _response.body;
|
195 | },
|
196 |
|
197 | set resp_body(value) {
|
198 |
|
199 | if (!(typeof value === 'string' || (typeof value === 'object' && !Array.isArray(value))
|
200 | || (value && typeof value.pipe === 'function') || (value instanceof Buffer))) {
|
201 | throw new Error(`Invalid resp_body: ${value}`);
|
202 | }
|
203 |
|
204 | debug('#%s Set body: %s', this.pid, _cutBody(value));
|
205 |
|
206 | _response.body = value;
|
207 | },
|
208 |
|
209 | get resp_charset() {
|
210 | return _response.charset;
|
211 | },
|
212 |
|
213 | set resp_charset(value) {
|
214 |
|
215 | if (typeof value !== 'string') {
|
216 | throw new Error(`Invalid charset: ${value}`);
|
217 | }
|
218 |
|
219 | _response.charset = value || 'utf8';
|
220 | },
|
221 |
|
222 | get resp_headers() {
|
223 | return this.res.getHeaders();
|
224 | },
|
225 |
|
226 | set resp_headers(value) {
|
227 |
|
228 | if (Object.prototype.toString.call(value) !== '[object Object]') {
|
229 | throw new Error(`Invalid headers: ${value}`);
|
230 | }
|
231 |
|
232 | this.res._headers = value;
|
233 | },
|
234 | },
|
235 | methods: {
|
236 |
|
237 | get_resp_header(name) {
|
238 |
|
239 | if (!(name && typeof name === 'string')) {
|
240 | throw new Error(`Invalid resp_header: '${name}'`);
|
241 | }
|
242 |
|
243 | return this.res.getHeader(name);
|
244 | },
|
245 |
|
246 | put_resp_header(name, value) {
|
247 |
|
248 | if (!name || typeof name !== 'string') {
|
249 | throw new Error(`Invalid resp_header: '${name}' => '${value}'`);
|
250 | }
|
251 |
|
252 | this.res.setHeader(name, value);
|
253 |
|
254 | return this;
|
255 | },
|
256 |
|
257 | merge_resp_headers(headers) {
|
258 |
|
259 | if (!(headers && (typeof headers === 'object' && !Array.isArray(headers)))) {
|
260 | throw new Error(`Invalid resp_headers: '${headers}'`);
|
261 | }
|
262 |
|
263 | Object.keys(headers).forEach(key => {
|
264 | this.put_resp_header(key, headers[key]);
|
265 | });
|
266 |
|
267 | return this;
|
268 | },
|
269 |
|
270 | delete_resp_header(name) {
|
271 |
|
272 | if (!(name && typeof name === 'string')) {
|
273 | throw new Error(`Invalid resp_header: '${name}'`);
|
274 | }
|
275 |
|
276 | this.res.removeHeader(name);
|
277 |
|
278 | return this;
|
279 | },
|
280 |
|
281 | redirect(location, timeout, body) {
|
282 |
|
283 | if (!(location && typeof location === 'string')) {
|
284 | throw new Error(`Invalid location: '${location}`);
|
285 | }
|
286 |
|
287 |
|
288 | if (timeout) {
|
289 | const meta = `<meta http-equiv="refresh" content="${timeout};url=${location}">${body || ''}`;
|
290 |
|
291 | return this.end(302, meta);
|
292 | }
|
293 |
|
294 | debug('#%s Done. Redirection was found', this.pid);
|
295 |
|
296 | return this.put_resp_header('Location', self._fixURL(location)).end(302);
|
297 | },
|
298 |
|
299 | json(value) {
|
300 |
|
301 | if (!value || typeof value !== 'object') {
|
302 | throw new Error(`Invalid JSON value: ${value}`);
|
303 | }
|
304 |
|
305 | return this.send(value);
|
306 | },
|
307 |
|
308 | get_file(_url, filePath) {
|
309 | return new Promise((resolve, reject) => {
|
310 | const dest = path.resolve(filePath);
|
311 | const file = fs.createWriteStream(dest);
|
312 |
|
313 | (_url.indexOf('https:') !== -1 ? https : http)
|
314 | .get(_url, response => {
|
315 | response.pipe(file);
|
316 | file.on('finish', () => file.close(() => resolve(dest)));
|
317 | }).on('error', err => {
|
318 | fs.unlink(dest);
|
319 | reject(err);
|
320 | });
|
321 | });
|
322 | },
|
323 |
|
324 | send_file(entry, mimeType) {
|
325 |
|
326 | if (typeof entry === 'object') {
|
327 | mimeType = entry.type || mimeType;
|
328 | entry = entry.file;
|
329 | }
|
330 |
|
331 |
|
332 | if (!mimeType) {
|
333 | mimeType = mime.getType(entry);
|
334 | }
|
335 |
|
336 | const pathname = encodeURI(path.basename(entry));
|
337 |
|
338 | const file = send(this.req, pathname, {
|
339 | root: path.dirname(entry),
|
340 | });
|
341 |
|
342 | file.on('headers', _res => {
|
343 |
|
344 | if (mimeType) {
|
345 | _res.setHeader('Content-Type', mimeType);
|
346 | }
|
347 | });
|
348 |
|
349 | return new Promise((resolve, reject) => {
|
350 | this.res.statusCode = 200;
|
351 | file.on('error', reject);
|
352 | file.on('end', resolve);
|
353 | file.pipe(this.res);
|
354 | });
|
355 | },
|
356 |
|
357 | send(body) {
|
358 | return self._finishRequest(this, body);
|
359 | },
|
360 |
|
361 | end(code, message) {
|
362 | return self._endRequest(this, code, message);
|
363 | },
|
364 | },
|
365 | };
|
366 | },
|
367 | });
|
368 | };
|