Added the follow-redirects module to iotjs_modules folder 37/138137/4
authorAkhil Kedia <akhil.kedia@samsung.com>
Tue, 11 Jul 2017 07:05:43 +0000 (16:05 +0900)
committerAkhil Kedia <akhil.kedia@samsung.com>
Mon, 17 Jul 2017 05:23:42 +0000 (14:23 +0900)
This has 2 benefits -
1. Prevents the source code from being lost in case of any issues with my PC
2. iotjs by default looks for the iotjs_modules folder in the present working directory,
so keeping the folder here makes testing easier.

[Update] Changed the readme sample code to include the bit.ly header

Change-Id: Ic51993a82199517570dbc7ecd7506e68375a3078
Signed-off-by: Akhil Kedia <akhil.kedia@samsung.com>
iotjs_modules/follow-redirects-iotjs/README.md [new file with mode: 0644]
iotjs_modules/follow-redirects-iotjs/index.js [new file with mode: 0644]
iotjs_modules/follow-redirects-iotjs/package.json [new file with mode: 0644]

diff --git a/iotjs_modules/follow-redirects-iotjs/README.md b/iotjs_modules/follow-redirects-iotjs/README.md
new file mode 100644 (file)
index 0000000..6454660
--- /dev/null
@@ -0,0 +1,93 @@
+## Follow Redirects
+
+Drop-in replacement for [IoT.js](http://iotjs.net/)' `http` and `https` that automatically follows redirects.
+
+`follow-redirects` provides [request](https://github.com/Samsung/iotjs/blob/master/docs/api/IoT.js-API-HTTP.md#httprequestoptions-callback) and [get](https://github.com/Samsung/iotjs/blob/master/docs/api/IoT.js-API-HTTP.md#httpgetoptions-callback)  methods that behave identically to those found on the native IoT.js' [http](https://github.com/Samsung/iotjs/blob/master/docs/api/IoT.js-API-HTTP.md) and [https](https://github.com/Samsung/iotjs/blob/master/docs/api/IoT.js-API-HTTPS.md) modules, with the exception that they will seamlessly follow redirects.
+
+```javascript
+var http = require('follow-redirects-iotjs').http;
+var https = require('follow-redirects-iotjs').https;
+
+options = {
+  host: 'bit.ly',
+  path: '/900913',
+  followRedirects: true,
+  headers: {'host': 'bit.ly'}
+};
+
+https.get(options, function (response) {
+  console.log('Got Response Callback with statusCode '+response.statusCode);
+  response.on('data', function (chunk) {
+    //Omitting console.log(chunk) as Google's response is not UTF-8
+    console.log('Got Data Callback!');
+  });
+}).on('error', function (err) {
+  console.error(err);
+});
+```
+
+You can inspect the final redirected URL through the `responseUrl` property on the `response`.
+If no redirection happened, `responseUrl` is the original request URL.
+
+```javascript
+https.request({
+  host: 'bitly.com',
+  path: '/UHfDGO',
+  followRedirects: true,
+  headers: {'host': 'bit.ly'}
+}, function (response) {
+  console.log(response.responseUrl);
+  // 'http://duckduckgo.com/robots.txt'
+});
+```
+
+## Options
+### Global options
+Global options are set directly on the `follow-redirects` module:
+
+```javascript
+var followRedirects = require('follow-redirects');
+followRedirects.maxRedirects = 10;
+```
+
+The following global options are supported:
+
+- `maxRedirects` (default: `21`) – sets the maximum number of allowed redirects; if exceeded, an error will be emitted.
+
+
+### Per-request options
+Per-request options are set by passing an `options` object:
+
+```javascript
+var url = require('url');
+var followRedirects = require('follow-redirects');
+
+options = {
+  host: 'bit.ly',
+  path: '/900913',
+  followRedirects: true,
+  maxRedirects: 10,
+  headers: {'host': 'bit.ly'}
+};
+http.request(options);
+```
+
+In addition to the [standard HTTP](https://github.com/Samsung/iotjs/blob/master/docs/api/IoT.js-API-HTTP.md) and [HTTPS options](https://github.com/Samsung/iotjs/blob/master/docs/api/IoT.js-API-HTTPS.md),
+the following per-request options are supported:
+- `followRedirects` (default: `true`) – whether redirects should be followed.
+- `maxRedirects` (default: `21`) – sets the maximum number of allowed redirects; if exceeded, an error will be emitted.
+
+## Contributing
+
+Pull Requests are always welcome. Please [file an issue](https://github.com/akhilkedia/iotjs-follow-redirects/issues) detailing your proposal before you invest your valuable time.
+
+## Authors
+
+- Akhil Kedia (akhil.kedia@samsung.com)
+- Olivier Lalonde (olalonde@gmail.com)
+- James Talmage (james@talmage.io)
+- [Ruben Verborgh](https://ruben.verborgh.org/)
+
+## License
+
+MIT: [http://olalonde.mit-license.org](http://olalonde.mit-license.org)
diff --git a/iotjs_modules/follow-redirects-iotjs/index.js b/iotjs_modules/follow-redirects-iotjs/index.js
new file mode 100644 (file)
index 0000000..7fd5134
--- /dev/null
@@ -0,0 +1,425 @@
+/*
+ * 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;
+    }
+  }
+}
diff --git a/iotjs_modules/follow-redirects-iotjs/package.json b/iotjs_modules/follow-redirects-iotjs/package.json
new file mode 100644 (file)
index 0000000..b6cb374
--- /dev/null
@@ -0,0 +1,35 @@
+{
+  "name": "follow-redirects-iotjs",
+  "version": "1.0.0",
+  "description": "HTTP and HTTPS modules for IoT.js that follow redirects.",
+  "main": "index.js",
+  "engines": {
+    "iotjs": ">=1.0"
+  },
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git@github.com:akhilkedia/iotjs-follow-redirects"
+  },
+  "homepage": "https://github.com/akhilkedia/iotjs-follow-redirects",
+  "bugs": {
+    "url": "https://github.com/akhilkedia/iotjs-follow-redirects"
+  },
+  "keywords": [
+    "http",
+    "https",
+    "url",
+    "redirect",
+    "client",
+    "location",
+    "utility"
+  ],
+  "author": {
+    "name": "Akhil Kedia",
+    "email": "akhil.kedia@samsung.com",
+    "url": "https://github.com/akhilkedia"
+  },
+  "license": "MIT"
+}