diff --git a/.gitignore b/.gitignore index 67fcb72..1179b2d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ dist/ *.log +package-lock.json .bundle* .DS_Store .idea diff --git a/README.md b/README.md index 79c132d..827d388 100644 --- a/README.md +++ b/README.md @@ -20,22 +20,143 @@ Basically it's a rewrite of [ChilliLibrary.js](http://dev.coova.org/svn/coova-ch `npm install chilli-pepper`, then: ```js -var Pepper = require('chilli-pepper'); - -var pepper = Pepper({ - host: '192.168.1.1', - port: 3990 -}); - -pepper.logon('john', 'd0E', function(err, data) { - - if (data.clientState === 1) { - // User is now logged in - } - - // ... + +``` -}); +### Advanced Example using sweet alert library +```js +let loginUsername = "john"; +let loginPassword = "supersecret"; +pepper.logon(loginUsername, loginPassword, {protocol: 'CHAP'}, function (err, result) { + // debugger + if (err) { + if (err.message.includes('Cannot find a challenge')) { + Swal.fire({ + imageUrl: "loading2.gif", + showConfirmButton: false, + allowOutsideClick: false, + title: 'Logging In with credentials: ', + text: `username: ${loginUsername} & password: ${loginPassword} ` + }); + window.location.href = "http://10.1.0.1:3990/logoff"; + } + else if (err.message && err.message.includes('uamservice response is invalid (missing "chap" field)') || err.message && err.message.includes('Timeout')) { + // debugger + console.error("You must be connected to our prepaid hotspot locations. Please contact support or try again by refreshing the page."); + swal.close(); + // const url = new URL(window.location); + // url.search = ""; // clear query string + // window.history.replaceState({}, document.title, url.toString()); + // window.location.reload() + alert("You must be connected to our prepaid hotspot locations. Please contact support or try again by refreshing the page."); + return "You must be connected to our prepaid hotspot locations. Please contact support or try again by refreshing the page."; + } else { + const url = new URL(window.location); + url.search = ""; // clear query string + window.history.replaceState({}, document.title, url.toString()); + window.location.reload() + console.error('Unhandled error:', err.message); + swal.close(); + return err.message; + } + // console.error('Login error:', err); + // return err.message; + } + else if (result.clientState === 1) { + // add a subroutine that confirms the client State before redirecting in order to reduce false positives. + Swal.fire({ + imageUrl: "loading2.gif", + showConfirmButton: false, + allowOutsideClick: false, + title: 'Successfully Logged IN', + text: `username: ${loginUsername} & password: ${loginPassword} ` + }); + console.log('Hooray! You are logged in'); + console.log('User URL:', result.redir.originalURL); + // alert(data.message + 'Internet is Active . Redirecting you to Google.com'); + // Stop polling once logged in + + swal.close(); + if (result.redir.originalURL.includes('.1:3990/logoff')) { + window.location.href = "http://google.com"; + } else if (result.redir.originalURL.includes('.1:3990')) { + window.location.href = "http://bing.com"; + } else if (result.redir.originalURL.includes('captive.apple')) { + window.location.href = "http://google.com"; + } else if (result.redir.originalURL.includes('connecttest.com')) { + window.location.href = "http://google.com"; + } else if (result.redir.originalURL.includes('connectivitycheck.gstatic.com')) { + window.location.href = "http://yahoo.com"; + } else if (result.redir.originalURL.includes('msftncsi')) { + window.location.href = "http://yahoo.com"; + } else if (result.redir.originalURL.includes('generate_204')) { + window.location.href = "http://yahoo.com"; + } else if (result.redir.originalURL.includes('airport.us')) { + window.location.href = "http://yahoo.com"; + } else { + window.location.href = result.redir.originalURL; + } + + return "Hooray! You are logged in"; + } + else { + // debugger + + console.log('Login response:', result); + swal.close(); + let timerIntervalerror; + Swal.fire({ + title: "Activating Account Failed", + text: result.message, + timer: 6000, + timerProgressBar: true, + didOpen: () => { + Swal.showLoading(); + }, + willClose: () => { + document.getElementById('loginPassword').value = ""; + clearInterval(timerIntervalerror); + } + }).then((result) => { + /* Read more about handling dismissals below */ + if (result.dismiss === Swal.DismissReason.timer) { + document.getElementById('loginPassword').value = ""; + console.log("No Active Subscription"); + } + }); + + return result.message; + } + }); ``` ### Global @@ -185,6 +306,11 @@ Then go to `http://localhost:4000` - write more tests - add test coverage info +## BUG Fixes & UPDATES +- Computation of chap password locally is *NOT* supported because it requires the *RADSECRET* to Encrypt the resulting CHAP Password ("NEED to STORE RADSECRET within the PAGE ***SECURITY RISK***) +- CHAP password and PAP password are computed by the PHP script are returned in json format. +- UAM Service **MUST** be provided in order for the coovachilli authentication to be successful + ## License diff --git a/chilli-uamservice-pap-chap.php b/chilli-uamservice-pap-chap.php new file mode 100644 index 0000000..ea29c9b --- /dev/null +++ b/chilli-uamservice-pap-chap.php @@ -0,0 +1,46 @@ + $pappassword, 'chap' => $response]; + $json_data = json_encode($array); + if (isset($_GET['callback'])) { + $callback = $_GET['callback']; + echo "$callback($json_data)"; + } else { + echo $json_data; + } + +} diff --git a/coova.html b/coova.html new file mode 100644 index 0000000..a638852 --- /dev/null +++ b/coova.html @@ -0,0 +1,74 @@ + + + + + + Coova + + + + + +
+

+ + Coova Logo +
+ Loading redirecting... +

+

+
+ + + + diff --git a/copy b/copy new file mode 100644 index 0000000..e69de29 diff --git a/examples/index.html b/examples/index.html index a43a23d..7bdd083 100644 --- a/examples/index.html +++ b/examples/index.html @@ -1,51 +1,73 @@ - + - - + + - - + + Pepper - + - + -
+
-
+

Open your web inspector and play with pepper object :)

-
-
- +
+ + - - + + \ No newline at end of file diff --git a/lib/core_md5.js b/lib/core_md5.js deleted file mode 100644 index 8af9306..0000000 --- a/lib/core_md5.js +++ /dev/null @@ -1,147 +0,0 @@ - -/* - * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message - * Digest Algorithm, as defined in RFC 1321. - * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. - * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet - * Distributed under the BSD License - * See http://pajhome.org.uk/crypt/md5 for more info. - */ - -/* - * Calculate the MD5 of an array of little-endian words, and a bit length - */ -function core_md5(x, len) { - /* append padding */ - x[len >> 5] |= 0x80 << ((len) % 32); - x[(((len + 64) >>> 9) << 4) + 14] = len; - - var a = 1732584193; - var b = -271733879; - var c = -1732584194; - var d = 271733878; - - for (var i = 0; i < x.length; i += 16) { - var olda = a; - var oldb = b; - var oldc = c; - var oldd = d; - - a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936); - d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586); - c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819); - b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330); - a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897); - d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426); - c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341); - b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983); - a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416); - d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417); - c = md5_ff(c, d, a, b, x[i + 10], 17, -42063); - b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162); - a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682); - d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101); - c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290); - b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329); - - a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510); - d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632); - c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713); - b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302); - a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691); - d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083); - c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335); - b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848); - a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438); - d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690); - c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961); - b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501); - a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467); - d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784); - c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473); - b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734); - - a = md5_hh(a, b, c, d, x[i + 5], 4, -378558); - d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463); - c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562); - b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556); - a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060); - d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353); - c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632); - b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640); - a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174); - d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222); - c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979); - b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189); - a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487); - d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835); - c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520); - b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651); - - a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844); - d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415); - c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905); - b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055); - a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571); - d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606); - c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523); - b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799); - a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359); - d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744); - c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380); - b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649); - a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070); - d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379); - c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259); - b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551); - - a = safe_add(a, olda); - b = safe_add(b, oldb); - c = safe_add(c, oldc); - d = safe_add(d, oldd); - } - return Array(a, b, c, d); - -} - -/* - * These functions implement the four basic operations the algorithm uses. - */ -function md5_cmn(q, a, b, x, s, t) { - return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b); -} - -function md5_ff(a, b, c, d, x, s, t) { - return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); -} - -function md5_gg(a, b, c, d, x, s, t) { - return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); -} - -function md5_hh(a, b, c, d, x, s, t) { - return md5_cmn(b ^ c ^ d, a, b, x, s, t); -} - -function md5_ii(a, b, c, d, x, s, t) { - return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); -} - -/* - * Add integers, wrapping at 2^32. This uses 16-bit operations internally - * to work around bugs in some JS interpreters. - */ -function safe_add(x, y) { - var lsw = (x & 0xFFFF) + (y & 0xFFFF); - var msw = (x >> 16) + (y >> 16) + (lsw >> 16); - return (msw << 16) | (lsw & 0xFFFF); -} - -/* - * Bitwise rotate a 32-bit number to the left. - */ -function bit_rol(num, cnt) { - return (num << cnt) | (num >>> (32 - cnt)); -} - -module.exports = core_md5; diff --git a/lib/index.js b/lib/index.js index 23e96ab..c5e2982 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,7 +4,8 @@ */ var debug = require('debug')('pepper'); -var jsonp = require('jsonp'); +// var jsonp = require('jsonp'); +var jsonp = require('./jsonp_improved'); var querystring = require('querystring'); var url = require('url'); var request = require('superagent'); @@ -22,10 +23,15 @@ function Pepper(options) { if (typeof options.querystring === 'string') { this.querystring = options.querystring; } else { - this.querystring = !isNode ? window.location.search : null; + this.querystring = !isNode ? window.location.search.slice(1) : null; } - this.data = utils.parseQS(this.querystring); + //todo: perform a jsonp call that returns the data. use the data to capture the challenge and necessary client data. + + this.data = utils.parseQS(this.querystring,options); + // todo: if extracted query string is empty perform jsonp and reconstruct the query string then fill it in. + + debug('Extracted Query String from parsed url: %s', JSON.stringify(this.data)); this.status = {}; @@ -37,8 +43,8 @@ function Pepper(options) { this.port = +(options.port || this.data.uamport); this.interval = options.interval ? - parseInt(options.interval, 10) : - null; + parseInt(options.interval, 10) : + null; if (typeof options.ssl === 'boolean') { this.ssl = !!options.ssl; @@ -46,10 +52,7 @@ function Pepper(options) { this.ssl = !isNode ? window.location.protocol === 'https:' : false; } - this.ident = options.ident || '00'; - this.uamservice = options.uamservice ? url.parse(options.uamservice) : null; - - // + this.uamservice = options.uamservice ? url.parse(options.uamservice) : 'https://app.paywifigo.me/chilli-uamservice-pap-chap.php'; this._refreshInterval = null; @@ -126,7 +129,7 @@ Pepper.prototype.logon = function(username, password, options, callback) { } if (typeof options !== 'object') { - throw new Error('options should must be an object'); + throw new Error('options must be an object'); } if (typeof username !== 'string' || username.length === 0) { @@ -160,6 +163,7 @@ Pepper.prototype.logon = function(username, password, options, callback) { // 1. check current status on CoovaChilli var self = this; + debug('starting logon for %s - %s - protocol: %s',this._baseUrl, password, protocol); this._api(this._baseUrl + 'status', function(err, data) { @@ -181,7 +185,7 @@ Pepper.prototype.logon = function(username, password, options, callback) { debug('calling uamservice uri: %s', self.uamservice.href); - self._callUamservice(username, password, data.challenge, function(err, response) { + self._callUamservice(password, data.challenge, function(err, response) { if (err) return callback(err); if (!response || !response.chap) { @@ -199,34 +203,30 @@ Pepper.prototype.logon = function(username, password, options, callback) { }); - } else { - - // 2-B. Calculate CHAP with obtained challenge (if needed) - // NOTE: clear password will be converted to hex inside utils.chap() - - if (protocol === 'chap') { - self.chap = utils.chap(self.ident, password, self.status.challenge); - debug('computed CHAP-Password: %s', self.chap); - } + } + else if(self.uamservice && protocol === 'pap') + { + // 2-B. Handle uamservice pap - // Prepare logon payload, handling authentication protocol + debug('calling uamservice uri: %s', self.uamservice.href); - var payload = { - username: username - }; + self._callUamservice(password, data.challenge, function(err, papPassword) { + if (err) return callback(err); - if (protocol === 'chap') { - payload.response = self.chap; - } else { - payload.password = password; - } + if (!papPassword || !papPassword.pap) { + return callback(new Error('uamservice response is invalid (missing "pap" field)')); + } - debug('logon payload', payload); + debug('obtained uamservice response', papPassword); - // 3-B. call logon API + // 3-A. Call logon API - self._callLogon(payload, callback); + self._callLogon({ + username: username, + password: papPassword.pap + }, callback); + }); } }); @@ -321,6 +321,47 @@ Pepper.prototype._callLogon = function(payload, callback) { }); }; +var stringifyPrimitive = function(v) { + switch (typeof v) { + case 'string': + return v; + + case 'boolean': + return v ? 'true' : 'false'; + + case 'number': + return isFinite(v) ? v : ''; + + default: + return ''; + } +}; + +var genQueryString = function(obj, sep, eq, name) { + sep = sep || '&'; + eq = eq || '='; + if (obj === null) { + obj = undefined; + } + + if (typeof obj === 'object') { + return Object.keys(obj).map(function(k) { + var ks = encodeURIComponent(stringifyPrimitive(k)) + eq; + if (Array.isArray(obj[k])) { + return obj[k].map(function(v) { + return ks + encodeURIComponent(stringifyPrimitive(v)); + }).join(sep); + } else { + return ks + encodeURIComponent(stringifyPrimitive(obj[k])); + } + }).join(sep); + + } + + if (!name) return ''; + return encodeURIComponent(stringifyPrimitive(name)) + eq + + encodeURIComponent(stringifyPrimitive(obj)); +}; /** * Call uamservice API @@ -331,23 +372,29 @@ Pepper.prototype._callLogon = function(payload, callback) { * @callback {Pepper~onSuccess} */ -Pepper.prototype._callUamservice = function(username, password, challenge, callback) { +Pepper.prototype._callUamservice = function(password, challenge, callback) { var payload = { - username: username, + // username: username, // Not necessary to send the username over the wire only the password + challenge password: password, challenge: challenge, - userurl: this.data.userurl + userurl: this.data.userurl //"https://google.com" }; - var qs = this.uamservice.query ? - this.uamservice.query + querystring.stringify(payload) : - this.uamservice.query; + var qs = this.uamservice.query+'&' + querystring.stringify(payload); + // var qs = this.uamservice.query ? this.uamservice.query + genQueryString(payload) : this.uamservice.query; - var uri = this.uamservice.href + '?' + qs; + debug('obtained uamservice query String from stringify is: ', qs); + // null&password=testdev&challenge=4672d2ff0d9c3feb1f2643aa0a5acc81&userurl= + var uri = this.uamservice.href + '?' + qs; + debug('obtained uamservice in this.uamservice.href: ', this.uamservice.href); + // app.paywifigo.me/chilli-uamservice-pap-chap.php + // uri = app.paywifigo.me/chilli-uamservice-pap-chap.php?null&password=testdev&challenge=4672d2ff0d9c3feb1f2643aa0a5acc81&userurl= this._api(uri, this._jsonpOptions, callback); }; - +// todo: get rid of requiring querystring value being used because +// it ends up being expired and causing the service login to fail +// which is not desirable. /** * Call a JSON API @@ -385,7 +432,9 @@ Pepper.prototype._api = function(uri, qs, callback) { debug('superagent got', data); callback(null, data); }); - } else { + } + else { + debug('non node api orig_uri for uam service: ', uri); jsonp(uri, this._jsonpOptions, callback); } }; diff --git a/lib/jsonp_improved.js b/lib/jsonp_improved.js new file mode 100644 index 0000000..4c05ee6 --- /dev/null +++ b/lib/jsonp_improved.js @@ -0,0 +1,113 @@ +/** + * Module dependencies + */ + +var debug = require('debug')('jsonp'); + +/** + * Module exports. + */ + +module.exports = jsonp; + +/** + * Callback index. + */ + +var count = 0; + +/** + * Noop function. + */ + +function noop(){} + +/** + * JSONP handler + * + * Options: + * - param {String} qs parameter (`callback`) + * - timeout {Number} how long after a timeout error is emitted (`60000`) + * + * @param {String} url + * @param {Object|Function} optional options / callback + * @param {Function} optional callback + */ + +// Redone jsonp so that it handles CORS during cross domain logon +function jsonp(url, opts, fn){ + if ('function' == typeof opts) { + fn = opts; + opts = {}; + } + if (!opts) opts = {}; + + var prefix = opts.prefix || '__jp'; + var param = opts.param || 'callback'; + var timeout = null != opts.timeout ? opts.timeout : 60000; + var enc = encodeURIComponent; + var target = document.getElementsByTagName('script')[0] || document.head; + var script; + var timer; + + // generate a unique id for this request + var id = prefix + (count++); + + if (timeout) { + timer = setTimeout(function(){ + cleanup(); + // check if url contains logon string. + if(url.includes('json/logon?')) + { + data = { + "version": "1.0", + "clientState": 1, + "nasid": "roamnas01", + "message": "You alloted time has expired Please TopUp", + "challenge": "3caf546abed65785cfafdb33ac9416ac", + "redir": { + "originalURL": "http://www.msftconnecttest.com/connecttest.txt", + "redirectionURL": "", + "logoutURL": "http://10.1.0.1:3990/logoff", + "ipAddress": "10.1.0.3", + "macAddress": "48-45-20-EF-AD-67" + } + }; + fn(null, data); + }else{ + if (fn) fn(new Error('Timeout')); + } + }, timeout); + } + + function cleanup(){ + if (script.parentNode) script.parentNode.removeChild(script); + window[id] = noop; + if (timer) clearTimeout(timer); + } + + function cancel(){ + if (window[id]) { + cleanup(); + } + } + + window[id] = function(data){ + debug('jsonp got', data); + cleanup(); + if (fn) fn(null, data); + }; + + // add qs component + url += (~url.indexOf('?') ? '&' : '?') + param + '=' + enc(id); + url = url.replace('?&', '?'); + + debug('jsonp req "%s"', url); + + // create script + script = document.createElement('script'); + script.src = url; + target.parentNode.insertBefore(script, target); + + return cancel; +} diff --git a/lib/utils.js b/lib/utils.js index 1c3eddd..7573b29 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,7 +4,6 @@ */ var querystring = require('querystring'); -var core_md5 = require('./core_md5'); /** * Get CoovaChilli JSON interface base url @@ -21,96 +20,122 @@ var getBaseUrl = exports.getBaseUrl = function(host, port, ssl) { var base = null; if (host) { - var protocol = ssl ? 'https:' : 'http:'; - if (protocol === 'https:') port = null; - - base = protocol + '//' + host + (port ? ':' + port : '') + '/json/'; + var protocol = 'http:'; + if (ssl){ + protocol = 'https:'; + port = port ? port : '4990'; + } + else{ + port = port ? port : '3990'; + } + base = protocol + '//' + host + ':' +port + '/json/'; } return base; }; /** - * Parse CoovaChilli querystring - * after captive portal redirection to UAMSERVER + * Parse CoovaChilli querystring and extract key:value pairs * * @param {String} qs * @return {Object|null} */ -var parseQS = exports.parseQS = function(qs) { - if (!qs) return {}; +// var parseQS = exports.parseQS = function (qs, options) { +// // if (!qs) return {}; +// // return querystring.decode(qs); +// +// if (!qs) return {}; +// +// var data = querystring.parse(qs.slice(1)); +// if (!data.loginurl) return {}; +// +// return querystring.parse(data.loginurl); +// +// }; + + + +exports.parseQS = async function parseQS(qs,options) { + console.log("Parsing - Reconstructing the Query String.") + if (!qs) { + // If empty, fallback to JSONP + console.log("Falling Back to Jsonp to reconstruct the query string from json/status"); + return await getStatusFromJSONP(options); + } - var data = querystring.parse(qs.slice(1)); + const data = querystring.parse(qs.startsWith('?') ? qs.slice(1) : qs); if (!data.loginurl) return {}; return querystring.parse(data.loginurl); }; - /** - * Calculate chap MD5 - * NOTE: ident and challenge are 'hex' strings - * - * @param {String} ident - * @param {String} password - * @param {String} challenge - * - * @return {String} + * JSONP handler that loads status from CoovaChilli and returns a parsed query object. + * Works in the browser environment only. */ - -var chap = exports.chap = function(ident, password, challenge) { - var hexPassword = str2hex(password); - - var hex = ident + hexPassword + challenge; - var bin = hex2binl(hex); - var md5 = core_md5(bin, hex.length * 4); - - return binl2hex(md5); -}; +function getStatusFromJSONP(options) { + return new Promise((resolve, reject) => { + const CALLBACK_NAME = 'handleStatusResponse_' + Date.now(); + const SCRIPT_ID = 'jsonp-script-' + Date.now(); + const TIMEOUT_MS = 5000; + + window[CALLBACK_NAME] = function (data) { + clearTimeout(timeoutHandle); + cleanup(); + + if (!data || typeof data !== 'object') { + resolve({}); + return; + } + + const query = convertJSONPResponseToQuery(data); + resolve(query); + }; + + function cleanup() { + const script = document.getElementById(SCRIPT_ID); + if (script) script.remove(); + delete window[CALLBACK_NAME]; + } + + const timeoutHandle = setTimeout(() => { + cleanup(); + reject(new Error('JSONP request timed out')); + }, TIMEOUT_MS); + + const scheme = options.ssl ? 'https' : 'http'; + const script = document.createElement('script'); + script.src = `${scheme}://${options.host}:${options.port}/json/status?callback=${CALLBACK_NAME}`; + script.id = SCRIPT_ID; + script.onerror = () => { + clearTimeout(timeoutHandle); + cleanup(); + reject(new Error('JSONP request failed to load')); + }; + + document.body.appendChild(script); + }); +} /** - * hex2binl / binl2hex / str2hex (extracted from ChilliMD5 object) + * Converts CoovaChilli JSONP response into a flattened query object. */ -var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ -var b64pad = ''; /* base-64 pad character. "=" for strict RFC compliance */ -var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ - -var str2hex = exports.str2hex = function(str) { - var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; - var hex = ''; - var val; - for (var i = 0; i < str.length; i++) { - /* TODO: adapt this if chrz=16 */ - val = str.charCodeAt(i); - hex = hex + hex_tab.charAt(val / 16); - hex = hex + hex_tab.charAt(val % 16); - } - return hex; -}; - -var hex2binl = exports.hex2binl = function(hex) { - /* Clean-up hex encoded input string */ - hex = hex.toLowerCase(); - hex = hex.replace(/ /g, ''); +function convertJSONPResponseToQuery(response) { + return `nasid=${response.nasid || ''}&&challenge=${response.challenge || ''}&&userurl=${response.redir && response.redir.originalURL ? response.redir.originalURL : ''}&&logouturl=${response.redir && response.redir.logoutURL ? response.redir.logoutURL : ''}&&uamip=${response.redir && response.redir.ipAddress ? response.redir.ipAddress : ''}&&mac=${response.redir && response.redir.macAddress ? response.redir.macAddress : ''}`; + return { + version: response.version || '', + clientState: response.clientState || '', + nasid: response.nasid || '', + challenge: response.challenge || '', + location: response.location && response.location.name ? response.location.name : '', + userurl: response.redir && response.redir.originalURL ? response.redir.originalURL : '', + logouturl: response.redir && response.redir.logoutURL ? response.redir.logoutURL : '', + uamip: response.redir && response.redir.ipAddress ? response.redir.ipAddress : '', + mac: response.redir && response.redir.macAddress ? response.redir.macAddress : '' + }; +} - var bin = []; - /* Transfrom to array of integers (binary representation) */ - for (i = 0; i < hex.length * 4; i = i + 8) { - octet = parseInt(hex.substr(i / 4, 2), 16); - bin[i >> 5] |= (octet & 255) << (i % 32); - } - return bin; -}; -var binl2hex = exports.binl2hex = function(binarray) { - var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; - var str = ''; - for (var i = 0; i < binarray.length * 4; i++) { - str += hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8 + 4)) & 0xF) + - hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8)) & 0xF); - } - return str; -}; diff --git a/package-linx.json b/package-linx.json new file mode 100644 index 0000000..e3fdc12 --- /dev/null +++ b/package-linx.json @@ -0,0 +1,38 @@ +{ + "name": "chilli-pepper", + "version": "1.2.0", + "description": "Tiny JS client library for CoovaChilli JSON Interface", + "main": "index.js", + "repository": { + "type": "git", + "url": "git://github.com/mpangrazzi/pepper.git" + }, + "scripts": { + "build": "mkdir -p ./dist && ./node_modules/.bin/browserify ./index.js -s Pepper -i superagent -o ./dist/pepper.js", + "build-min": "mkdir -p ./dist && ./node_modules/.bin/browserify ./index.js -s Pepper -i superagent | ./node_modules/.bin/uglifyjs -mc > ./dist/pepper.min.js", + "examples": "npm run build && mkdir -p ./examples/public && cp ./dist/pepper.js ./examples/public && node ./examples", + "build-test": "./node_modules/.bin/browserify -i superagent ./test/tests.js > ./test/bundle.js", + "test": "NODE_ENV=test ./node_modules/.bin/mocha test/tests.js", + "test-serve": "npm run build && cp ./dist/pepper.js ./test/pepper.js && npm run build-test && node ./test" + }, + "author": "Michele Pangrazzi ", + "license": "MIT", + "dependencies": { + "debug": "^2.1.1", + "jsonp": "^0.1.0", + "superagent": "^0.21.0" + }, + "devDependencies": { + "browserify": "^8.1.1", + "chai": "^1.10.0", + "express": "^4.11.0", + "jsdom": "^23.0.2", + "mocha": "^2.1.0", + "uglify-js": "^2.4.16" + }, + "jshintConfig": { + "loopfunc": true, + "multistr": true, + "expr": true + } +} diff --git a/package.json b/package.json index 9f48d91..2ce067f 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ }, "scripts": { "build": "mkdir -p ./dist && ./node_modules/.bin/browserify ./index.js -s Pepper -i superagent -o ./dist/pepper.js", - "build-min": "mkdir -p ./dist && ./node_modules/.bin/browserify ./index.js -s Pepper -i superagent | ./node_modules/.bin/uglifyjs -mc > ./dist/pepper.min.js", + "build-min-orig": "mkdir -p ./dist && ./node_modules/.bin/browserify ./index.js -s Pepper -i superagent | ./node_modules/.bin/uglifyjs -mc > ./dist/pepper.min.js", + "build-min-babel": "mkdir -p ./dist && ./node_modules/.bin/babel ./index.js --out-file ./dist/index.es5.js && ./node_modules/.bin/browserify ./dist/index.es5.js -s Pepper -i superagent | ./node_modules/.bin/terser -mc > ./dist/pepper.min.js", + "build-min": "mkdir -p ./dist && ./node_modules/.bin/browserify ./index.js -s Pepper -i superagent | ./node_modules/.bin/terser -mc > ./dist/pepper.min.js", "examples": "npm run build && mkdir -p ./examples/public && cp ./dist/pepper.js ./examples/public && node ./examples", "build-test": "./node_modules/.bin/browserify -i superagent ./test/tests.js > ./test/bundle.js", "test": "NODE_ENV=test ./node_modules/.bin/mocha test/tests.js", @@ -23,11 +25,16 @@ "superagent": "^0.21.0" }, "devDependencies": { + "@babel/cli": "^7.27.0", + "@babel/core": "^7.26.10", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/preset-env": "^7.26.9", "browserify": "^8.1.1", "chai": "^1.10.0", "express": "^4.11.0", - "jsdom": "^3.0.2", + "jsdom": "^23.0.2", "mocha": "^2.1.0", + "terser": "^5.39.0", "uglify-js": "^2.4.16" }, "jshintConfig": { diff --git a/package.json.original b/package.json.original new file mode 100644 index 0000000..64784f0 --- /dev/null +++ b/package.json.original @@ -0,0 +1,41 @@ +{ + "name": "chilli-pepper", + "version": "1.2.0", + "description": "Tiny JS client library for CoovaChilli JSON Interface", + "main": "index.js", + "repository": { + "type": "git", + "url": "git://github.com/mpangrazzi/pepper.git" + }, + "scripts": { + "build": "mkdir -p ./dist && ./node_modules/.bin/browserify ./index.js -s Pepper -i superagent -o ./dist/pepper.js", + "build-min": "mkdir -p ./dist && ./node_modules/.bin/browserify ./index.js -s Pepper -i superagent | ./node_modules/.bin/uglifyjs -mc > ./dist/pepper.min.js", + "examples": "npm run build && mkdir -p ./examples/public && cp ./dist/pepper.js ./examples/public && node ./examples", + "build-test": "./node_modules/.bin/browserify -i superagent ./test/tests.js > ./test/bundle.js", + "test": "NODE_ENV=test ./node_modules/.bin/mocha test/tests.js", + "test-serve": "npm run build && cp ./dist/pepper.js ./test/pepper.js && npm run build-test && node ./test" + }, + "author": "Michele Pangrazzi ", + "license": "MIT", + "dependencies": { + "debug": "^2.1.1", + "jsonp": "^0.1.0", + "superagent": "^0.21.0" + }, + "devDependencies": { + "@babel/cli": "^7.27.0", + "@babel/core": "^7.26.10", + "@babel/preset-env": "^7.26.9", + "browserify": "^8.1.1", + "chai": "^1.10.0", + "express": "^4.11.0", + "jsdom": "^23.0.2", + "mocha": "^2.1.0", + "uglify-js": "^2.4.16" + }, + "jshintConfig": { + "loopfunc": true, + "multistr": true, + "expr": true + } +} diff --git a/pepper.js b/pepper.js new file mode 100644 index 0000000..f6dcfc1 --- /dev/null +++ b/pepper.js @@ -0,0 +1,2700 @@ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.Pepper=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. + * + * TODO: add a `localStorage` variable to explicitly enable/disable colors + */ + +function useColors() { + // NB: In an Electron preload script, document will be defined but not fully + // initialized. Since we know we're in Chrome, we'll just detect this case + // explicitly + if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') { + return true; + } + + // is webkit? http://stackoverflow.com/a/16459606/376773 + // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 + return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || + // is firebug? http://stackoverflow.com/a/398120/376773 + (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || + // is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || + // double check webkit in userAgent just in case we are in a worker + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); +} + +/** + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. + */ + +exports.formatters.j = function(v) { + try { + return JSON.stringify(v); + } catch (err) { + return '[UnexpectedJSONParseError]: ' + err.message; + } +}; + + +/** + * Colorize log arguments if enabled. + * + * @api public + */ + +function formatArgs(args) { + var useColors = this.useColors; + + args[0] = (useColors ? '%c' : '') + + this.namespace + + (useColors ? ' %c' : ' ') + + args[0] + + (useColors ? '%c ' : ' ') + + '+' + exports.humanize(this.diff); + + if (!useColors) return; + + var c = 'color: ' + this.color; + args.splice(1, 0, c, 'color: inherit') + + // the final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + var index = 0; + var lastC = 0; + args[0].replace(/%[a-zA-Z%]/g, function(match) { + if ('%%' === match) return; + index++; + if ('%c' === match) { + // we only are interested in the *last* %c + // (the user may have provided their own) + lastC = index; + } + }); + + args.splice(lastC, 0, c); +} + +/** + * Invokes `console.log()` when available. + * No-op when `console.log` is not a "function". + * + * @api public + */ + +function log() { + // this hackery is required for IE8/9, where + // the `console.log` function doesn't have 'apply' + return 'object' === typeof console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); +} + +/** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ + +function save(namespaces) { + try { + if (null == namespaces) { + exports.storage.removeItem('debug'); + } else { + exports.storage.debug = namespaces; + } + } catch(e) {} +} + +/** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ + +function load() { + var r; + try { + r = exports.storage.debug; + } catch(e) {} + + // If debug isn't set in LS, and we're in Electron, try to load $DEBUG + if (!r && typeof process !== 'undefined' && 'env' in process) { + r = process.env.DEBUG; + } + + return r; +} + +/** + * Enable namespaces listed in `localStorage.debug` initially. + */ + +exports.enable(load()); + +/** + * Localstorage attempts to return the localstorage. + * + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. + * + * @return {LocalStorage} + * @api private + */ + +function localstorage() { + try { + return window.localStorage; + } catch (e) {} +} + +}).call(this,require('_process')) +},{"./debug":7,"_process":9}],7:[function(require,module,exports){ + +/** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = createDebug.debug = createDebug['default'] = createDebug; +exports.coerce = coerce; +exports.disable = disable; +exports.enable = enable; +exports.enabled = enabled; +exports.humanize = require('ms'); + +/** + * The currently active debug mode names, and names to skip. + */ + +exports.names = []; +exports.skips = []; + +/** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". + */ + +exports.formatters = {}; + +/** + * Previous log timestamp. + */ + +var prevTime; + +/** + * Select a color. + * @param {String} namespace + * @return {Number} + * @api private + */ + +function selectColor(namespace) { + var hash = 0, i; + + for (i in namespace) { + hash = ((hash << 5) - hash) + namespace.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } + + return exports.colors[Math.abs(hash) % exports.colors.length]; +} + +/** + * Create a debugger with the given `namespace`. + * + * @param {String} namespace + * @return {Function} + * @api public + */ + +function createDebug(namespace) { + + function debug() { + // disabled? + if (!debug.enabled) return; + + var self = debug; + + // set `diff` timestamp + var curr = +new Date(); + var ms = curr - (prevTime || curr); + self.diff = ms; + self.prev = prevTime; + self.curr = curr; + prevTime = curr; + + // turn the `arguments` into a proper Array + var args = new Array(arguments.length); + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i]; + } + + args[0] = exports.coerce(args[0]); + + if ('string' !== typeof args[0]) { + // anything else let's inspect with %O + args.unshift('%O'); + } + + // apply any `formatters` transformations + var index = 0; + args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) { + // if we encounter an escaped % then don't increase the array index + if (match === '%%') return match; + index++; + var formatter = exports.formatters[format]; + if ('function' === typeof formatter) { + var val = args[index]; + match = formatter.call(self, val); + + // now we need to remove `args[index]` since it's inlined in the `format` + args.splice(index, 1); + index--; + } + return match; + }); + + // apply env-specific formatting (colors, etc.) + exports.formatArgs.call(self, args); + + var logFn = debug.log || exports.log || console.log.bind(console); + logFn.apply(self, args); + } + + debug.namespace = namespace; + debug.enabled = exports.enabled(namespace); + debug.useColors = exports.useColors(); + debug.color = selectColor(namespace); + + // env-specific initialization logic for debug instances + if ('function' === typeof exports.init) { + exports.init(debug); + } + + return debug; +} + +/** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {String} namespaces + * @api public + */ + +function enable(namespaces) { + exports.save(namespaces); + + exports.names = []; + exports.skips = []; + + var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); + var len = split.length; + + for (var i = 0; i < len; i++) { + if (!split[i]) continue; // ignore empty strings + namespaces = split[i].replace(/\*/g, '.*?'); + if (namespaces[0] === '-') { + exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); + } else { + exports.names.push(new RegExp('^' + namespaces + '$')); + } + } +} + +/** + * Disable debug output. + * + * @api public + */ + +function disable() { + exports.enable(''); +} + +/** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + +function enabled(name) { + var i, len; + for (i = 0, len = exports.skips.length; i < len; i++) { + if (exports.skips[i].test(name)) { + return false; + } + } + for (i = 0, len = exports.names.length; i < len; i++) { + if (exports.names[i].test(name)) { + return true; + } + } + return false; +} + +/** + * Coerce `val`. + * + * @param {Mixed} val + * @return {Mixed} + * @api private + */ + +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; +} + +},{"ms":8}],8:[function(require,module,exports){ +/** + * Helpers. + */ + +var s = 1000; +var m = s * 60; +var h = m * 60; +var d = h * 24; +var y = d * 365.25; + +/** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} [options] + * @throws {Error} throw an error if val is not a non-empty string or a number + * @return {String|Number} + * @api public + */ + +module.exports = function(val, options) { + options = options || {}; + var type = typeof val; + if (type === 'string' && val.length > 0) { + return parse(val); + } else if (type === 'number' && isNaN(val) === false) { + return options.long ? fmtLong(val) : fmtShort(val); + } + throw new Error( + 'val is not a non-empty string or a valid number. val=' + + JSON.stringify(val) + ); +}; + +/** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ + +function parse(str) { + str = String(str); + if (str.length > 100) { + return; + } + var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec( + str + ); + if (!match) { + return; + } + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'yrs': + case 'yr': + case 'y': + return n * y; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'hrs': + case 'hr': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'mins': + case 'min': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 'secs': + case 'sec': + case 's': + return n * s; + case 'milliseconds': + case 'millisecond': + case 'msecs': + case 'msec': + case 'ms': + return n; + default: + return undefined; + } +} + +/** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function fmtShort(ms) { + if (ms >= d) { + return Math.round(ms / d) + 'd'; + } + if (ms >= h) { + return Math.round(ms / h) + 'h'; + } + if (ms >= m) { + return Math.round(ms / m) + 'm'; + } + if (ms >= s) { + return Math.round(ms / s) + 's'; + } + return ms + 'ms'; +} + +/** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function fmtLong(ms) { + return plural(ms, d, 'day') || + plural(ms, h, 'hour') || + plural(ms, m, 'minute') || + plural(ms, s, 'second') || + ms + ' ms'; +} + +/** + * Pluralization helper. + */ + +function plural(ms, n, name) { + if (ms < n) { + return; + } + if (ms < n * 1.5) { + return Math.floor(ms / n) + ' ' + name; + } + return Math.ceil(ms / n) + ' ' + name + 's'; +} + +},{}],9:[function(require,module,exports){ +// shim for using process in browser + +var process = module.exports = {}; +var queue = []; +var draining = false; + +function drainQueue() { + if (draining) { + return; + } + draining = true; + var currentQueue; + var len = queue.length; + while(len) { + currentQueue = queue; + queue = []; + var i = -1; + while (++i < len) { + currentQueue[i](); + } + len = queue.length; + } + draining = false; +} +process.nextTick = function (fun) { + queue.push(fun); + if (!draining) { + setTimeout(drainQueue, 0); + } +}; + +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +// TODO(shtylman) +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}],10:[function(require,module,exports){ +(function (global){ +/*! http://mths.be/punycode v1.2.4 by @mathias */ +;(function(root) { + + /** Detect free variables */ + var freeExports = typeof exports == 'object' && exports; + var freeModule = typeof module == 'object' && module && + module.exports == freeExports && module; + var freeGlobal = typeof global == 'object' && global; + if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) { + root = freeGlobal; + } + + /** + * The `punycode` object. + * @name punycode + * @type Object + */ + var punycode, + + /** Highest positive signed 32-bit float value */ + maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1 + + /** Bootstring parameters */ + base = 36, + tMin = 1, + tMax = 26, + skew = 38, + damp = 700, + initialBias = 72, + initialN = 128, // 0x80 + delimiter = '-', // '\x2D' + + /** Regular expressions */ + regexPunycode = /^xn--/, + regexNonASCII = /[^ -~]/, // unprintable ASCII chars + non-ASCII chars + regexSeparators = /\x2E|\u3002|\uFF0E|\uFF61/g, // RFC 3490 separators + + /** Error messages */ + errors = { + 'overflow': 'Overflow: input needs wider integers to process', + 'not-basic': 'Illegal input >= 0x80 (not a basic code point)', + 'invalid-input': 'Invalid input' + }, + + /** Convenience shortcuts */ + baseMinusTMin = base - tMin, + floor = Math.floor, + stringFromCharCode = String.fromCharCode, + + /** Temporary variable */ + key; + + /*--------------------------------------------------------------------------*/ + + /** + * A generic error utility function. + * @private + * @param {String} type The error type. + * @returns {Error} Throws a `RangeError` with the applicable error message. + */ + function error(type) { + throw RangeError(errors[type]); + } + + /** + * A generic `Array#map` utility function. + * @private + * @param {Array} array The array to iterate over. + * @param {Function} callback The function that gets called for every array + * item. + * @returns {Array} A new array of values returned by the callback function. + */ + function map(array, fn) { + var length = array.length; + while (length--) { + array[length] = fn(array[length]); + } + return array; + } + + /** + * A simple `Array#map`-like wrapper to work with domain name strings. + * @private + * @param {String} domain The domain name. + * @param {Function} callback The function that gets called for every + * character. + * @returns {Array} A new string of characters returned by the callback + * function. + */ + function mapDomain(string, fn) { + return map(string.split(regexSeparators), fn).join('.'); + } + + /** + * Creates an array containing the numeric code points of each Unicode + * character in the string. While JavaScript uses UCS-2 internally, + * this function will convert a pair of surrogate halves (each of which + * UCS-2 exposes as separate characters) into a single code point, + * matching UTF-16. + * @see `punycode.ucs2.encode` + * @see + * @memberOf punycode.ucs2 + * @name decode + * @param {String} string The Unicode input string (UCS-2). + * @returns {Array} The new array of code points. + */ + function ucs2decode(string) { + var output = [], + counter = 0, + length = string.length, + value, + extra; + while (counter < length) { + value = string.charCodeAt(counter++); + if (value >= 0xD800 && value <= 0xDBFF && counter < length) { + // high surrogate, and there is a next character + extra = string.charCodeAt(counter++); + if ((extra & 0xFC00) == 0xDC00) { // low surrogate + output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); + } else { + // unmatched surrogate; only append this code unit, in case the next + // code unit is the high surrogate of a surrogate pair + output.push(value); + counter--; + } + } else { + output.push(value); + } + } + return output; + } + + /** + * Creates a string based on an array of numeric code points. + * @see `punycode.ucs2.decode` + * @memberOf punycode.ucs2 + * @name encode + * @param {Array} codePoints The array of numeric code points. + * @returns {String} The new Unicode string (UCS-2). + */ + function ucs2encode(array) { + return map(array, function(value) { + var output = ''; + if (value > 0xFFFF) { + value -= 0x10000; + output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800); + value = 0xDC00 | value & 0x3FF; + } + output += stringFromCharCode(value); + return output; + }).join(''); + } + + /** + * Converts a basic code point into a digit/integer. + * @see `digitToBasic()` + * @private + * @param {Number} codePoint The basic numeric code point value. + * @returns {Number} The numeric value of a basic code point (for use in + * representing integers) in the range `0` to `base - 1`, or `base` if + * the code point does not represent a value. + */ + function basicToDigit(codePoint) { + if (codePoint - 48 < 10) { + return codePoint - 22; + } + if (codePoint - 65 < 26) { + return codePoint - 65; + } + if (codePoint - 97 < 26) { + return codePoint - 97; + } + return base; + } + + /** + * Converts a digit/integer into a basic code point. + * @see `basicToDigit()` + * @private + * @param {Number} digit The numeric value of a basic code point. + * @returns {Number} The basic code point whose value (when used for + * representing integers) is `digit`, which needs to be in the range + * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is + * used; else, the lowercase form is used. The behavior is undefined + * if `flag` is non-zero and `digit` has no uppercase form. + */ + function digitToBasic(digit, flag) { + // 0..25 map to ASCII a..z or A..Z + // 26..35 map to ASCII 0..9 + return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5); + } + + /** + * Bias adaptation function as per section 3.4 of RFC 3492. + * http://tools.ietf.org/html/rfc3492#section-3.4 + * @private + */ + function adapt(delta, numPoints, firstTime) { + var k = 0; + delta = firstTime ? floor(delta / damp) : delta >> 1; + delta += floor(delta / numPoints); + for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) { + delta = floor(delta / baseMinusTMin); + } + return floor(k + (baseMinusTMin + 1) * delta / (delta + skew)); + } + + /** + * Converts a Punycode string of ASCII-only symbols to a string of Unicode + * symbols. + * @memberOf punycode + * @param {String} input The Punycode string of ASCII-only symbols. + * @returns {String} The resulting string of Unicode symbols. + */ + function decode(input) { + // Don't use UCS-2 + var output = [], + inputLength = input.length, + out, + i = 0, + n = initialN, + bias = initialBias, + basic, + j, + index, + oldi, + w, + k, + digit, + t, + /** Cached calculation results */ + baseMinusT; + + // Handle the basic code points: let `basic` be the number of input code + // points before the last delimiter, or `0` if there is none, then copy + // the first basic code points to the output. + + basic = input.lastIndexOf(delimiter); + if (basic < 0) { + basic = 0; + } + + for (j = 0; j < basic; ++j) { + // if it's not a basic code point + if (input.charCodeAt(j) >= 0x80) { + error('not-basic'); + } + output.push(input.charCodeAt(j)); + } + + // Main decoding loop: start just after the last delimiter if any basic code + // points were copied; start at the beginning otherwise. + + for (index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) { + + // `index` is the index of the next character to be consumed. + // Decode a generalized variable-length integer into `delta`, + // which gets added to `i`. The overflow checking is easier + // if we increase `i` as we go, then subtract off its starting + // value at the end to obtain `delta`. + for (oldi = i, w = 1, k = base; /* no condition */; k += base) { + + if (index >= inputLength) { + error('invalid-input'); + } + + digit = basicToDigit(input.charCodeAt(index++)); + + if (digit >= base || digit > floor((maxInt - i) / w)) { + error('overflow'); + } + + i += digit * w; + t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); + + if (digit < t) { + break; + } + + baseMinusT = base - t; + if (w > floor(maxInt / baseMinusT)) { + error('overflow'); + } + + w *= baseMinusT; + + } + + out = output.length + 1; + bias = adapt(i - oldi, out, oldi == 0); + + // `i` was supposed to wrap around from `out` to `0`, + // incrementing `n` each time, so we'll fix that now: + if (floor(i / out) > maxInt - n) { + error('overflow'); + } + + n += floor(i / out); + i %= out; + + // Insert `n` at position `i` of the output + output.splice(i++, 0, n); + + } + + return ucs2encode(output); + } + + /** + * Converts a string of Unicode symbols to a Punycode string of ASCII-only + * symbols. + * @memberOf punycode + * @param {String} input The string of Unicode symbols. + * @returns {String} The resulting Punycode string of ASCII-only symbols. + */ + function encode(input) { + var n, + delta, + handledCPCount, + basicLength, + bias, + j, + m, + q, + k, + t, + currentValue, + output = [], + /** `inputLength` will hold the number of code points in `input`. */ + inputLength, + /** Cached calculation results */ + handledCPCountPlusOne, + baseMinusT, + qMinusT; + + // Convert the input in UCS-2 to Unicode + input = ucs2decode(input); + + // Cache the length + inputLength = input.length; + + // Initialize the state + n = initialN; + delta = 0; + bias = initialBias; + + // Handle the basic code points + for (j = 0; j < inputLength; ++j) { + currentValue = input[j]; + if (currentValue < 0x80) { + output.push(stringFromCharCode(currentValue)); + } + } + + handledCPCount = basicLength = output.length; + + // `handledCPCount` is the number of code points that have been handled; + // `basicLength` is the number of basic code points. + + // Finish the basic string - if it is not empty - with a delimiter + if (basicLength) { + output.push(delimiter); + } + + // Main encoding loop: + while (handledCPCount < inputLength) { + + // All non-basic code points < n have been handled already. Find the next + // larger one: + for (m = maxInt, j = 0; j < inputLength; ++j) { + currentValue = input[j]; + if (currentValue >= n && currentValue < m) { + m = currentValue; + } + } + + // Increase `delta` enough to advance the decoder's state to , + // but guard against overflow + handledCPCountPlusOne = handledCPCount + 1; + if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) { + error('overflow'); + } + + delta += (m - n) * handledCPCountPlusOne; + n = m; + + for (j = 0; j < inputLength; ++j) { + currentValue = input[j]; + + if (currentValue < n && ++delta > maxInt) { + error('overflow'); + } + + if (currentValue == n) { + // Represent delta as a generalized variable-length integer + for (q = delta, k = base; /* no condition */; k += base) { + t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); + if (q < t) { + break; + } + qMinusT = q - t; + baseMinusT = base - t; + output.push( + stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0)) + ); + q = floor(qMinusT / baseMinusT); + } + + output.push(stringFromCharCode(digitToBasic(q, 0))); + bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength); + delta = 0; + ++handledCPCount; + } + } + + ++delta; + ++n; + + } + return output.join(''); + } + + /** + * Converts a Punycode string representing a domain name to Unicode. Only the + * Punycoded parts of the domain name will be converted, i.e. it doesn't + * matter if you call it on a string that has already been converted to + * Unicode. + * @memberOf punycode + * @param {String} domain The Punycode domain name to convert to Unicode. + * @returns {String} The Unicode representation of the given Punycode + * string. + */ + function toUnicode(domain) { + return mapDomain(domain, function(string) { + return regexPunycode.test(string) + ? decode(string.slice(4).toLowerCase()) + : string; + }); + } + + /** + * Converts a Unicode string representing a domain name to Punycode. Only the + * non-ASCII parts of the domain name will be converted, i.e. it doesn't + * matter if you call it with a domain that's already in ASCII. + * @memberOf punycode + * @param {String} domain The domain name to convert, as a Unicode string. + * @returns {String} The Punycode representation of the given domain name. + */ + function toASCII(domain) { + return mapDomain(domain, function(string) { + return regexNonASCII.test(string) + ? 'xn--' + encode(string) + : string; + }); + } + + /*--------------------------------------------------------------------------*/ + + /** Define the public API */ + punycode = { + /** + * A string representing the current Punycode.js version number. + * @memberOf punycode + * @type String + */ + 'version': '1.2.4', + /** + * An object of methods to convert from JavaScript's internal character + * representation (UCS-2) to Unicode code points, and back. + * @see + * @memberOf punycode + * @type Object + */ + 'ucs2': { + 'decode': ucs2decode, + 'encode': ucs2encode + }, + 'decode': decode, + 'encode': encode, + 'toASCII': toASCII, + 'toUnicode': toUnicode + }; + + /** Expose `punycode` */ + // Some AMD build optimizers, like r.js, check for specific condition patterns + // like the following: + if ( + typeof define == 'function' && + typeof define.amd == 'object' && + define.amd + ) { + define('punycode', function() { + return punycode; + }); + } else if (freeExports && !freeExports.nodeType) { + if (freeModule) { // in Node.js or RingoJS v0.8.0+ + freeModule.exports = punycode; + } else { // in Narwhal or RingoJS v0.7.0- + for (key in punycode) { + punycode.hasOwnProperty(key) && (freeExports[key] = punycode[key]); + } + } + } else { // in Rhino or a web browser + root.punycode = punycode; + } + +}(this)); + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],11:[function(require,module,exports){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +// If obj.hasOwnProperty has been overridden, then calling +// obj.hasOwnProperty(prop) will break. +// See: https://github.com/joyent/node/issues/1707 +function hasOwnProperty(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); +} + +module.exports = function(qs, sep, eq, options) { + sep = sep || '&'; + eq = eq || '='; + var obj = {}; + + if (typeof qs !== 'string' || qs.length === 0) { + return obj; + } + + var regexp = /\+/g; + qs = qs.split(sep); + + var maxKeys = 1000; + if (options && typeof options.maxKeys === 'number') { + maxKeys = options.maxKeys; + } + + var len = qs.length; + // maxKeys <= 0 means that we should not limit keys count + if (maxKeys > 0 && len > maxKeys) { + len = maxKeys; + } + + for (var i = 0; i < len; ++i) { + var x = qs[i].replace(regexp, '%20'), + idx = x.indexOf(eq), + kstr, vstr, k, v; + + if (idx >= 0) { + kstr = x.substr(0, idx); + vstr = x.substr(idx + 1); + } else { + kstr = x; + vstr = ''; + } + + k = decodeURIComponent(kstr); + v = decodeURIComponent(vstr); + + if (!hasOwnProperty(obj, k)) { + obj[k] = v; + } else if (isArray(obj[k])) { + obj[k].push(v); + } else { + obj[k] = [obj[k], v]; + } + } + + return obj; +}; + +var isArray = Array.isArray || function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]'; +}; + +},{}],12:[function(require,module,exports){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +var stringifyPrimitive = function(v) { + switch (typeof v) { + case 'string': + return v; + + case 'boolean': + return v ? 'true' : 'false'; + + case 'number': + return isFinite(v) ? v : ''; + + default: + return ''; + } +}; + +module.exports = function(obj, sep, eq, name) { + sep = sep || '&'; + eq = eq || '='; + if (obj === null) { + obj = undefined; + } + + if (typeof obj === 'object') { + return map(objectKeys(obj), function(k) { + var ks = encodeURIComponent(stringifyPrimitive(k)) + eq; + if (isArray(obj[k])) { + return map(obj[k], function(v) { + return ks + encodeURIComponent(stringifyPrimitive(v)); + }).join(sep); + } else { + return ks + encodeURIComponent(stringifyPrimitive(obj[k])); + } + }).join(sep); + + } + + if (!name) return ''; + return encodeURIComponent(stringifyPrimitive(name)) + eq + + encodeURIComponent(stringifyPrimitive(obj)); +}; + +var isArray = Array.isArray || function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]'; +}; + +function map (xs, f) { + if (xs.map) return xs.map(f); + var res = []; + for (var i = 0; i < xs.length; i++) { + res.push(f(xs[i], i)); + } + return res; +} + +var objectKeys = Object.keys || function (obj) { + var res = []; + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) res.push(key); + } + return res; +}; + +},{}],13:[function(require,module,exports){ +'use strict'; + +exports.decode = exports.parse = require('./decode'); +exports.encode = exports.stringify = require('./encode'); + +},{"./decode":11,"./encode":12}],14:[function(require,module,exports){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +var punycode = require('punycode'); + +exports.parse = urlParse; +exports.resolve = urlResolve; +exports.resolveObject = urlResolveObject; +exports.format = urlFormat; + +exports.Url = Url; + +function Url() { + this.protocol = null; + this.slashes = null; + this.auth = null; + this.host = null; + this.port = null; + this.hostname = null; + this.hash = null; + this.search = null; + this.query = null; + this.pathname = null; + this.path = null; + this.href = null; +} + +// Reference: RFC 3986, RFC 1808, RFC 2396 + +// define these here so at least they only have to be +// compiled once on the first module load. +var protocolPattern = /^([a-z0-9.+-]+:)/i, + portPattern = /:[0-9]*$/, + + // RFC 2396: characters reserved for delimiting URLs. + // We actually just auto-escape these. + delims = ['<', '>', '"', '`', ' ', '\r', '\n', '\t'], + + // RFC 2396: characters not allowed for various reasons. + unwise = ['{', '}', '|', '\\', '^', '`'].concat(delims), + + // Allowed by RFCs, but cause of XSS attacks. Always escape these. + autoEscape = ['\''].concat(unwise), + // Characters that are never ever allowed in a hostname. + // Note that any invalid chars are also handled, but these + // are the ones that are *expected* to be seen, so we fast-path + // them. + nonHostChars = ['%', '/', '?', ';', '#'].concat(autoEscape), + hostEndingChars = ['/', '?', '#'], + hostnameMaxLen = 255, + hostnamePartPattern = /^[a-z0-9A-Z_-]{0,63}$/, + hostnamePartStart = /^([a-z0-9A-Z_-]{0,63})(.*)$/, + // protocols that can allow "unsafe" and "unwise" chars. + unsafeProtocol = { + 'javascript': true, + 'javascript:': true + }, + // protocols that never have a hostname. + hostlessProtocol = { + 'javascript': true, + 'javascript:': true + }, + // protocols that always contain a // bit. + slashedProtocol = { + 'http': true, + 'https': true, + 'ftp': true, + 'gopher': true, + 'file': true, + 'http:': true, + 'https:': true, + 'ftp:': true, + 'gopher:': true, + 'file:': true + }, + querystring = require('querystring'); + +function urlParse(url, parseQueryString, slashesDenoteHost) { + if (url && isObject(url) && url instanceof Url) return url; + + var u = new Url; + u.parse(url, parseQueryString, slashesDenoteHost); + return u; +} + +Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { + if (!isString(url)) { + throw new TypeError("Parameter 'url' must be a string, not " + typeof url); + } + + var rest = url; + + // trim before proceeding. + // This is to support parse stuff like " http://foo.com \n" + rest = rest.trim(); + + var proto = protocolPattern.exec(rest); + if (proto) { + proto = proto[0]; + var lowerProto = proto.toLowerCase(); + this.protocol = lowerProto; + rest = rest.substr(proto.length); + } + + // figure out if it's got a host + // user@server is *always* interpreted as a hostname, and url + // resolution will treat //foo/bar as host=foo,path=bar because that's + // how the browser resolves relative URLs. + if (slashesDenoteHost || proto || rest.match(/^\/\/[^@\/]+@[^@\/]+/)) { + var slashes = rest.substr(0, 2) === '//'; + if (slashes && !(proto && hostlessProtocol[proto])) { + rest = rest.substr(2); + this.slashes = true; + } + } + + if (!hostlessProtocol[proto] && + (slashes || (proto && !slashedProtocol[proto]))) { + + // there's a hostname. + // the first instance of /, ?, ;, or # ends the host. + // + // If there is an @ in the hostname, then non-host chars *are* allowed + // to the left of the last @ sign, unless some host-ending character + // comes *before* the @-sign. + // URLs are obnoxious. + // + // ex: + // http://a@b@c/ => user:a@b host:c + // http://a@b?@c => user:a host:c path:/?@c + + // v0.12 TODO(isaacs): This is not quite how Chrome does things. + // Review our test case against browsers more comprehensively. + + // find the first instance of any hostEndingChars + var hostEnd = -1; + for (var i = 0; i < hostEndingChars.length; i++) { + var hec = rest.indexOf(hostEndingChars[i]); + if (hec !== -1 && (hostEnd === -1 || hec < hostEnd)) + hostEnd = hec; + } + + // at this point, either we have an explicit point where the + // auth portion cannot go past, or the last @ char is the decider. + var auth, atSign; + if (hostEnd === -1) { + // atSign can be anywhere. + atSign = rest.lastIndexOf('@'); + } else { + // atSign must be in auth portion. + // http://a@b/c@d => host:b auth:a path:/c@d + atSign = rest.lastIndexOf('@', hostEnd); + } + + // Now we have a portion which is definitely the auth. + // Pull that off. + if (atSign !== -1) { + auth = rest.slice(0, atSign); + rest = rest.slice(atSign + 1); + this.auth = decodeURIComponent(auth); + } + + // the host is the remaining to the left of the first non-host char + hostEnd = -1; + for (var i = 0; i < nonHostChars.length; i++) { + var hec = rest.indexOf(nonHostChars[i]); + if (hec !== -1 && (hostEnd === -1 || hec < hostEnd)) + hostEnd = hec; + } + // if we still have not hit it, then the entire thing is a host. + if (hostEnd === -1) + hostEnd = rest.length; + + this.host = rest.slice(0, hostEnd); + rest = rest.slice(hostEnd); + + // pull out port. + this.parseHost(); + + // we've indicated that there is a hostname, + // so even if it's empty, it has to be present. + this.hostname = this.hostname || ''; + + // if hostname begins with [ and ends with ] + // assume that it's an IPv6 address. + var ipv6Hostname = this.hostname[0] === '[' && + this.hostname[this.hostname.length - 1] === ']'; + + // validate a little. + if (!ipv6Hostname) { + var hostparts = this.hostname.split(/\./); + for (var i = 0, l = hostparts.length; i < l; i++) { + var part = hostparts[i]; + if (!part) continue; + if (!part.match(hostnamePartPattern)) { + var newpart = ''; + for (var j = 0, k = part.length; j < k; j++) { + if (part.charCodeAt(j) > 127) { + // we replace non-ASCII char with a temporary placeholder + // we need this to make sure size of hostname is not + // broken by replacing non-ASCII by nothing + newpart += 'x'; + } else { + newpart += part[j]; + } + } + // we test again with ASCII char only + if (!newpart.match(hostnamePartPattern)) { + var validParts = hostparts.slice(0, i); + var notHost = hostparts.slice(i + 1); + var bit = part.match(hostnamePartStart); + if (bit) { + validParts.push(bit[1]); + notHost.unshift(bit[2]); + } + if (notHost.length) { + rest = '/' + notHost.join('.') + rest; + } + this.hostname = validParts.join('.'); + break; + } + } + } + } + + if (this.hostname.length > hostnameMaxLen) { + this.hostname = ''; + } else { + // hostnames are always lower case. + this.hostname = this.hostname.toLowerCase(); + } + + if (!ipv6Hostname) { + // IDNA Support: Returns a puny coded representation of "domain". + // It only converts the part of the domain name that + // has non ASCII characters. I.e. it dosent matter if + // you call it with a domain that already is in ASCII. + var domainArray = this.hostname.split('.'); + var newOut = []; + for (var i = 0; i < domainArray.length; ++i) { + var s = domainArray[i]; + newOut.push(s.match(/[^A-Za-z0-9_-]/) ? + 'xn--' + punycode.encode(s) : s); + } + this.hostname = newOut.join('.'); + } + + var p = this.port ? ':' + this.port : ''; + var h = this.hostname || ''; + this.host = h + p; + this.href += this.host; + + // strip [ and ] from the hostname + // the host field still retains them, though + if (ipv6Hostname) { + this.hostname = this.hostname.substr(1, this.hostname.length - 2); + if (rest[0] !== '/') { + rest = '/' + rest; + } + } + } + + // now rest is set to the post-host stuff. + // chop off any delim chars. + if (!unsafeProtocol[lowerProto]) { + + // First, make 100% sure that any "autoEscape" chars get + // escaped, even if encodeURIComponent doesn't think they + // need to be. + for (var i = 0, l = autoEscape.length; i < l; i++) { + var ae = autoEscape[i]; + var esc = encodeURIComponent(ae); + if (esc === ae) { + esc = escape(ae); + } + rest = rest.split(ae).join(esc); + } + } + + + // chop off from the tail first. + var hash = rest.indexOf('#'); + if (hash !== -1) { + // got a fragment string. + this.hash = rest.substr(hash); + rest = rest.slice(0, hash); + } + var qm = rest.indexOf('?'); + if (qm !== -1) { + this.search = rest.substr(qm); + this.query = rest.substr(qm + 1); + if (parseQueryString) { + this.query = querystring.parse(this.query); + } + rest = rest.slice(0, qm); + } else if (parseQueryString) { + // no query string, but parseQueryString still requested + this.search = ''; + this.query = {}; + } + if (rest) this.pathname = rest; + if (slashedProtocol[lowerProto] && + this.hostname && !this.pathname) { + this.pathname = '/'; + } + + //to support http.request + if (this.pathname || this.search) { + var p = this.pathname || ''; + var s = this.search || ''; + this.path = p + s; + } + + // finally, reconstruct the href based on what has been validated. + this.href = this.format(); + return this; +}; + +// format a parsed object into a url string +function urlFormat(obj) { + // ensure it's an object, and not a string url. + // If it's an obj, this is a no-op. + // this way, you can call url_format() on strings + // to clean up potentially wonky urls. + if (isString(obj)) obj = urlParse(obj); + if (!(obj instanceof Url)) return Url.prototype.format.call(obj); + return obj.format(); +} + +Url.prototype.format = function() { + var auth = this.auth || ''; + if (auth) { + auth = encodeURIComponent(auth); + auth = auth.replace(/%3A/i, ':'); + auth += '@'; + } + + var protocol = this.protocol || '', + pathname = this.pathname || '', + hash = this.hash || '', + host = false, + query = ''; + + if (this.host) { + host = auth + this.host; + } else if (this.hostname) { + host = auth + (this.hostname.indexOf(':') === -1 ? + this.hostname : + '[' + this.hostname + ']'); + if (this.port) { + host += ':' + this.port; + } + } + + if (this.query && + isObject(this.query) && + Object.keys(this.query).length) { + query = querystring.stringify(this.query); + } + + var search = this.search || (query && ('?' + query)) || ''; + + if (protocol && protocol.substr(-1) !== ':') protocol += ':'; + + // only the slashedProtocols get the //. Not mailto:, xmpp:, etc. + // unless they had them to begin with. + if (this.slashes || + (!protocol || slashedProtocol[protocol]) && host !== false) { + host = '//' + (host || ''); + if (pathname && pathname.charAt(0) !== '/') pathname = '/' + pathname; + } else if (!host) { + host = ''; + } + + if (hash && hash.charAt(0) !== '#') hash = '#' + hash; + if (search && search.charAt(0) !== '?') search = '?' + search; + + pathname = pathname.replace(/[?#]/g, function(match) { + return encodeURIComponent(match); + }); + search = search.replace('#', '%23'); + + return protocol + host + pathname + search + hash; +}; + +function urlResolve(source, relative) { + return urlParse(source, false, true).resolve(relative); +} + +Url.prototype.resolve = function(relative) { + return this.resolveObject(urlParse(relative, false, true)).format(); +}; + +function urlResolveObject(source, relative) { + if (!source) return relative; + return urlParse(source, false, true).resolveObject(relative); +} + +Url.prototype.resolveObject = function(relative) { + if (isString(relative)) { + var rel = new Url(); + rel.parse(relative, false, true); + relative = rel; + } + + var result = new Url(); + Object.keys(this).forEach(function(k) { + result[k] = this[k]; + }, this); + + // hash is always overridden, no matter what. + // even href="" will remove it. + result.hash = relative.hash; + + // if the relative url is empty, then there's nothing left to do here. + if (relative.href === '') { + result.href = result.format(); + return result; + } + + // hrefs like //foo/bar always cut to the protocol. + if (relative.slashes && !relative.protocol) { + // take everything except the protocol from relative + Object.keys(relative).forEach(function(k) { + if (k !== 'protocol') + result[k] = relative[k]; + }); + + //urlParse appends trailing / to urls like http://www.example.com + if (slashedProtocol[result.protocol] && + result.hostname && !result.pathname) { + result.path = result.pathname = '/'; + } + + result.href = result.format(); + return result; + } + + if (relative.protocol && relative.protocol !== result.protocol) { + // if it's a known url protocol, then changing + // the protocol does weird things + // first, if it's not file:, then we MUST have a host, + // and if there was a path + // to begin with, then we MUST have a path. + // if it is file:, then the host is dropped, + // because that's known to be hostless. + // anything else is assumed to be absolute. + if (!slashedProtocol[relative.protocol]) { + Object.keys(relative).forEach(function(k) { + result[k] = relative[k]; + }); + result.href = result.format(); + return result; + } + + result.protocol = relative.protocol; + if (!relative.host && !hostlessProtocol[relative.protocol]) { + var relPath = (relative.pathname || '').split('/'); + while (relPath.length && !(relative.host = relPath.shift())); + if (!relative.host) relative.host = ''; + if (!relative.hostname) relative.hostname = ''; + if (relPath[0] !== '') relPath.unshift(''); + if (relPath.length < 2) relPath.unshift(''); + result.pathname = relPath.join('/'); + } else { + result.pathname = relative.pathname; + } + result.search = relative.search; + result.query = relative.query; + result.host = relative.host || ''; + result.auth = relative.auth; + result.hostname = relative.hostname || relative.host; + result.port = relative.port; + // to support http.request + if (result.pathname || result.search) { + var p = result.pathname || ''; + var s = result.search || ''; + result.path = p + s; + } + result.slashes = result.slashes || relative.slashes; + result.href = result.format(); + return result; + } + + var isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/'), + isRelAbs = ( + relative.host || + relative.pathname && relative.pathname.charAt(0) === '/' + ), + mustEndAbs = (isRelAbs || isSourceAbs || + (result.host && relative.pathname)), + removeAllDots = mustEndAbs, + srcPath = result.pathname && result.pathname.split('/') || [], + relPath = relative.pathname && relative.pathname.split('/') || [], + psychotic = result.protocol && !slashedProtocol[result.protocol]; + + // if the url is a non-slashed url, then relative + // links like ../.. should be able + // to crawl up to the hostname, as well. This is strange. + // result.protocol has already been set by now. + // Later on, put the first path part into the host field. + if (psychotic) { + result.hostname = ''; + result.port = null; + if (result.host) { + if (srcPath[0] === '') srcPath[0] = result.host; + else srcPath.unshift(result.host); + } + result.host = ''; + if (relative.protocol) { + relative.hostname = null; + relative.port = null; + if (relative.host) { + if (relPath[0] === '') relPath[0] = relative.host; + else relPath.unshift(relative.host); + } + relative.host = null; + } + mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === ''); + } + + if (isRelAbs) { + // it's absolute. + result.host = (relative.host || relative.host === '') ? + relative.host : result.host; + result.hostname = (relative.hostname || relative.hostname === '') ? + relative.hostname : result.hostname; + result.search = relative.search; + result.query = relative.query; + srcPath = relPath; + // fall through to the dot-handling below. + } else if (relPath.length) { + // it's relative + // throw away the existing file, and take the new path instead. + if (!srcPath) srcPath = []; + srcPath.pop(); + srcPath = srcPath.concat(relPath); + result.search = relative.search; + result.query = relative.query; + } else if (!isNullOrUndefined(relative.search)) { + // just pull out the search. + // like href='?foo'. + // Put this after the other two cases because it simplifies the booleans + if (psychotic) { + result.hostname = result.host = srcPath.shift(); + //occationaly the auth can get stuck only in host + //this especialy happens in cases like + //url.resolveObject('mailto:local1@domain1', 'local2@domain2') + var authInHost = result.host && result.host.indexOf('@') > 0 ? + result.host.split('@') : false; + if (authInHost) { + result.auth = authInHost.shift(); + result.host = result.hostname = authInHost.shift(); + } + } + result.search = relative.search; + result.query = relative.query; + //to support http.request + if (!isNull(result.pathname) || !isNull(result.search)) { + result.path = (result.pathname ? result.pathname : '') + + (result.search ? result.search : ''); + } + result.href = result.format(); + return result; + } + + if (!srcPath.length) { + // no path at all. easy. + // we've already handled the other stuff above. + result.pathname = null; + //to support http.request + if (result.search) { + result.path = '/' + result.search; + } else { + result.path = null; + } + result.href = result.format(); + return result; + } + + // if a url ENDs in . or .., then it must get a trailing slash. + // however, if it ends in anything else non-slashy, + // then it must NOT get a trailing slash. + var last = srcPath.slice(-1)[0]; + var hasTrailingSlash = ( + (result.host || relative.host) && (last === '.' || last === '..') || + last === ''); + + // strip single dots, resolve double dots to parent dir + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for (var i = srcPath.length; i >= 0; i--) { + last = srcPath[i]; + if (last == '.') { + srcPath.splice(i, 1); + } else if (last === '..') { + srcPath.splice(i, 1); + up++; + } else if (up) { + srcPath.splice(i, 1); + up--; + } + } + + // if the path is allowed to go above the root, restore leading ..s + if (!mustEndAbs && !removeAllDots) { + for (; up--; up) { + srcPath.unshift('..'); + } + } + + if (mustEndAbs && srcPath[0] !== '' && + (!srcPath[0] || srcPath[0].charAt(0) !== '/')) { + srcPath.unshift(''); + } + + if (hasTrailingSlash && (srcPath.join('/').substr(-1) !== '/')) { + srcPath.push(''); + } + + var isAbsolute = srcPath[0] === '' || + (srcPath[0] && srcPath[0].charAt(0) === '/'); + + // put the host back + if (psychotic) { + result.hostname = result.host = isAbsolute ? '' : + srcPath.length ? srcPath.shift() : ''; + //occationaly the auth can get stuck only in host + //this especialy happens in cases like + //url.resolveObject('mailto:local1@domain1', 'local2@domain2') + var authInHost = result.host && result.host.indexOf('@') > 0 ? + result.host.split('@') : false; + if (authInHost) { + result.auth = authInHost.shift(); + result.host = result.hostname = authInHost.shift(); + } + } + + mustEndAbs = mustEndAbs || (result.host && srcPath.length); + + if (mustEndAbs && !isAbsolute) { + srcPath.unshift(''); + } + + if (!srcPath.length) { + result.pathname = null; + result.path = null; + } else { + result.pathname = srcPath.join('/'); + } + + //to support request.http + if (!isNull(result.pathname) || !isNull(result.search)) { + result.path = (result.pathname ? result.pathname : '') + + (result.search ? result.search : ''); + } + result.auth = relative.auth || result.auth; + result.slashes = result.slashes || relative.slashes; + result.href = result.format(); + return result; +}; + +Url.prototype.parseHost = function() { + var host = this.host; + var port = portPattern.exec(host); + if (port) { + port = port[0]; + if (port !== ':') { + this.port = port.substr(1); + } + host = host.substr(0, host.length - port.length); + } + if (host) this.hostname = host; +}; + +function isString(arg) { + return typeof arg === "string"; +} + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} + +function isNull(arg) { + return arg === null; +} +function isNullOrUndefined(arg) { + return arg == null; +} + +},{"punycode":10,"querystring":13}]},{},[1])(1) +}); \ No newline at end of file diff --git a/test/fixtures/uamservice.js b/test/fixtures/uamservice.js index ecf36c7..58e56d9 100644 --- a/test/fixtures/uamservice.js +++ b/test/fixtures/uamservice.js @@ -2,7 +2,8 @@ module.exports = function() { return { - chap: '1231b655a9c14dbcdea738991cd4a0c6' + chap: '1231b655a9c14dbcdea738991cd4a0c6', + pap: '1231b655a9c14dbcdea738991cd4a0c6' }; }; diff --git a/test/tests.js b/test/tests.js index 522c791..8118d9f 100644 --- a/test/tests.js +++ b/test/tests.js @@ -42,7 +42,6 @@ describe('Pepper', function() { var pepper = Pepper({ querystring: sampleQs }); - expect(pepper.data).to.have.property('called'); expect(pepper.data).to.have.property('challenge'); expect(pepper.data).to.have.property('ip'); @@ -59,7 +58,8 @@ describe('Pepper', function() { var pepper = Pepper({ host: '10.10.0.1', port: 3990, - ssl: false + ssl: false, + uamservice: 'http://localhost/chilli-uamservice-pap-chap.php' }); expect(pepper._baseUrl).to.equal('http://10.10.0.1:3990/json/'); @@ -71,6 +71,7 @@ describe('Pepper', function() { host: '10.10.0.1', port: 3990, ssl: false, + uamservice: 'http://localhost/chilli-uamservice-pap-chap.php', querystring: sampleQs }); @@ -82,6 +83,7 @@ describe('Pepper', function() { var pepper = Pepper({ host: 'localhost', port: 5000, + uamservice: 'http://localhost/chilli-uamservice-pap-chap.php', ssl: false }); @@ -100,6 +102,7 @@ describe('Pepper', function() { var pepper = Pepper({ host: 'localhost', port: 5000, + uamservice: 'http://localhost/chilli-uamservice-pap-chap.php', ssl: false }); @@ -116,6 +119,7 @@ describe('Pepper', function() { var pepper = Pepper({ host: 'localhost', port: 5000, + uamservice: 'http://localhost/chilli-uamservice-pap-chap.php', ssl: false }); @@ -132,6 +136,7 @@ describe('Pepper', function() { var pepper = Pepper({ host: 'localhost', port: 5000, + uamservice: 'http://localhost/chilli-uamservice-pap-chap.php', ssl: false }); @@ -146,6 +151,7 @@ describe('Pepper', function() { var pepper = Pepper({ host: 'localhost', port: 5000, + uamservice: 'http://localhost/chilli-uamservice-pap-chap.php', ssl: false }); @@ -164,6 +170,7 @@ describe('Pepper', function() { var pepper = Pepper({ host: 'localhost', port: 5000, + uamservice: 'http://localhost/chilli-uamservice-pap-chap.php', ssl: false }); @@ -183,6 +190,7 @@ describe('Pepper', function() { var pepper = Pepper({ host: 'localhost', port: 5000, + uamservice: 'http://localhost/chilli-uamservice-pap-chap.php', ssl: false }); @@ -202,6 +210,7 @@ describe('Pepper', function() { var pepper = Pepper({ host: 'localhost', port: 5000, + uamservice: 'http://localhost/chilli-uamservice-pap-chap.php', ssl: false }); @@ -223,7 +232,7 @@ describe('Pepper', function() { host: 'localhost', port: 5000, ssl: false, - uamservice: 'https://uamservice.service.com' + uamservice: 'http://localhost/chilli-uamservice-pap-chap.php', }); pepper.logon('test', 'test', function(err, data) { @@ -240,6 +249,7 @@ describe('Pepper', function() { host: 'localhost', port: 5000, ssl: false, + uamservice: 'http://localhost/chilli-uamservice-pap-chap.php', interval: 2500 });