1 | 'use strict'
|
2 |
|
3 | const http = require('http')
|
4 | const https = require('https')
|
5 | const headersFactory = require('../mock/headers')
|
6 |
|
7 | const http2ForbiddenResponseHeaders = [
|
8 | 'transfer-encoding',
|
9 | 'connection',
|
10 | 'keep-alive'
|
11 | ]
|
12 |
|
13 | function protocol (url) {
|
14 | if (url.startsWith('https')) {
|
15 | return https
|
16 | }
|
17 | return http
|
18 | }
|
19 |
|
20 | function unsecureCookies (headers) {
|
21 | const setCookie = headers['set-cookie']
|
22 | if (setCookie) {
|
23 | headers['set-cookie'] = setCookie.map(cookie => cookie.replace(/\s*secure;/i, ''))
|
24 | }
|
25 | }
|
26 |
|
27 | function noop () {}
|
28 |
|
29 | function validateHook (mapping, hookName) {
|
30 | if (typeof mapping[hookName] === 'string') {
|
31 | mapping[hookName] = require(mapping[hookName])
|
32 | }
|
33 | }
|
34 |
|
35 | module.exports = {
|
36 | schema: {
|
37 | 'unsecure-cookies': {
|
38 | type: 'boolean',
|
39 | defaultValue: false
|
40 | },
|
41 | 'forward-request': {
|
42 | types: ['function', 'string'],
|
43 | defaultValue: noop
|
44 | },
|
45 | 'forward-response': {
|
46 | types: ['function', 'string'],
|
47 | defaultValue: noop
|
48 | },
|
49 | 'ignore-unverifiable-certificate': {
|
50 | type: 'boolean',
|
51 | defaultValue: false
|
52 | }
|
53 | },
|
54 | validate: async mapping => {
|
55 | validateHook(mapping, 'forward-request')
|
56 | validateHook(mapping, 'forward-response')
|
57 | },
|
58 | redirect: async ({ configuration, mapping, match, redirect: url, request, response }) => {
|
59 | let done
|
60 | let fail
|
61 | const promise = new Promise((resolve, reject) => {
|
62 | done = resolve
|
63 | fail = reject
|
64 | })
|
65 | const { method, headers } = request
|
66 | const options = {
|
67 | method,
|
68 | url,
|
69 | headers: headersFactory(headers)
|
70 | }
|
71 | delete options.headers.host
|
72 | if (mapping['ignore-unverifiable-certificate']) {
|
73 | options.rejectUnauthorized = false
|
74 | }
|
75 | const context = {}
|
76 | const hookParams = {
|
77 | configuration,
|
78 | context,
|
79 | mapping,
|
80 | match,
|
81 | request: options,
|
82 | incoming: request
|
83 | }
|
84 | await mapping['forward-request'](hookParams)
|
85 | Object.keys(options.headers)
|
86 | .filter(header => header.startsWith(':'))
|
87 | .forEach(header => delete options.headers[header])
|
88 | const redirectedRequest = protocol(options.url).request(options.url, options, async redirectedResponse => {
|
89 | if (mapping['unsecure-cookies']) {
|
90 | unsecureCookies(redirectedResponse.headers)
|
91 | }
|
92 | const { headers: responseHeaders } = redirectedResponse
|
93 | const result = await mapping['forward-response']({
|
94 | ...hookParams,
|
95 | statusCode: redirectedResponse.statusCode,
|
96 | headers: responseHeaders
|
97 | })
|
98 | if (result !== undefined) {
|
99 | if (!['GET', 'HEAD'].includes(request.method)) {
|
100 | return fail(new Error('Internal redirection impossible because the body is already consumed'))
|
101 | }
|
102 | return done(result)
|
103 | }
|
104 | if (configuration.http2) {
|
105 | http2ForbiddenResponseHeaders.forEach(header => delete responseHeaders[header])
|
106 | }
|
107 | response.writeHead(redirectedResponse.statusCode, responseHeaders)
|
108 | if (request.aborted) {
|
109 | response.end()
|
110 | return done()
|
111 | }
|
112 | response.on('finish', () => done(result))
|
113 | redirectedResponse
|
114 | .on('error', fail)
|
115 | .pipe(response)
|
116 | })
|
117 | redirectedRequest.on('error', fail)
|
118 | request
|
119 | .on('error', fail)
|
120 | .pipe(redirectedRequest)
|
121 | return promise
|
122 | }
|
123 | }
|