1 | 'use strict'
|
2 |
|
3 | const logError = require('./logError')
|
4 | const interpolate = require('./interpolate')
|
5 | const {
|
6 | $configurationInterface,
|
7 | $configurationRequests,
|
8 | $dispatcherEnd,
|
9 | $mappingMatch,
|
10 | $requestId,
|
11 | $requestInternal,
|
12 | $responseEnded
|
13 | } = require('./symbols')
|
14 |
|
15 | function hookEnd (response) {
|
16 | if (!response.end[$dispatcherEnd]) {
|
17 | const end = response.end
|
18 | response.end = function () {
|
19 | this[$responseEnded] = true
|
20 | return end.apply(this, arguments)
|
21 | }
|
22 | response.end[$dispatcherEnd] = true
|
23 | }
|
24 | return response
|
25 | }
|
26 |
|
27 | function emit (event, emitParameters, additionalParameters) {
|
28 | this.emit(event, { ...emitParameters, ...additionalParameters })
|
29 | }
|
30 |
|
31 | function emitError (reason) {
|
32 | try {
|
33 | emit.call(this.eventEmitter, 'error', this.emitParameters, { reason })
|
34 | } catch (e) {
|
35 | logError({ ...this.emitParameters, reason: e })
|
36 | }
|
37 | }
|
38 |
|
39 | function redirected () {
|
40 | const end = new Date()
|
41 | Object.assign(this.emitParameters, {
|
42 | end,
|
43 | timeSpent: end - this.emitParameters.start,
|
44 | statusCode: this.response.statusCode
|
45 | })
|
46 | try {
|
47 | emit.call(this.eventEmitter, 'redirected', this.emitParameters)
|
48 | } catch (reason) {
|
49 | emitError.call(this, reason)
|
50 | }
|
51 | this.redirected()
|
52 | }
|
53 |
|
54 | function error (reason) {
|
55 | let statusCode
|
56 | if (typeof reason === 'number') {
|
57 | statusCode = reason
|
58 | } else {
|
59 | statusCode = 500
|
60 | }
|
61 | emitError.call(this, reason)
|
62 | if (this.failed) {
|
63 |
|
64 | this.response.end()
|
65 | redirected.call(this)
|
66 | } else {
|
67 | this.failed = true
|
68 | dispatch.call(this, statusCode)
|
69 | }
|
70 | }
|
71 |
|
72 | function redispatch (url) {
|
73 | const redirectCount = ++this.redirectCount
|
74 | if (redirectCount > this.configuration['max-redirect']) {
|
75 | error.call(this, 508)
|
76 | } else {
|
77 | dispatch.call(this, url)
|
78 | }
|
79 | }
|
80 |
|
81 | function redirecting ({ mapping = {}, match, handler, type, redirect, url, index = 0 }) {
|
82 | try {
|
83 | emit.call(this.eventEmitter, 'redirecting', this.emitParameters, { type, redirect })
|
84 | if (mapping['exclude-from-holding-list']) {
|
85 | this.setAsNonHolding()
|
86 | }
|
87 | return handler.redirect({
|
88 | configuration: this.configuration[$configurationInterface],
|
89 | mapping,
|
90 | match,
|
91 | redirect,
|
92 | request: this.request,
|
93 | response: hookEnd(this.response)
|
94 | })
|
95 | .then(result => {
|
96 | if (undefined !== result) {
|
97 | redispatch.call(this, result)
|
98 | } else if (this.response[$responseEnded]) {
|
99 | redirected.call(this)
|
100 | } else {
|
101 | dispatch.call(this, url, index + 1)
|
102 | }
|
103 | }, error.bind(this))
|
104 | } catch (e) {
|
105 | error.call(this, e)
|
106 | }
|
107 | }
|
108 |
|
109 | async function dispatch (url, index = 0) {
|
110 | if (typeof url === 'number') {
|
111 | return redirecting.call(this, {
|
112 | type: 'status',
|
113 | handler: this.configuration.handlers.status,
|
114 | redirect: url
|
115 | })
|
116 | }
|
117 | const length = this.configuration.mappings.length
|
118 | while (index < length) {
|
119 | const mapping = this.configuration.mappings[index]
|
120 | let match
|
121 | try {
|
122 | match = await mapping[$mappingMatch](this.request, url)
|
123 | } catch (reason) {
|
124 | return error.call(this, reason)
|
125 | }
|
126 | if (match) {
|
127 | if (['string', 'number'].includes(typeof match)) {
|
128 | return redispatch.call(this, match)
|
129 | }
|
130 | const { handler, redirect, type } = this.configuration.handler(mapping)
|
131 | return redirecting.call(this, { mapping, match, handler, type, redirect: interpolate(match, redirect), url, index })
|
132 | }
|
133 | ++index
|
134 | }
|
135 | error.call(this, 501)
|
136 | }
|
137 |
|
138 | module.exports = function (configuration, request, response) {
|
139 | const configurationRequests = configuration[$configurationRequests]
|
140 | const { contexts } = configurationRequests
|
141 | const emitParameters = {
|
142 | id: ++configurationRequests.lastId,
|
143 | internal: !!request[$requestInternal],
|
144 | method: request.method,
|
145 | url: request.url,
|
146 | headers: Object.assign({}, request.headers),
|
147 | start: new Date()
|
148 | }
|
149 | let dispatched
|
150 | const dispatching = new Promise(resolve => { dispatched = resolve })
|
151 | let release
|
152 | const holding = new Promise(resolve => { release = resolve })
|
153 | const context = {
|
154 | configuration,
|
155 | eventEmitter: this,
|
156 | emitParameters,
|
157 | holding,
|
158 | redirectCount: 0,
|
159 | redirected () {
|
160 | this.setAsNonHolding()
|
161 | dispatched()
|
162 | },
|
163 | request,
|
164 | response,
|
165 | setAsNonHolding () {
|
166 | this.released = true
|
167 | release()
|
168 | }
|
169 | }
|
170 | request[$requestId] = emitParameters.id
|
171 | request.on('aborted', emit.bind(this, 'aborted', emitParameters))
|
172 | request.on('close', emit.bind(this, 'closed', emitParameters))
|
173 | try {
|
174 | emit.call(this, 'incoming', emitParameters)
|
175 | } catch (reason) {
|
176 | error.call(context, reason)
|
177 | return dispatching
|
178 | }
|
179 | return configurationRequests.holding
|
180 | .then(() => {
|
181 | contexts.push(context)
|
182 | dispatch.call(context, request.url)
|
183 | return dispatching
|
184 | })
|
185 | .then(() => {
|
186 | const index = contexts.findIndex(candidate => candidate === context)
|
187 | contexts.splice(index, 1)
|
188 | })
|
189 | }
|