source: node_modules/undici/lib/handler/RedirectHandler.js

main
Last change on this file was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 7.1 KB
Line 
1'use strict'
2
3const util = require('../core/util')
4const { kBodyUsed } = require('../core/symbols')
5const assert = require('assert')
6const { InvalidArgumentError } = require('../core/errors')
7const EE = require('events')
8
9const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
10
11const kBody = Symbol('body')
12
13class 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
26class 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
173function 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
186function 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
196function 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
216module.exports = RedirectHandler
Note: See TracBrowser for help on using the repository browser.