--- /dev/null
+/*
+ * follow-redirects-iotjs
+ *
+ * Copyright (c) 2017 Samsung Electronics Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+
+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);
+['aborted', 'close', 'error', 'finish', '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;
+
+ // Create the native request
+ var nativeProtocol = nativeProtocols[protocol];
+ var request = this._currentRequest =
+ nativeProtocol.request(this._options, this._onNativeResponse);
+
+ //We know protocol will already be set here from wrappedProtocol.request
+ var protocol = this._options.protocol;
+ var host = this._options.host = this._options.hostname
+ || this._options.host || '127.0.0.1';
+ var path = this._options.path || '/';
+
+ //Set Port based on protocol
+ var port = 443;
+ if (protocol == 'https:') {
+ port = this._options.port = this._options.port || 443;
+ } else {
+ port = this._options.port = this._options.port || 80;
+ }
+ this._currentUrl = protocol + '//' + host + ':' + port + path;
+
+ // Set up event handlers
+ request._redirectable = this;
+ for (var event in eventHandlers) {
+ /* istanbul ignore else */
+ if (event) {
+ request.on(event, eventHandlers[event]);
+ }
+ }
+
+ // End a redirected request
+ // (The first request must be ended explicitly with RedirectableRequest#end)
+ if (this._isRedirect) {
+ // If the request doesn't have en entity, end directly.
+ var bufferedWrites = this._bufferedWrites;
+ if (bufferedWrites.length === 0) {
+ request.end();
+ // Otherwise, write the request entity and end afterwards.
+ } else {
+ var i = 0;
+ (function writeNext() {
+ if (i < bufferedWrites.length) {
+ var bufferedWrite = bufferedWrites[i++];
+ request.write(bufferedWrite.data, 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 || 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: 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.
+ // 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.
+ var header;
+ var headers = this._options.headers;
+ if (response.statusCode !== 307 && !(this._options.method in safeMethods)) {
+ this._options.method = 'GET';
+ // Drop a possible entity and headers related to it
+ this._bufferedWrites = [];
+ for (header in headers) {
+ if (/^content-/i.test(header)) {
+ delete headers[header];
+ }
+ }
+ }
+
+ //Resolve the (possibly relative) URL wrt the Base url
+ this._options = urlResolve(this._options, this._currentUrl, location);
+ this._isRedirect = true;
+
+ // Change the Host header, as the redirect might lead to a different host
+ if (this._isRedirect) {
+ for (header in headers) {
+ if (/^host$/i.test(header)) {
+ headers[header] = this._options.host;
+ }
+ }
+ }
+
+ // Perform the redirected request
+ 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() {
+ assert.equal(this._options.protocol, 'https:',
+ 'Abort only supported for https');
+ this._currentRequest.abort();
+};
+
+// 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, callback) {
+ this._currentRequest.write(data, callback);
+ this._bufferedWrites.push({
+ data: data
+ });
+};
+
+// Ends the current native request
+RedirectableRequest.prototype.end = function(data, callback) {
+ this._currentRequest.end(data, callback);
+ if (data) {
+ this._bufferedWrites.push({
+ data: data
+ });
+ }
+};
+
+// 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 (!options) {
+ options = {};
+ }
+ var optionsCopy = JSON.parse(JSON.stringify(options));
+ if (!optionsCopy.maxRedirects) {
+ optionsCopy.maxRedirects = exports.maxRedirects;
+ }
+
+ if (optionsCopy.protocol) {
+ assert.equal(optionsCopy.protocol, protocol, 'protocol mismatch');
+ } else {
+ optionsCopy.protocol = protocol;
+ }
+
+ return new RedirectableRequest(optionsCopy, callback);
+ };
+
+ // Executes a GET request, following redirects
+ wrappedProtocol.get = function(options, callback) {
+ var request = wrappedProtocol.request(options, callback);
+ request.end();
+ return request;
+ };
+});
+
+
+// ------------ Functions for resolving the redirect URL ---------------
+// RFC3986§5.2: an algorithm for converting a URI reference
+// that might be relative to a given base URI into the parsed components
+// of the reference's target. The components can then be recomposed
+urlResolve = function(options, currentUrl, location){
+ var baseUrl = urlParse(currentUrl);
+ var redirUrl = urlParse(location);
+ var targetUrl = urlTransform(baseUrl, redirUrl);
+ var host = targetUrl.authority;
+
+ // RFC3986§3.2.3: The port subcomponent of authority is designated by an
+ // optional port number in decimal following the host and delimited from it
+ // by a single colon (":") character.
+ delete options.port;
+ if (targetUrl.authority) {
+ var lastColon = targetUrl.authority.lastIndexOf(":");
+ if (lastColon != -1) {
+ var port = parseInt(targetUrl.authority.substring(lastColon+1));
+ if (!isNaN(port)) {
+ host = targetUrl.authority.substring(0, lastColon);
+ // RFC3986§7.2: Applications should prevent dereference of a URI that
+ // specifies a TCP port number within the "well-known port" range
+ // (0 - 1023)
+ if (port > 1023) {
+ options.port = port;
+ }
+ }
+ }
+ }
+ options.host = host;
+
+ // RFC3968§5.3 Parsed URI components can be recomposed to obtain the
+ // corresponding URI reference string...
+ if (!targetUrl.query) {
+ targetUrl.query='';
+ } else {
+ targetUrl.query = '?' + targetUrl.query;
+ }
+
+ if (!targetUrl.fragment) {
+ targetUrl.fragment='';
+ } else {
+ targetUrl.fragment = '#' + targetUrl.fragment;
+ }
+ options.path = targetUrl.path + targetUrl.query + targetUrl.fragment;
+ if (targetUrl.scheme != 'http:' && targetUrl.scheme != 'https:') {
+ console.log('Incorrect scheme in the request. Defaulting to base scheme');
+ options.protocol = baseUrl.scheme;
+ } else {
+ options.protocol = targetUrl.scheme;
+ }
+ return options;
+};
+// RFC3968§5.2.2 For each URI reference (R), the following pseudocode describes
+// an algorithm for transforming R into its target URI (T)
+urlTransform = function(base, redir) {
+ var trans = {};
+ if (redir.scheme == base.scheme) {
+ redir.scheme = null;
+ }
+
+ if (redir.scheme) {
+ trans.scheme = redir.scheme;
+ trans.authority = redir.authority;
+ trans.path = urlRemoveDot(redir.path);
+ trans.query = redir.query;
+ } else {
+ if (redir.authority) {
+ trans.authority = redir.authority;
+ trans.path = urlRemoveDot(redir.path);
+ trans.query = redir.query;
+ } else {
+ if (redir.path == "") {
+ trans.path = base.path;
+ if (redir.query) {
+ trans.query = redir.query;
+ } else {
+ trans.query = base.query;
+ }
+ } else {
+ if (redir.path.charAt(0) == "/") {
+ trans.path = urlRemoveDot(redir.path);
+ } else {
+ trans.path = urlMerge(base.path, redir.path);
+ trans.path = urlRemoveDot(trans.path);
+ }
+ trans.query = redir.query;
+ }
+ trans.authority = base.authority;
+ }
+ trans.scheme = base.scheme;
+ }
+
+ trans.fragment = redir.fragment;
+ return trans;
+}
+// RFC3968§Appendix B The following line is the regular expression for
+// breaking-down a well-formed URI reference into its components.
+// ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+// 12 3 4 5 6 7 8 9
+urlParse = function(location){
+ var redirOptions = {};
+ var urlRegex = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?/g;
+ var m = urlRegex.exec(location);
+
+ redirOptions.scheme = m[1];
+ redirOptions.authority = m[4];
+ redirOptions.path = m[5];
+ redirOptions.query = m[7];
+ redirOptions.fragment = m[9];
+
+ return redirOptions;
+};
+
+// RFC3968§5.2.4 routine for
+// interpreting and removing the special "." and ".." complete path
+// segments from a referenced path. This is done after the path is
+// extracted from a reference, whether or not the path was relative, in
+// order to remove any invalid or extraneous dot-segments prior to
+// forming the target URI
+urlRemoveDot = function(path){
+ var rval = '';
+ if (path.indexOf('/') === 0) {
+ rval = '/';
+ }
+
+ var input = path.split('/');
+ var output = [];
+ while (input.length > 0) {
+ if (input[0] === '.' || (input[0] === '' && input.length > 1)) {
+ input.shift();
+ continue;
+ }
+ if (input[0] === '..') {
+ input.shift();
+ if (output.length > 0 && output[output.length - 1] !== '..') {
+ output.pop();
+ } else {
+ // leading relative URL '..'
+ // THIS DISOBEYS RFC but is common in browsers
+ output.push('..');
+ }
+ continue;
+ }
+ output.push(input.shift());
+ }
+ return rval + output.join('/');
+};
+
+// RFC3968§5.2.3 The pseudocode above refers to a "merge" routine for merging a
+// relative-path reference with the path of the base URI.
+urlMerge = function(basePath, redirPath){
+ if (!basePath) {
+ return "/"+redirPath;
+ } else {
+ var lastSlash = basePath.lastIndexOf("/");
+ if (lastSlash == -1) {
+ return redirPath;
+ } else {
+ return basePath.substring(0, lastSlash+1) + redirPath;
+ }
+ }
+}