-
Notifications
You must be signed in to change notification settings - Fork 474
/
follow-redirects.js
208 lines (183 loc) · 7.02 KB
/
follow-redirects.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
'use strict';
var url = require('url');
var assert = require('assert');
var http = require('http');
var https = require('https');
var Writable = require('stream').Writable;
var nativeProtocols = {'http:': http, 'https:': https};
var schemes = {};
var exports = module.exports = {
maxRedirects: 21
};
// RFC7231§4.2.1: Of the request methods defined by this specification,
// the GET, HEAD, OPTIONS, and TRACE methods are defined to be safe.
var safeMethods = {GET: true, HEAD: true, OPTIONS: true, TRACE: true};
// Create handlers that pass events from native requests
var eventHandlers = Object.create(null);
['abort', 'aborted', 'error', 'socket'].forEach(function (event) {
eventHandlers[event] = function (arg) {
this._redirectable.emit(event, arg);
};
});
// An HTTP(S) request that can be redirected
function RedirectableRequest(options, responseCallback) {
// Initialize the request
Writable.call(this);
this._options = options;
this._redirectCount = 0;
this._bufferedWrites = [];
// Attach a callback if passed
if (responseCallback) {
this.on('response', responseCallback);
}
// React to responses of native requests
var self = this;
this._onNativeResponse = function (response) {
self._processResponse(response);
};
// Perform the first request
this._performRequest();
}
RedirectableRequest.prototype = Object.create(Writable.prototype);
// Executes the next native request (initial or redirect)
RedirectableRequest.prototype._performRequest = function () {
// If specified, use the agent corresponding to the protocol
// (HTTP and HTTPS use different types of agents)
var protocol = this._options.protocol;
if (this._options.agents) {
this._options.agent = this._options.agents[schemes[protocol]];
}
// Create the native request
var nativeProtocol = nativeProtocols[this._options.protocol];
var request = this._currentRequest =
nativeProtocol.request(this._options, this._onNativeResponse);
this._currentUrl = url.format(this._options);
// Set up event handlers
request._redirectable = this;
for (var event in eventHandlers) {
if (event) {
request.on(event, eventHandlers[event]);
}
}
// End a redirected request
// (The first request must be ended explicitly with RedirectableRequest#end)
if (this._currentResponse) {
// If no body was written to the original request, or the method was changed to GET,
// end the redirected request (without writing a body).
var bufferedWrites = this._bufferedWrites;
if (bufferedWrites.length === 0 || this._options.method === 'GET') {
request.end();
// The body of the original request must be added to the redirected request.
} else {
var i = 0;
(function writeNext() {
if (i < bufferedWrites.length) {
var bufferedWrite = bufferedWrites[i++];
request.write(bufferedWrite.data, bufferedWrite.encoding, writeNext);
} else {
request.end();
}
})();
}
}
};
// Processes a response from the current native request
RedirectableRequest.prototype._processResponse = function (response) {
// RFC7231§6.4: The 3xx (Redirection) class of status code indicates
// that further action needs to be taken by the user agent in order to
// fulfill the request. If a Location header field is provided,
// the user agent MAY automatically redirect its request to the URI
// referenced by the Location field value,
// even if the specific status code is not understood.
var location = response.headers.location;
if (location && this._options.followRedirects !== false &&
response.statusCode >= 300 && response.statusCode < 400) {
// RFC7231§6.4: A client SHOULD detect and intervene
// in cyclical redirections (i.e., "infinite" redirection loops).
if (++this._redirectCount > this._options.maxRedirects) {
return this.emit('error', new Error('Max redirects exceeded.'));
}
// RFC7231§6.4.7: The 307 (Temporary Redirect) status code indicates
// that the target resource resides temporarily under a different URI
// and the user agent MUST NOT change the request method
// if it performs an automatic redirection to that URI.
if (response.statusCode !== 307) {
// RFC7231§6.4: Automatic redirection needs to done with
// care for methods not known to be safe […],
// since the user might not wish to redirect an unsafe request.
if (!(this._options.method in safeMethods)) {
this._options.method = 'GET';
}
}
// Perform the redirected request
var redirectUrl = url.resolve(this._currentUrl, location);
Object.assign(this._options, url.parse(redirectUrl));
this._currentResponse = response;
this._performRequest();
} else {
// The response is not a redirect; return it as-is
response.responseUrl = this._currentUrl;
this.emit('response', response);
// Clean up
delete this._options;
delete this._bufferedWrites;
}
};
// Aborts the current native request
RedirectableRequest.prototype.abort = function () {
this._currentRequest.abort();
};
// Flushes the headers of the current native request
RedirectableRequest.prototype.flushHeaders = function () {
this._currentRequest.flushHeaders();
};
// Sets the noDelay option of the current native request
RedirectableRequest.prototype.setNoDelay = function (noDelay) {
this._currentRequest.setNoDelay(noDelay);
};
// Sets the socketKeepAlive option of the current native request
RedirectableRequest.prototype.setSocketKeepAlive = function (enable, initialDelay) {
this._currentRequest.setSocketKeepAlive(enable, initialDelay);
};
// Sets the timeout option of the current native request
RedirectableRequest.prototype.setTimeout = function (timeout, callback) {
this._currentRequest.setTimeout(timeout, callback);
};
// Writes buffered data to the current native request
RedirectableRequest.prototype._write = function (data, encoding, callback) {
this._currentRequest.write(data, encoding, callback);
this._bufferedWrites.push({data: data, encoding: encoding});
};
// Ends the current native request
RedirectableRequest.prototype.end = function (data, encoding, callback) {
this._currentRequest.end(data, encoding, callback);
if (data) {
this._bufferedWrites.push({data: data, encoding: encoding});
}
};
// Export a redirecting wrapper for each native protocol
Object.keys(nativeProtocols).forEach(function (protocol) {
var scheme = schemes[protocol] = protocol.substr(0, protocol.length - 1);
var nativeProtocol = nativeProtocols[protocol];
var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol);
// Executes an HTTP request, following redirects
wrappedProtocol.request = function (options, callback) {
if (typeof options === 'string') {
options = url.parse(options);
options.maxRedirects = exports.maxRedirects;
} else {
options = Object.assign({
maxRedirects: exports.maxRedirects,
protocol: protocol
}, options);
}
assert.equal(options.protocol, protocol, 'protocol mismatch');
return new RedirectableRequest(options, callback);
};
// Executes a GET request, following redirects
wrappedProtocol.get = function (options, callback) {
var request = wrappedProtocol.request(options, callback);
request.end();
return request;
};
});