1 | 'use strict'
|
---|
2 |
|
---|
3 | const util = require('../core/util')
|
---|
4 | const { kBodyUsed } = require('../core/symbols')
|
---|
5 | const assert = require('assert')
|
---|
6 | const { InvalidArgumentError } = require('../core/errors')
|
---|
7 | const EE = require('events')
|
---|
8 |
|
---|
9 | const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
|
---|
10 |
|
---|
11 | const kBody = Symbol('body')
|
---|
12 |
|
---|
13 | class BodyAsyncIterable {
|
---|
14 | constructor (body) {
|
---|
15 | this[kBody] = body
|
---|
16 | this[kBodyUsed] = false
|
---|
17 | }
|
---|
18 |
|
---|
19 | async * [Symbol.asyncIterator] () {
|
---|
20 | assert(!this[kBodyUsed], 'disturbed')
|
---|
21 | this[kBodyUsed] = true
|
---|
22 | yield * this[kBody]
|
---|
23 | }
|
---|
24 | }
|
---|
25 |
|
---|
26 | class RedirectHandler {
|
---|
27 | constructor (dispatch, maxRedirections, opts, handler) {
|
---|
28 | if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) {
|
---|
29 | throw new InvalidArgumentError('maxRedirections must be a positive number')
|
---|
30 | }
|
---|
31 |
|
---|
32 | util.validateHandler(handler, opts.method, opts.upgrade)
|
---|
33 |
|
---|
34 | this.dispatch = dispatch
|
---|
35 | this.location = null
|
---|
36 | this.abort = null
|
---|
37 | this.opts = { ...opts, maxRedirections: 0 } // opts must be a copy
|
---|
38 | this.maxRedirections = maxRedirections
|
---|
39 | this.handler = handler
|
---|
40 | this.history = []
|
---|
41 |
|
---|
42 | if (util.isStream(this.opts.body)) {
|
---|
43 | // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
|
---|
44 | // so that it can be dispatched again?
|
---|
45 | // TODO (fix): Do we need 100-expect support to provide a way to do this properly?
|
---|
46 | if (util.bodyLength(this.opts.body) === 0) {
|
---|
47 | this.opts.body
|
---|
48 | .on('data', function () {
|
---|
49 | assert(false)
|
---|
50 | })
|
---|
51 | }
|
---|
52 |
|
---|
53 | if (typeof this.opts.body.readableDidRead !== 'boolean') {
|
---|
54 | this.opts.body[kBodyUsed] = false
|
---|
55 | EE.prototype.on.call(this.opts.body, 'data', function () {
|
---|
56 | this[kBodyUsed] = true
|
---|
57 | })
|
---|
58 | }
|
---|
59 | } else if (this.opts.body && typeof this.opts.body.pipeTo === 'function') {
|
---|
60 | // TODO (fix): We can't access ReadableStream internal state
|
---|
61 | // to determine whether or not it has been disturbed. This is just
|
---|
62 | // a workaround.
|
---|
63 | this.opts.body = new BodyAsyncIterable(this.opts.body)
|
---|
64 | } else if (
|
---|
65 | this.opts.body &&
|
---|
66 | typeof this.opts.body !== 'string' &&
|
---|
67 | !ArrayBuffer.isView(this.opts.body) &&
|
---|
68 | util.isIterable(this.opts.body)
|
---|
69 | ) {
|
---|
70 | // TODO: Should we allow re-using iterable if !this.opts.idempotent
|
---|
71 | // or through some other flag?
|
---|
72 | this.opts.body = new BodyAsyncIterable(this.opts.body)
|
---|
73 | }
|
---|
74 | }
|
---|
75 |
|
---|
76 | onConnect (abort) {
|
---|
77 | this.abort = abort
|
---|
78 | this.handler.onConnect(abort, { history: this.history })
|
---|
79 | }
|
---|
80 |
|
---|
81 | onUpgrade (statusCode, headers, socket) {
|
---|
82 | this.handler.onUpgrade(statusCode, headers, socket)
|
---|
83 | }
|
---|
84 |
|
---|
85 | onError (error) {
|
---|
86 | this.handler.onError(error)
|
---|
87 | }
|
---|
88 |
|
---|
89 | onHeaders (statusCode, headers, resume, statusText) {
|
---|
90 | this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body)
|
---|
91 | ? null
|
---|
92 | : parseLocation(statusCode, headers)
|
---|
93 |
|
---|
94 | if (this.opts.origin) {
|
---|
95 | this.history.push(new URL(this.opts.path, this.opts.origin))
|
---|
96 | }
|
---|
97 |
|
---|
98 | if (!this.location) {
|
---|
99 | return this.handler.onHeaders(statusCode, headers, resume, statusText)
|
---|
100 | }
|
---|
101 |
|
---|
102 | const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
|
---|
103 | const path = search ? `${pathname}${search}` : pathname
|
---|
104 |
|
---|
105 | // Remove headers referring to the original URL.
|
---|
106 | // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers.
|
---|
107 | // https://tools.ietf.org/html/rfc7231#section-6.4
|
---|
108 | this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin)
|
---|
109 | this.opts.path = path
|
---|
110 | this.opts.origin = origin
|
---|
111 | this.opts.maxRedirections = 0
|
---|
112 | this.opts.query = null
|
---|
113 |
|
---|
114 | // https://tools.ietf.org/html/rfc7231#section-6.4.4
|
---|
115 | // In case of HTTP 303, always replace method to be either HEAD or GET
|
---|
116 | if (statusCode === 303 && this.opts.method !== 'HEAD') {
|
---|
117 | this.opts.method = 'GET'
|
---|
118 | this.opts.body = null
|
---|
119 | }
|
---|
120 | }
|
---|
121 |
|
---|
122 | onData (chunk) {
|
---|
123 | if (this.location) {
|
---|
124 | /*
|
---|
125 | https://tools.ietf.org/html/rfc7231#section-6.4
|
---|
126 |
|
---|
127 | TLDR: undici always ignores 3xx response bodies.
|
---|
128 |
|
---|
129 | Redirection is used to serve the requested resource from another URL, so it is assumes that
|
---|
130 | no body is generated (and thus can be ignored). Even though generating a body is not prohibited.
|
---|
131 |
|
---|
132 | For status 301, 302, 303, 307 and 308 (the latter from RFC 7238), the specs mention that the body usually
|
---|
133 | (which means it's optional and not mandated) contain just an hyperlink to the value of
|
---|
134 | the Location response header, so the body can be ignored safely.
|
---|
135 |
|
---|
136 | For status 300, which is "Multiple Choices", the spec mentions both generating a Location
|
---|
137 | response header AND a response body with the other possible location to follow.
|
---|
138 | Since the spec explicitily chooses not to specify a format for such body and leave it to
|
---|
139 | servers and browsers implementors, we ignore the body as there is no specified way to eventually parse it.
|
---|
140 | */
|
---|
141 | } else {
|
---|
142 | return this.handler.onData(chunk)
|
---|
143 | }
|
---|
144 | }
|
---|
145 |
|
---|
146 | onComplete (trailers) {
|
---|
147 | if (this.location) {
|
---|
148 | /*
|
---|
149 | https://tools.ietf.org/html/rfc7231#section-6.4
|
---|
150 |
|
---|
151 | TLDR: undici always ignores 3xx response trailers as they are not expected in case of redirections
|
---|
152 | and neither are useful if present.
|
---|
153 |
|
---|
154 | See comment on onData method above for more detailed informations.
|
---|
155 | */
|
---|
156 |
|
---|
157 | this.location = null
|
---|
158 | this.abort = null
|
---|
159 |
|
---|
160 | this.dispatch(this.opts, this)
|
---|
161 | } else {
|
---|
162 | this.handler.onComplete(trailers)
|
---|
163 | }
|
---|
164 | }
|
---|
165 |
|
---|
166 | onBodySent (chunk) {
|
---|
167 | if (this.handler.onBodySent) {
|
---|
168 | this.handler.onBodySent(chunk)
|
---|
169 | }
|
---|
170 | }
|
---|
171 | }
|
---|
172 |
|
---|
173 | function parseLocation (statusCode, headers) {
|
---|
174 | if (redirectableStatusCodes.indexOf(statusCode) === -1) {
|
---|
175 | return null
|
---|
176 | }
|
---|
177 |
|
---|
178 | for (let i = 0; i < headers.length; i += 2) {
|
---|
179 | if (headers[i].toString().toLowerCase() === 'location') {
|
---|
180 | return headers[i + 1]
|
---|
181 | }
|
---|
182 | }
|
---|
183 | }
|
---|
184 |
|
---|
185 | // https://tools.ietf.org/html/rfc7231#section-6.4.4
|
---|
186 | function shouldRemoveHeader (header, removeContent, unknownOrigin) {
|
---|
187 | return (
|
---|
188 | (header.length === 4 && header.toString().toLowerCase() === 'host') ||
|
---|
189 | (removeContent && header.toString().toLowerCase().indexOf('content-') === 0) ||
|
---|
190 | (unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') ||
|
---|
191 | (unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie')
|
---|
192 | )
|
---|
193 | }
|
---|
194 |
|
---|
195 | // https://tools.ietf.org/html/rfc7231#section-6.4
|
---|
196 | function cleanRequestHeaders (headers, removeContent, unknownOrigin) {
|
---|
197 | const ret = []
|
---|
198 | if (Array.isArray(headers)) {
|
---|
199 | for (let i = 0; i < headers.length; i += 2) {
|
---|
200 | if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin)) {
|
---|
201 | ret.push(headers[i], headers[i + 1])
|
---|
202 | }
|
---|
203 | }
|
---|
204 | } else if (headers && typeof headers === 'object') {
|
---|
205 | for (const key of Object.keys(headers)) {
|
---|
206 | if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) {
|
---|
207 | ret.push(key, headers[key])
|
---|
208 | }
|
---|
209 | }
|
---|
210 | } else {
|
---|
211 | assert(headers == null, 'headers must be an object or an array')
|
---|
212 | }
|
---|
213 | return ret
|
---|
214 | }
|
---|
215 |
|
---|
216 | module.exports = RedirectHandler
|
---|