From e16e5a34e6f96177ca922dc37b97f659963f926c Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 24 Sep 2018 18:11:11 -0600 Subject: [PATCH 01/68] WIP launcher updates --- usr/share/install-launcher.js | 34 ++----------------- usr/share/which.js | 63 +++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 32 deletions(-) create mode 100644 usr/share/which.js diff --git a/usr/share/install-launcher.js b/usr/share/install-launcher.js index eec6c56..3935fc2 100644 --- a/usr/share/install-launcher.js +++ b/usr/share/install-launcher.js @@ -11,7 +11,7 @@ Launcher._killAll = function (fn) { var psList = require('ps-list'); psList().then(function (procs) { procs.forEach(function (proc) { - if ('node' === proc.name && /\btelebitd\b/i.test(proc.cmd)) { + if ('node' === proc.name && /\btelebit(d| daemon)\b/i.test(proc.cmd)) { console.log(proc); process.kill(proc.pid); return true; @@ -45,37 +45,7 @@ Launcher._detect = function (things, fn) { } } - // could have used "command-exists" but I'm trying to stay low-dependency - // os.platform(), os.type() - if (!/^win/i.test(os.platform())) { - if (/^darwin/i.test(os.platform())) { - exec('command -v launchctl', things._execOpts, function (err, stdout, stderr) { - err = Launcher._getError(err, stderr); - fn(err, 'launchctl'); - }); - } else { - exec('command -v systemctl', things._execOpts, function (err, stdout, stderr) { - err = Launcher._getError(err, stderr); - fn(err, 'systemctl'); - }); - } - } else { - // https://stackoverflow.com/questions/17908789/how-to-add-an-item-to-registry-to-run-at-startup-without-uac - // wininit? regedit? SCM? - // REG ADD "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /V "My App" /t REG_SZ /F /D "C:\MyAppPath\MyApp.exe" - // https://www.microsoft.com/developerblog/2015/11/09/reading-and-writing-to-the-windows-registry-in-process-from-node-js/ - // https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/reg-add - // https://social.msdn.microsoft.com/Forums/en-US/5b318f44-281e-4098-8dee-3ba8435fa391/add-registry-key-for-autostart-of-app-in-ice?forum=quebectools - // utils.elevate - // https://github.com/CatalystCode/windows-registry-node - exec('where reg.exe', things._execOpts, function (err, stdout, stderr) { - //console.log((stdout||'').trim()); - if (stderr) { - console.error(stderr); - } - fn(err, 'reg.exe'); - }); - } + require('./which.js').launcher(things._execOpts, fn); }; Launcher.install = function (things, fn) { if (!fn) { fn = function (err) { if (err) { console.error(err); } }; } diff --git a/usr/share/which.js b/usr/share/which.js new file mode 100644 index 0000000..dc23027 --- /dev/null +++ b/usr/share/which.js @@ -0,0 +1,63 @@ +'use strict'; + +var os = require('os'); +var exec = require('child_process').exec; + +var which = module.exports; + +which._getError = function getError(err, stderr) { + if (err) { return err; } + if (stderr) { + err = new Error(stderr); + err.code = 'EWHICH'; + return err; + } +}; + +module.exports.which = function (cmd, execOpts, fn) { + return module.exports._which({ + mac: cmd + , linux: cmd + , win: cmd + }, execOpts, fn); +}; +module.exports.launcher = function (execOpts, fn) { + return module.exports._which({ + mac: 'launchctl' + , linux: 'systemctl' + , win: 'reg.exe' + }, execOpts, fn); +}; +module.exports._which = function (progs, execOpts, fn) { + // could have used "command-exists" but I'm trying to stay low-dependency + // os.platform(), os.type() + if (!/^win/i.test(os.platform())) { + if (/^darwin/i.test(os.platform())) { + exec('command -v ' + progs.mac, execOpts, function (err, stdout, stderr) { + err = which._getError(err, stderr); + fn(err, progs.mac); + }); + } else { + exec('command -v ' + progs.linux, execOpts, function (err, stdout, stderr) { + err = which._getError(err, stderr); + fn(err, progs.linux); + }); + } + } else { + // https://stackoverflow.com/questions/17908789/how-to-add-an-item-to-registry-to-run-at-startup-without-uac + // wininit? regedit? SCM? + // REG ADD "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /V "My App" /t REG_SZ /F /D "C:\MyAppPath\MyApp.exe" + // https://www.microsoft.com/developerblog/2015/11/09/reading-and-writing-to-the-windows-registry-in-process-from-node-js/ + // https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/reg-add + // https://social.msdn.microsoft.com/Forums/en-US/5b318f44-281e-4098-8dee-3ba8435fa391/add-registry-key-for-autostart-of-app-in-ice?forum=quebectools + // utils.elevate + // https://github.com/CatalystCode/windows-registry-node + exec('where ' + progs.win, execOpts, function (err, stdout, stderr) { + //console.log((stdout||'').trim()); + if (stderr) { + console.error(stderr); + } + fn(err, progs.win); + }); + } +}; -- 2.38.5 From c3e9bbaa5aa74cf3334c11fdc95ab108361c8c75 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 25 Sep 2018 00:45:32 -0600 Subject: [PATCH 02/68] WIP keep parsing and console output in remote, move other stuff out --- bin/telebit-remote.js | 691 +++++++++++++++-------------------- lib/en-us.toml | 12 + lib/remote-control-client.js | 116 ++++++ 3 files changed, 423 insertions(+), 396 deletions(-) create mode 100644 lib/remote-control-client.js diff --git a/bin/telebit-remote.js b/bin/telebit-remote.js index 14a7b51..429a9c5 100755 --- a/bin/telebit-remote.js +++ b/bin/telebit-remote.js @@ -8,11 +8,11 @@ var os = require('os'); //var url = require('url'); var fs = require('fs'); var path = require('path'); -var http = require('http'); //var https = require('https'); var YAML = require('js-yaml'); var TOML = require('toml'); var TPLS = TOML.parse(fs.readFileSync(path.join(__dirname, "../lib/en-us.toml"), 'utf8')); + /* if ('function' !== typeof TOML.stringify) { TOML.stringify = require('json2toml'); @@ -314,395 +314,142 @@ function askForConfig(state, mainCb) { next(); } -var utils = { - request: function request(opts, fn) { - if (!opts) { opts = {}; } - var service = opts.service || 'config'; - var req = http.request({ - socketPath: state._ipc.path - , method: opts.method || 'GET' - , path: '/rpc/' + service - }, function (resp) { - var body = ''; - - function finish() { - if (200 !== resp.statusCode) { - console.warn(resp.statusCode); - console.warn(body || ('get' + service + ' failed')); - //cb(new Error("not okay"), body); - return; - } - - if (!body) { fn(null, null); return; } - - try { - body = JSON.parse(body); - } catch(e) { - // ignore - } - - fn(null, body); - } - - if (resp.headers['content-length']) { - resp.on('data', function (chunk) { - body += chunk.toString(); - }); - resp.on('end', function () { - finish(); - }); - } else { - finish(); - } - }); - req.on('error', function (err) { - // ENOENT - never started, cleanly exited last start, or creating socket at a different path - // ECONNREFUSED - leftover socket just needs to be restarted - if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) { - if (opts._taketwo) { - console.error("Either the telebit service was not already (and could not be started) or its socket could not be written to."); - console.error(err); - return; - } - require('../usr/share/install-launcher.js').install({ env: process.env }, function (err) { - if (err) { fn(err); return; } - opts._taketwo = true; - setTimeout(function () { - utils.request(opts, fn); - }, 2500); - }); - return; - } - if ('ENOTSOCK' === err.code) { - console.error(err); - return; - } - console.error(err); - return; - }); - req.end(); - } -, putConfig: function putConfig(service, args, fn) { - var req = http.request({ - socketPath: state._ipc.path - , method: 'POST' - , path: '/rpc/' + service + '?_body=' + encodeURIComponent(JSON.stringify(args)) - }, function (resp) { - - function finish() { - if ('function' === typeof fn) { - fn(null, resp); - return; - } - - console.info(""); - if (200 !== resp.statusCode) { - console.warn("'" + service + "' may have failed." - + " Consider peaking at the logs either with 'journalctl -xeu telebit' or /opt/telebit/var/log/error.log"); - console.warn(resp.statusCode, body); - //cb(new Error("not okay"), body); - return; - } - - if (!body) { - console.info("👌"); - return; - } - - try { - body = JSON.parse(body); - } catch(e) { - // ignore - } - - if ("AWAIT_AUTH" === body.code) { - console.info(body.message); - } else if ("CONFIG" === body.code) { - delete body.code; - //console.info(TOML.stringify(body)); - console.info(YAML.safeDump(body)); - } else { - if ('http' === body.module) { - // TODO we'll support slingshot-ing in the future - if (String(body.local) === String(parseInt(body.local, 10))) { - console.info('> Forwarding https://' + body.remote + ' => localhost:' + body.local); - } else { - console.info('> Serving ' + body.local + ' as https://' + body.remote); - } - } else if ('tcp' === body.module) { - console.info('> Forwarding ' + state.config.relay + ':' + body.remote + ' => localhost:' + body.local); - } else if ('ssh' === body.module) { - //console.info('> Forwarding ' + state.config.relay + ' -p ' + JSON.stringify(body) + ' => localhost:' + body.local); - console.info('> Forwarding ssh+https (openssl proxy) => localhost:' + body.local); - } else { - console.info(JSON.stringify(body, null, 2)); - } - console.info(); - } - } - - var body = ''; - if (resp.headers['content-length']) { - resp.on('data', function (chunk) { - body += chunk.toString(); - }); - resp.on('end', function () { - finish(); - }); - } else { - finish(); - } - }); - req.on('error', function (err) { - console.error('Put Config Error:'); - console.error(err); - return; - }); - req.end(); - } -}; - -// Two styles: -// http 3000 -// http modulename -function makeRpc(key) { - if (key !== argv[0]) { - return false; - } - utils.putConfig(argv[0], argv.slice(1)); - return true; -} - -function packConfig(config) { - return Object.keys(config).map(function (key) { - var val = config[key]; - if ('undefined' === val) { - throw new Error("'undefined' used as a string value"); - } - if ('undefined' === typeof val) { - //console.warn('[DEBUG]', key, 'is present but undefined'); - return; - } - if (val && 'object' === typeof val && !Array.isArray(val)) { - val = JSON.stringify(val); - } - return key + ':' + val; // converts arrays to strings with , - }); -} - -function getToken(err, state) { - if (err) { - console.error("Error while initializing config [init]:"); - throw err; - } - state.relay = state.config.relay; - - // { _otp, config: {} } - common.api.token(state, { - error: function (err/*, next*/) { - console.error("[Error] common.api.token:"); - console.error(err); - return; - } - , directory: function (dir, next) { - //console.log('[directory] Telebit Relay Discovered:'); - //console.log(dir); - state._apiDirectory = dir; - next(); - } - , tunnelUrl: function (tunnelUrl, next) { - //console.log('[tunnelUrl] Telebit Relay Tunnel Socket:', tunnelUrl); - state.wss = tunnelUrl; - next(); - } - , requested: function (authReq, next) { - //console.log("[requested] Pairing Requested"); - state.config._otp = state.config._otp = authReq.otp; - - if (!state.config.token && state._can_pair) { - console.info(""); - console.info("=============================================="); - console.info(" Hey, Listen! "); - console.info("=============================================="); - console.info(" "); - console.info(" GO CHECK YOUR EMAIL! "); - console.info(" "); - console.info(" DEVICE PAIR CODE: 0000 ".replace(/0000/g, state.config._otp)); - console.info(" "); - console.info("=============================================="); - console.info(""); - } - - next(); - } - , connect: function (pretoken, next) { - //console.log("[connect] Enabling Pairing Locally..."); - state.config.pretoken = pretoken; - state._connecting = true; - - // TODO use php-style object querification - utils.putConfig('config', packConfig(state.config), function (err/*, body*/) { - if (err) { - state._error = err; - console.error("Error while initializing config [connect]:"); - console.error(err); - return; - } - console.info("waiting..."); - next(); - }); - } - , offer: function (token, next) { - //console.log("[offer] Pairing Enabled by Relay"); - state.config.token = token; - if (state._error) { - return; - } - state._connecting = true; - try { - require('jsonwebtoken').decode(token); - //console.log(require('jsonwebtoken').decode(token)); - } catch(e) { - console.warn("[warning] could not decode token"); - } - utils.putConfig('config', packConfig(state.config), function (err/*, body*/) { - if (err) { - state._error = err; - console.error("Error while initializing config [offer]:"); - console.error(err); - return; - } - //console.log("Pairing Enabled Locally"); - next(); - }); - } - , granted: function (_, next) { - //console.log("[grant] Pairing complete!"); - next(); - } - , end: function () { - utils.putConfig('enable', [], function (err) { - if (err) { console.error(err); return; } - console.info("Success"); - - // workaround for https://github.com/nodejs/node/issues/21319 - if (state._useTty) { - setTimeout(function () { - console.info("Some fun things to try first:\n"); - console.info(" ~/telebit http ~/public"); - console.info(" ~/telebit tcp 5050"); - console.info(" ~/telebit ssh auto"); - console.info(); - console.info("Press any key to continue..."); - console.info(); - process.exit(0); - }, 0.5 * 1000); - return; - } - // end workaround - - parseCli(state); - }); - } - }); -} - -function parseCli(/*state*/) { - var special = [ - 'false', 'none', 'off', 'disable' - , 'true', 'auto', 'on', 'enable' - ]; - if (-1 !== argv.indexOf('init')) { - utils.putConfig('list', []/*, function (err) { - }*/); - return; - } - - if ([ 'ssh', 'http', 'tcp' ].some(function (key) { - if (key !== argv[0]) { - return false; - } - if (argv[1]) { - if (String(argv[1]) === String(parseInt(argv[1], 10))) { - // looks like a port - argv[1] = parseInt(argv[1], 10); - } else if (/\/|\\/.test(argv[1])) { - // looks like a path - argv[1] = path.resolve(argv[1]); - // TODO make a default assignment here - } else if (-1 === special.indexOf(argv[1])) { - console.error("Not sure what you meant by '" + argv[1] + "'."); - console.error("Remember: paths should begin with ." + path.sep + ", like '." + path.sep + argv[1] + "'"); - return true; - } - utils.putConfig(argv[0], argv.slice(1)); - return true; - } - return true; - })) { - return; - } - - if ([ 'status', 'enable', 'disable', 'restart', 'list', 'save' ].some(makeRpc)) { - return; - } - - help(); - process.exit(11); -} - -function handleConfig(err, config) { - //console.log('CONFIG'); - //console.log(config); - state.config = config; - var verstrd = [ pkg.name + ' daemon v' + state.config.version ]; - if (state.config.version && state.config.version !== pkg.version) { - console.info(verstr.join(' '), verstrd.join(' ')); - } else { - console.info(verstr.join(' ')); - } - - if (err) { console.error(err); process.exit(101); return; } - - // - // check for init first, before anything else - // because it has arguments that may help in - // the next steps - // - if (-1 !== argv.indexOf('init')) { - parsers.init(argv, getToken); - return; - } - - if (!state.config.relay || !state.config.token) { - if (!state.config.relay) { - state.config.relay = 'telebit.cloud'; - } - - //console.log("question the user?", Date.now()); - askForConfig(state, function (err, state) { - // no errors actually get passed, so this is just future-proofing - if (err) { throw err; } - - if (!state.config.token && state._can_pair) { - state.config._otp = common.otp(); - } - - //console.log("done questioning:", Date.now()); - if (!state.token && !state.config.token) { - getToken(err, state); - } else { - parseCli(state); - } - }); - return; - } - - //console.log("no questioning:"); - parseCli(state); -} +var RC; function parseConfig(err, text) { + function handleConfig(err, config) { + //console.log('CONFIG'); + //console.log(config); + state.config = config; + var verstrd = [ pkg.name + ' daemon v' + state.config.version ]; + if (state.config.version && state.config.version !== pkg.version) { + console.info(verstr.join(' '), verstrd.join(' ')); + } else { + console.info(verstr.join(' ')); + } + + if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) { + console.error("Either the telebit service was not already (and could not be started) or its socket could not be written to."); + console.error(err); + } else if ('ENOTSOCK' === err.code) { + console.error(err); + return; + } else { + console.error(err); + } + if (err) { process.exit(101); return; } + + // + // check for init first, before anything else + // because it has arguments that may help in + // the next steps + // + if (-1 !== argv.indexOf('init')) { + parsers.init(argv, function (err) { + if (err) { + console.error("Error while initializing config [init]:"); + throw err; + } + getToken(function (err) { + if (err) { + console.error("Error while getting token [init]:"); + throw err; + } + parseCli(state); + }); + }); + return; + } + + if (!state.config.relay || !state.config.token) { + if (!state.config.relay) { + state.config.relay = 'telebit.cloud'; + } + + //console.log("question the user?", Date.now()); + askForConfig(state, function (err, state) { + // no errors actually get passed, so this is just future-proofing + if (err) { throw err; } + + if (!state.config.token && state._can_pair) { + state.config._otp = common.otp(); + } + + //console.log("done questioning:", Date.now()); + if (!state.token && !state.config.token) { + if (err) { + console.error("Error while initializing config [init]:"); + throw err; + } + getToken(function (err) { + if (err) { + console.error("Error while getting token [init]:"); + throw err; + } + parseCli(state); + }); + } else { + parseCli(state); + } + }); + return; + } + + //console.log("no questioning:"); + parseCli(state); + } + function parseCli(/*state*/) { + var special = [ + 'false', 'none', 'off', 'disable' + , 'true', 'auto', 'on', 'enable' + ]; + if (-1 !== argv.indexOf('init')) { + RC.request({ service: 'list', method: 'POST', data: [] }, handleRemoteRequest('list')); + return; + } + + if ([ 'ssh', 'http', 'tcp' ].some(function (key) { + if (key !== argv[0]) { + return false; + } + if (argv[1]) { + if (String(argv[1]) === String(parseInt(argv[1], 10))) { + // looks like a port + argv[1] = parseInt(argv[1], 10); + } else if (/\/|\\/.test(argv[1])) { + // looks like a path + argv[1] = path.resolve(argv[1]); + // TODO make a default assignment here + } else if (-1 === special.indexOf(argv[1])) { + console.error("Not sure what you meant by '" + argv[1] + "'."); + console.error("Remember: paths should begin with ." + path.sep + ", like '." + path.sep + argv[1] + "'"); + return true; + } + RC.request({ service: argv[0], method: 'POST', data: argv.slice(1) }, handleRemoteRequest(argv[0])); + return true; + } + return true; + })) { + return; + } + + // Two styles: + // http 3000 + // http modulename + function makeRpc(key) { + if (key !== argv[0]) { + return false; + } + RC.request({ service: argv[0], method: 'POST', data: argv.slice(1) }, handleRemoteRequest(argv[0])); + return true; + } + if ([ 'status', 'enable', 'disable', 'restart', 'list', 'save' ].some(makeRpc)) { + return; + } + + help(); + process.exit(11); + } try { state._clientConfig = JSON.parse(text || '{}'); } catch(e1) { @@ -721,13 +468,7 @@ function parseConfig(err, text) { } state._clientConfig = camelCopy(state._clientConfig || {}) || {}; - common._init( - // make a default working dir and log dir - state._clientConfig.root || path.join(os.homedir(), '.local/share/telebit') - , (state._clientConfig.root && path.join(state._clientConfig.root, 'etc')) - || path.resolve(common.DEFAULT_CONFIG_PATH, '..') - ); - state._ipc = common.pipename(state._clientConfig, true); + RC = require('../lib/remote-control-client.js').create(state); if (!Object.keys(state._clientConfig).length) { console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')'); @@ -742,7 +483,165 @@ function parseConfig(err, text) { } } - utils.request({ service: 'config' }, handleConfig); + function handleRemoteRequest(service, fn) { + return function (err, body) { + if ('function' === typeof fn) { + fn(err, body); // XXX was resp + return; + } + console.info(""); + if (err) { + console.warn("'" + service + "' may have failed." + + " Consider peaking at the logs either with 'journalctl -xeu telebit' or /opt/telebit/var/log/error.log"); + console.warn(err.statusCode, err.message); + //cb(new Error("not okay"), body); + return; + } + + if (!body) { + console.info("👌"); + return; + } + + try { + body = JSON.parse(body); + } catch(e) { + // ignore + + } + + if ("AWAIT_AUTH" === body.code) { + console.info(body.message); + } else if ("CONFIG" === body.code) { + delete body.code; + //console.info(TOML.stringify(body)); + console.info(YAML.safeDump(body)); + } else { + if ('http' === body.module) { + // TODO we'll support slingshot-ing in the future + if (String(body.local) === String(parseInt(body.local, 10))) { + console.info('> Forwarding https://' + body.remote + ' => localhost:' + body.local); + } else { + console.info('> Serving ' + body.local + ' as https://' + body.remote); + } + } else if ('tcp' === body.module) { + console.info('> Forwarding ' + state.config.relay + ':' + body.remote + ' => localhost:' + body.local); + } else if ('ssh' === body.module) { + //console.info('> Forwarding ' + state.config.relay + ' -p ' + JSON.stringify(body) + ' => localhost:' + body.local); + console.info('> Forwarding ssh+https (openssl proxy) => localhost:' + body.local); + } else { + console.info(JSON.stringify(body, null, 2)); + } + console.info(); + } + }; + } + + function getToken(fn) { + state.relay = state.config.relay; + + // { _otp, config: {} } + common.api.token(state, { + error: function (err/*, next*/) { + console.error("[Error] common.api.token:"); + console.error(err); + return; + } + , directory: function (dir, next) { + //console.log('[directory] Telebit Relay Discovered:'); + //console.log(dir); + state._apiDirectory = dir; + next(); + } + , tunnelUrl: function (tunnelUrl, next) { + //console.log('[tunnelUrl] Telebit Relay Tunnel Socket:', tunnelUrl); + state.wss = tunnelUrl; + next(); + } + , requested: function (authReq, next) { + //console.log("[requested] Pairing Requested"); + state.config._otp = state.config._otp = authReq.otp; + + if (!state.config.token && state._can_pair) { + console.info(TPLS.remote.code.replace(/0000/g, state.config._otp)); + } + + next(); + } + , connect: function (pretoken, next) { + //console.log("[connect] Enabling Pairing Locally..."); + state.config.pretoken = pretoken; + state._connecting = true; + + // TODO use php-style object querification + RC.request({ service: 'config', method: 'POST', data: state.config }, handleRemoteRequest('config', function (err/*, body*/) { + if (err) { + state._error = err; + console.error("Error while initializing config [connect]:"); + console.error(err); + return; + } + console.info("waiting..."); + next(); + })); + } + , offer: function (token, next) { + //console.log("[offer] Pairing Enabled by Relay"); + state.config.token = token; + if (state._error) { + return; + } + state._connecting = true; + try { + require('jsonwebtoken').decode(token); + //console.log(require('jsonwebtoken').decode(token)); + } catch(e) { + console.warn("[warning] could not decode token"); + } + RC.request({ service: 'config', method: 'POST', data: state.config }, handleRemoteRequest('config', function (err/*, body*/) { + if (err) { + state._error = err; + console.error("Error while initializing config [offer]:"); + console.error(err); + return; + } + //console.log("Pairing Enabled Locally"); + next(); + })); + } + , granted: function (_, next) { + //console.log("[grant] Pairing complete!"); + next(); + } + , end: function () { + RC.request({ service: 'enable', method: 'POST', data: [] }, handleRemoteRequest('enable', function (err) { + if (err) { console.error(err); return; } + console.info("Success"); + + // workaround for https://github.com/nodejs/node/issues/21319 + if (state._useTty) { + setTimeout(function () { + console.info("Some fun things to try first:\n"); + console.info(" ~/telebit http ~/public"); + console.info(" ~/telebit tcp 5050"); + console.info(" ~/telebit ssh auto"); + console.info(); + console.info("Press any key to continue..."); + console.info(); + process.exit(0); + }, 0.5 * 1000); + return; + } + // end workaround + + //parseCli(state); + fn(); + })); + } + }); + } + + RC.request({ service: 'config', method: 'GET' }, handleRemoteRequest('config', handleConfig)); } var parsers = { diff --git a/lib/en-us.toml b/lib/en-us.toml index c4c845d..167e75a 100644 --- a/lib/en-us.toml +++ b/lib/en-us.toml @@ -452,5 +452,17 @@ The secret flags are: [remote] version = "telebit remote v{version}" +code = " +============================================== + Hey, Listen! +============================================== + + GO CHECK YOUR EMAIL! + + DEVICE PAIR CODE: 0000 + +============================================== +" + [daemon] version = "telebit daemon v{version}" diff --git a/lib/remote-control-client.js b/lib/remote-control-client.js new file mode 100644 index 0000000..0f284fd --- /dev/null +++ b/lib/remote-control-client.js @@ -0,0 +1,116 @@ +'use strict'; + +var os = require('os'); +var path = require('path'); +var http = require('http'); + +var common = require('./cli-common.js'); + +function packConfig(config) { + return Object.keys(config).map(function (key) { + var val = config[key]; + if ('undefined' === val) { + throw new Error("'undefined' used as a string value"); + } + if ('undefined' === typeof val) { + //console.warn('[DEBUG]', key, 'is present but undefined'); + return; + } + if (val && 'object' === typeof val && !Array.isArray(val)) { + val = JSON.stringify(val); + } + return key + ':' + val; // converts arrays to strings with , + }); +} + +module.exports.create = function (state) { + common._init( + // make a default working dir and log dir + state._clientConfig.root || path.join(os.homedir(), '.local/share/telebit') + , (state._clientConfig.root && path.join(state._clientConfig.root, 'etc')) + || path.resolve(common.DEFAULT_CONFIG_PATH, '..') + ); + state._ipc = common.pipename(state._clientConfig, true); + + function makeResponder(service, resp, fn) { + var body = ''; + + function finish() { + var err; + + if (200 !== resp.statusCode) { + err = new Error(body || ('get' + service + ' failed')); + err.statusCode = resp.statusCode; + err.code = "E_REQUEST"; + } + + try { + body = JSON.parse(body); + } catch(e) { + // ignore + } + + fn(err, body); + } + + if (!resp.headers['content-length'] && !resp.headers['content-type']) { + finish(); + return; + } + + // TODO use readable + resp.on('data', function (chunk) { + body += chunk.toString(); + }); + resp.on('end', finish); + } + + var RC = {}; + RC.request = function request(opts, fn) { + if (!opts) { opts = {}; } + var service = opts.service || 'config'; + var args = opts.data; + if (args && 'control' === service) { + args = packConfig(args); + } + var json = JSON.stringify(args); + var url = '/rpc/' + service; + if (json) { + url += ('?_body=' + encodeURIComponent(json)); + } + var method = opts.method || (args && 'POST') || 'GET'; + var req = http.request({ + socketPath: state._ipc.path + , method: method + , path: url + }, function (resp) { + makeResponder(service, resp, fn); + }); + + req.on('error', function (err) { + // ENOENT - never started, cleanly exited last start, or creating socket at a different path + // ECONNREFUSED - leftover socket just needs to be restarted + if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) { + if (opts._taketwo) { + fn(err); + return; + } + require('../usr/share/install-launcher.js').install({ env: process.env }, function (err) { + if (err) { fn(err); return; } + opts._taketwo = true; + setTimeout(function () { + RC.request(opts, fn); + }, 2500); + }); + return; + } + + fn(err); + }); + if ('POST' === method && opts.data) { + req.write(json || opts.data); + } + req.end(); + }; + return RC; +}; -- 2.38.5 From 3e0c97751174b03a10b36b5aa2303cf9ea5f4b76 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 25 Sep 2018 00:54:51 -0600 Subject: [PATCH 03/68] fix a few WIP parser bugs --- bin/telebit-remote.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/bin/telebit-remote.js b/bin/telebit-remote.js index 429a9c5..bb4f51f 100755 --- a/bin/telebit-remote.js +++ b/bin/telebit-remote.js @@ -328,16 +328,19 @@ function parseConfig(err, text) { console.info(verstr.join(' ')); } - if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) { - console.error("Either the telebit service was not already (and could not be started) or its socket could not be written to."); - console.error(err); - } else if ('ENOTSOCK' === err.code) { - console.error(err); + if (err) { + if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) { + console.error("Either the telebit service was not already (and could not be started) or its socket could not be written to."); + console.error(err); + } else if ('ENOTSOCK' === err.code) { + console.error(err); + return; + } else { + console.error(err); + } + process.exit(101); return; - } else { - console.error(err); } - if (err) { process.exit(101); return; } // // check for init first, before anything else @@ -428,6 +431,7 @@ function parseConfig(err, text) { RC.request({ service: argv[0], method: 'POST', data: argv.slice(1) }, handleRemoteRequest(argv[0])); return true; } + help(); return true; })) { return; -- 2.38.5 From 82f5545d72ed084f7662ceb8c078eb46b7e1abe2 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 25 Sep 2018 01:14:54 -0600 Subject: [PATCH 04/68] wip minor refactoring --- bin/telebit-remote.js | 3 ++- bin/telebitd.js | 2 +- lib/{remote.js => daemon/index.js} | 10 ++++++---- lib/{remote-control-client.js => rc/index.js} | 2 +- package.json | 4 ++-- 5 files changed, 12 insertions(+), 9 deletions(-) rename lib/{remote.js => daemon/index.js} (98%) rename lib/{remote-control-client.js => rc/index.js} (98%) diff --git a/bin/telebit-remote.js b/bin/telebit-remote.js index bb4f51f..0d53829 100755 --- a/bin/telebit-remote.js +++ b/bin/telebit-remote.js @@ -401,6 +401,7 @@ function parseConfig(err, text) { //console.log("no questioning:"); parseCli(state); } + function parseCli(/*state*/) { var special = [ 'false', 'none', 'off', 'disable' @@ -472,7 +473,7 @@ function parseConfig(err, text) { } state._clientConfig = camelCopy(state._clientConfig || {}) || {}; - RC = require('../lib/remote-control-client.js').create(state); + RC = require('../lib/rc/index.js').create(state); if (!Object.keys(state._clientConfig).length) { console.info('(' + state._ipc.comment + ": " + state._ipc.path + ')'); diff --git a/bin/telebitd.js b/bin/telebitd.js index 4014b8c..245901f 100755 --- a/bin/telebitd.js +++ b/bin/telebitd.js @@ -24,7 +24,7 @@ var camelCopy = recase.camelCopy.bind(recase); var snakeCopy = recase.snakeCopy.bind(recase); var TPLS = TOML.parse(fs.readFileSync(path.join(__dirname, "../lib/en-us.toml"), 'utf8')); -var TelebitRemote = require('../').TelebitRemote; +var TelebitRemote = require('../lib/daemon/index.js').TelebitRemote; var state = { homedir: os.homedir(), servernames: {}, ports: {}, keepAlive: { state: false } }; diff --git a/lib/remote.js b/lib/daemon/index.js similarity index 98% rename from lib/remote.js rename to lib/daemon/index.js index 0f59c7b..22fcd7b 100644 --- a/lib/remote.js +++ b/lib/daemon/index.js @@ -28,6 +28,7 @@ function TelebitRemote(state) { EventEmitter.call(this); var me = this; var priv = {}; + var path = require('path'); //var defaultHttpTimeout = (2 * 60); //var activityTimeout = state.activityTimeout || (defaultHttpTimeout - 5) * 1000; @@ -39,8 +40,9 @@ function TelebitRemote(state) { priv.tokens = []; var auth; if(!state.sortingHat) { - state.sortingHat = "./sorting-hat.js"; + state.sortingHat = path.join(__dirname, '../sorting-hat.js'); } + state._connectionHandler = require(state.sortingHat); if (state.token) { if ('undefined' === state.token) { throw new Error("passed string 'undefined' as token"); @@ -349,7 +351,7 @@ function TelebitRemote(state) { // TODO use readable streams instead wstunneler._socket.pause(); - require(state.sortingHat).assign(state, tun, function (err, conn) { + state._connectionHandler.assign(state, tun, function (err, conn) { if (err) { err.message = err.message.replace(/:tun_id/, tun._id); packerHandlers._onConnectError(cid, tun, err); @@ -472,12 +474,12 @@ function TelebitRemote(state) { priv.timeoutId = null; var machine = Packer.create(packerHandlers); - console.info("[telebit:lib/remote.js] [connect] '" + (state.wss || state.relay) + "'"); + console.info("[telebit:lib/daemon.js] [connect] '" + (state.wss || state.relay) + "'"); var tunnelUrl = (state.wss || state.relay).replace(/\/$/, '') + '/'; // + auth; wstunneler = new WebSocket(tunnelUrl, { rejectUnauthorized: !state.insecure }); // XXXXXX wstunneler.on('open', function () { - console.info("[telebit:lib/remote.js] [open] connected to '" + (state.wss || state.relay) + "'"); + console.info("[telebit:lib/daemon.js] [open] connected to '" + (state.wss || state.relay) + "'"); me.emit('connect'); priv.refreshTimeout(); priv.timeoutId = setTimeout(priv.checkTimeout, activityTimeout); diff --git a/lib/remote-control-client.js b/lib/rc/index.js similarity index 98% rename from lib/remote-control-client.js rename to lib/rc/index.js index 0f284fd..6d9bb12 100644 --- a/lib/remote-control-client.js +++ b/lib/rc/index.js @@ -4,7 +4,7 @@ var os = require('os'); var path = require('path'); var http = require('http'); -var common = require('./cli-common.js'); +var common = require('../cli-common.js'); function packConfig(config) { return Object.keys(config).map(function (key) { diff --git a/package.json b/package.json index 289ad92..1edbf3e 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "telebit", - "version": "0.20.4", + "version": "0.20.5-wip", "description": "Break out of localhost. Connect to any device from anywhere over any tcp port or securely in a browser. A secure tunnel. A poor man's reverse VPN.", - "main": "lib/remote.js", + "main": "lib/daemon/index.js", "files": [ "bin", "lib", -- 2.38.5 From aad592c8931954ddb58396e843a10f8ba1f9203e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 25 Sep 2018 01:20:15 -0600 Subject: [PATCH 05/68] clarification --- bin/telebit.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/telebit.js b/bin/telebit.js index 21dbc68..c880681 100644 --- a/bin/telebit.js +++ b/bin/telebit.js @@ -21,6 +21,7 @@ if ('rsync' === process.argv[2]) { require('sclient/bin/sclient.js'); return; } +// handle ssh client rather than ssh https tunnel if ('ssh' === process.argv[2] && /[\w-]+\.[a-z]{2,}/i.test(process.argv[3])) { process.argv.splice(1,1,'sclient'); process.argv.splice(2,1,'ssh'); -- 2.38.5 From 6c9d13f15507b0b4696160ce93dc31a5fd1d7cde Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 25 Sep 2018 01:26:47 -0600 Subject: [PATCH 06/68] update require path --- lib/rc/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rc/index.js b/lib/rc/index.js index 6d9bb12..389458d 100644 --- a/lib/rc/index.js +++ b/lib/rc/index.js @@ -95,7 +95,7 @@ module.exports.create = function (state) { fn(err); return; } - require('../usr/share/install-launcher.js').install({ env: process.env }, function (err) { + require('../../usr/share/install-launcher.js').install({ env: process.env }, function (err) { if (err) { fn(err); return; } opts._taketwo = true; setTimeout(function () { -- 2.38.5 From 8f7e865d491af2c04a78c4a1b31206f975dd31fe Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 25 Sep 2018 02:18:38 -0600 Subject: [PATCH 07/68] move from json-in-querystring to POST bodies --- bin/telebit-remote.js | 20 ++++-- bin/telebitd.js | 143 +++++++++++++++++++++++++----------------- lib/rc/index.js | 1 + 3 files changed, 102 insertions(+), 62 deletions(-) diff --git a/bin/telebit-remote.js b/bin/telebit-remote.js index 0d53829..3ba1b1e 100755 --- a/bin/telebit-remote.js +++ b/bin/telebit-remote.js @@ -524,16 +524,28 @@ function parseConfig(err, text) { } else { if ('http' === body.module) { // TODO we'll support slingshot-ing in the future - if (String(body.local) === String(parseInt(body.local, 10))) { - console.info('> Forwarding https://' + body.remote + ' => localhost:' + body.local); + if (body.local) { + if (String(body.local) === String(parseInt(body.local, 10))) { + console.info('> Forwarding https://' + body.remote + ' => localhost:' + body.local); + } else { + console.info('> Serving ' + body.local + ' as https://' + body.remote); + } } else { - console.info('> Serving ' + body.local + ' as https://' + body.remote); + console.info('> Rejecting End-to-End Encrypted HTTPS for now'); } } else if ('tcp' === body.module) { + if (body.local) { console.info('> Forwarding ' + state.config.relay + ':' + body.remote + ' => localhost:' + body.local); + } else { + console.info('> Rejecting Legacy TCP'); + } } else if ('ssh' === body.module) { - //console.info('> Forwarding ' + state.config.relay + ' -p ' + JSON.stringify(body) + ' => localhost:' + body.local); + //console.info('> Forwarding ' + state.config.relay + ' -p ' + JSON.stringify(body) + ' => localhost:' + body.local); + if (body.local) { console.info('> Forwarding ssh+https (openssl proxy) => localhost:' + body.local); + } else { + console.info('> Rejecting SSH-over-HTTPS for now'); + } } else { console.info(JSON.stringify(body, null, 2)); } diff --git a/bin/telebitd.js b/bin/telebitd.js index 74bbdb1..ee76d64 100755 --- a/bin/telebitd.js +++ b/bin/telebitd.js @@ -245,11 +245,11 @@ controllers.tcp = function (req, res, opts) { if (remotePort) { if (!state.ports[remotePort]) { active = false; - return; + } else { + // forward-to port-or-module + // TODO with the connect event bug fixed, we should now be able to send files over tcp + state.ports[remotePort].handler = portOrPath; } - // forward-to port-or-module - // TODO we can't send files over tcp until we fix the connect event bug - state.ports[remotePort].handler = portOrPath; } else { if (!Object.keys(state.ports).some(function (key) { if (!state.ports[key].handler) { @@ -329,7 +329,7 @@ controllers.ssh = function (req, res, opts) { function serveControlsHelper() { controlServer = http.createServer(function (req, res) { var opts = url.parse(req.url, true); - if (opts.query._body) { + if (false && opts.query._body) { try { opts.body = JSON.parse(decodeURIComponent(opts.query._body, true)); } catch(e) { @@ -591,63 +591,90 @@ function serveControlsHelper() { )); } - if (/\b(config)\b/.test(opts.pathname) && /get/i.test(req.method)) { - getConfigOnly(); - return; + function route() { + if (/\b(config)\b/.test(opts.pathname) && /get/i.test(req.method)) { + getConfigOnly(); + return; + } + if (/\b(init|config)\b/.test(opts.pathname)) { + initOrConfig(); + return; + } + if (/restart/.test(opts.pathname)) { + restart(); + return; + } + // + // Check for proper config + // + if (!state.config.relay || !state.config.email || !state.config.agreeTos) { + invalidConfig(); + return; + } + // + // With proper config + // + if (/http/.test(opts.pathname)) { + controllers.http(req, res, opts); + return; + } + if (/tcp/.test(opts.pathname)) { + controllers.tcp(req, res, opts); + return; + } + if (/save|commit/.test(opts.pathname)) { + saveAndCommit(); + return; + } + if (/ssh/.test(opts.pathname)) { + controllers.ssh(req, res, opts); + return; + } + if (/enable/.test(opts.pathname)) { + enable(); + return; + } + if (/disable/.test(opts.pathname)) { + disable(); + return; + } + if (/status/.test(opts.pathname)) { + getStatus(); + return; + } + if (/list/.test(opts.pathname)) { + listSuccess(); + return; + } + + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({"error":{"message":"unrecognized rpc"}})); } - if (/\b(init|config)\b/.test(opts.pathname)) { - initOrConfig(); - return; - } - if (/restart/.test(opts.pathname)) { - restart(); - return; - } - // - // Check for proper config - // - if (!state.config.relay || !state.config.email || !state.config.agreeTos) { - invalidConfig(); - return; - } - // - // With proper config - // - if (/http/.test(opts.pathname)) { - controllers.http(req, res, opts); - return; - } - if (/tcp/.test(opts.pathname)) { - controllers.tcp(req, res, opts); - return; - } - if (/save|commit/.test(opts.pathname)) { - saveAndCommit(); - return; - } - if (/ssh/.test(opts.pathname)) { - controllers.ssh(req, res, opts); - return; - } - if (/enable/.test(opts.pathname)) { - enable(); - return; - } - if (/disable/.test(opts.pathname)) { - disable(); - return; - } - if (/status/.test(opts.pathname)) { - getStatus(); - return; - } - if (/list/.test(opts.pathname)) { - listSuccess(); + + if (!req.headers['content-length'] && !req.headers['content-type']) { + route(); return; } - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({"error":{"message":"unrecognized rpc"}})); + var body = ''; + req.on('readable', function () { + var data; + while (true) { + data = req.read(); + if (!data) { break; } + body += data.toString(); + } + }); + req.on('end', function () { + try { + opts.body = JSON.parse(body); + } catch(e) { + res.statusCode = 400; + res.end('{"error":{"message":"POST body is not valid json"}}'); + return; + } + route(); + }); }); if (fs.existsSync(state._ipc.path)) { diff --git a/lib/rc/index.js b/lib/rc/index.js index 389458d..acd0dc7 100644 --- a/lib/rc/index.js +++ b/lib/rc/index.js @@ -108,6 +108,7 @@ module.exports.create = function (state) { fn(err); }); if ('POST' === method && opts.data) { + req.setHeader("content-type", 'application/json'); req.write(json || opts.data); } req.end(); -- 2.38.5 From be7a895dc7798d4a84537076f7c459f573579f33 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 15 Oct 2018 20:37:07 -0600 Subject: [PATCH 08/68] WIP web remote client --- bin/telebit-remote.js | 39 +- bin/telebitd.js | 644 +-- lib/admin/index.html | 60 + lib/admin/js/app.js | 50 + lib/admin/js/vue.js | 10947 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 11428 insertions(+), 312 deletions(-) create mode 100644 lib/admin/index.html create mode 100644 lib/admin/js/app.js create mode 100644 lib/admin/js/vue.js diff --git a/bin/telebit-remote.js b/bin/telebit-remote.js index 14a7b51..d97c0d2 100755 --- a/bin/telebit-remote.js +++ b/bin/telebit-remote.js @@ -73,7 +73,6 @@ if (!confpath || /^--/.test(confpath)) { } function askForConfig(state, mainCb) { - var fs = require('fs'); var ttyname = '/dev/tty'; var stdin = useTty ? fs.createReadStream(ttyname, { fd: fs.openSync(ttyname, fs.constants.O_RDONLY | fs.constants.O_NOCTTY) @@ -318,11 +317,20 @@ var utils = { request: function request(opts, fn) { if (!opts) { opts = {}; } var service = opts.service || 'config'; - var req = http.request({ - socketPath: state._ipc.path - , method: opts.method || 'GET' + + var reqOpts = { + method: opts.method || 'GET' , path: '/rpc/' + service - }, function (resp) { + }; + var portFile = path.join(path.dirname(state._ipc.path), 'telebit.port'); + if (fs.existsSync(portFile)) { + reqOpts.host = 'localhost'; + reqOpts.port = parseInt(fs.readFileSync(portFile, 'utf8').trim(), 10); + } else { + reqOpts.socketPath = state._ipc.path; + } + + var req = http.request(reqOpts, function (resp) { var body = ''; function finish() { @@ -383,12 +391,20 @@ var utils = { req.end(); } , putConfig: function putConfig(service, args, fn) { - var req = http.request({ - socketPath: state._ipc.path - , method: 'POST' - , path: '/rpc/' + service + '?_body=' + encodeURIComponent(JSON.stringify(args)) - }, function (resp) { + var reqOpts = { + method: 'POST' + , path: '/rpc/' + service + '?_body=' + encodeURIComponent(JSON.stringify(args)) + }; + var portFile = path.join(path.dirname(state._ipc.path), 'telebit.port'); + if (fs.existsSync(portFile)) { + reqOpts.host = 'localhost'; + reqOpts.port = parseInt(fs.readFileSync(portFile, 'utf8').trim(), 10); + } else { + reqOpts.socketPath = state._ipc.path; + } + + var req = http.request(reqOpts, function (resp) { function finish() { if ('function' === typeof fn) { fn(null, resp); @@ -434,6 +450,9 @@ var utils = { } else if ('ssh' === body.module) { //console.info('> Forwarding ' + state.config.relay + ' -p ' + JSON.stringify(body) + ' => localhost:' + body.local); console.info('> Forwarding ssh+https (openssl proxy) => localhost:' + body.local); + } else if ('status' === body.module) { + console.info('http://localhost:' + reqOpts.port); + console.info(JSON.stringify(body, null, 2)); } else { console.info(JSON.stringify(body, null, 2)); } diff --git a/bin/telebitd.js b/bin/telebitd.js index d70f4df..722cbd8 100755 --- a/bin/telebitd.js +++ b/bin/telebitd.js @@ -326,329 +326,339 @@ controllers.ssh = function (req, res, opts) { state.config.sshAuto = sshAuto; sshSuccess(); }; -function serveControlsHelper() { - controlServer = http.createServer(function (req, res) { - var opts = url.parse(req.url, true); - if (opts.query._body) { - try { - opts.body = JSON.parse(decodeURIComponent(opts.query._body, true)); - } catch(e) { + +var serveStatic = require('serve-static')(path.join(__dirname, '../lib/admin/')); +function handleRemoteClient(req, res) { + if (/^\/(rpc|api)\//.test(req.url)) { + return handleApi(req, res); + } + serveStatic(req, res, require('finalhandler')(req, res)); +} +function handleApi(req, res) { + var opts = url.parse(req.url, true); + if (opts.query._body) { + try { + opts.body = JSON.parse(decodeURIComponent(opts.query._body, true)); + } catch(e) { + res.statusCode = 500; + res.end('{"error":{"message":"?_body={{bad_format}}"}}'); + return; + } + } + + function listSuccess() { + var dumpy = { + servernames: state.servernames + , ports: state.ports + , ssh: state.config.sshAuto || 'disabled' + , code: 'CONFIG' + }; + if (state.otp) { + dumpy.device_pair_code = state.otp; + } + + if (state._can_pair && state.config.email && !state.token) { + dumpy.code = "AWAIT_AUTH"; + dumpy.message = "Please run 'telebit init' to authenticate."; + } + + res.end(JSON.stringify(dumpy)); + } + + function getConfigOnly() { + var resp = JSON.parse(JSON.stringify(state.config)); + resp.version = pkg.version; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(resp)); + } + + // + // without proper config + // + function saveAndReport() { + console.log('[DEBUG] saveAndReport config write', confpath); + console.log(YAML.safeDump(snakeCopy(state.config))); + fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { + if (err) { res.statusCode = 500; - res.end('{"error":{"message":"?_body={{bad_format}}"}}'); + res.setHeader('Content-Type', 'application/json'); + res.end('{"error":{"message":"Could not save config file after init: ' + err.message.replace(/"/g, "'") + + '.\nPerhaps check that the file exists and your user has permissions to write it?"}}'); return; } + + listSuccess(); + }); + } + + function initOrConfig() { + var conf = {}; + if (!opts.body) { + res.statusCode = 422; + res.end('{"error":{"message":"module \'init\' needs more arguments"}}'); + return; + } + // relay, email, agree_tos, servernames, ports + // + opts.body.forEach(function (opt) { + var parts = opt.split(/:/); + if ('true' === parts[1]) { + parts[1] = true; + } else if ('false' === parts[1]) { + parts[1] = false; + } else if ('null' === parts[1]) { + parts[1] = null; + } else if ('undefined' === parts[1]) { + parts[1] = undefined; + } + conf[parts[0]] = parts[1]; + }); + + // TODO camelCase query + state.config.email = conf.email || state.config.email || ''; + if ('undefined' !== typeof conf.agreeTos + || 'undefined' !== typeof conf.agreeTos ) { + state.config.agreeTos = conf.agreeTos || conf.agree_tos; + } + state.otp = conf._otp; // this should only be done on the client side + state.config.relay = conf.relay || state.config.relay || ''; + console.log(); + console.log('conf.token', typeof conf.token, conf.token); + console.log('state.config.token', typeof state.config.token, state.config.token); + state.config.token = conf.token || state.config.token || null; + state.config.secret = conf.secret || state.config.secret || null; + state.pretoken = conf.pretoken || state.config.pretoken || null; + if (state.secret) { + console.log('state.secret'); + state.token = common.signToken(state); + } + if (!state.token) { + console.log('!state.token'); + state.token = conf._token; + } + console.log(); + console.log('JSON.stringify(conf)'); + console.log(JSON.stringify(conf)); + console.log(); + console.log('JSON.stringify(state)'); + console.log(JSON.stringify(state)); + console.log(); + if ('undefined' !== typeof conf.newsletter) { + state.config.newsletter = conf.newsletter; + } + if ('undefined' !== typeof conf.communityMember + || 'undefined' !== typeof conf.community_member) { + state.config.communityMember = conf.communityMember || conf.community_member; + } + if ('undefined' !== typeof conf.telemetry) { + state.config.telemetry = conf.telemetry; + } + if (conf._servernames) { + (conf._servernames||'').split(/,/g).forEach(function (key) { + if (!state.config.servernames[key]) { + state.config.servernames[key] = { sub: undefined }; + } + }); + } + if (conf._ports) { + (conf._ports||'').split(/,/g).forEach(function (key) { + if (!state.config.ports[key]) { + state.config.ports[key] = {}; + } + }); } - function listSuccess() { - var dumpy = { - servernames: state.servernames - , ports: state.ports - , ssh: state.config.sshAuto || 'disabled' - , code: 'CONFIG' - }; - if (state.otp) { - dumpy.device_pair_code = state.otp; - } + if (!state.config.relay || !state.config.email || !state.config.agreeTos) { + console.warn('missing config'); + res.statusCode = 400; - if (state._can_pair && state.config.email && !state.token) { - dumpy.code = "AWAIT_AUTH"; - dumpy.message = "Please run 'telebit init' to authenticate."; - } - - res.end(JSON.stringify(dumpy)); - } - - function getConfigOnly() { - var resp = JSON.parse(JSON.stringify(state.config)); - resp.version = pkg.version; res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(resp)); + res.end(JSON.stringify({ + error: { + code: "E_INIT" + , message: "Missing important config file params" + , _params: JSON.stringify(conf) + , _config: JSON.stringify(state.config) + , _body: JSON.stringify(opts.body) + } + })); + return; } - // - // without proper config - // - function saveAndReport() { - console.log('[DEBUG] saveAndReport config write', confpath); - console.log(YAML.safeDump(snakeCopy(state.config))); - fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { - if (err) { - res.statusCode = 500; - res.setHeader('Content-Type', 'application/json'); - res.end('{"error":{"message":"Could not save config file after init: ' + err.message.replace(/"/g, "'") - + '.\nPerhaps check that the file exists and your user has permissions to write it?"}}'); - return; - } + // init also means enable + delete state.config.disable; + safeStartTelebitRemote(true).then(saveAndReport).catch(handleError); + } - listSuccess(); - }); + function restart() { + console.info("[telebitd.js] server closing..."); + state.keepAlive.state = false; + if (myRemote) { + myRemote.end(); + myRemote.on('end', respondAndClose); + // failsafe + setTimeout(function () { + console.info("[telebitd.js] closing too slowly, force quit"); + respondAndClose(); + }, 5 * 1000); + } else { + respondAndClose(); } - function initOrConfig() { - var conf = {}; - if (!opts.body) { - res.statusCode = 422; - res.end('{"error":{"message":"module \'init\' needs more arguments"}}'); - return; - } - // relay, email, agree_tos, servernames, ports - // - opts.body.forEach(function (opt) { - var parts = opt.split(/:/); - if ('true' === parts[1]) { - parts[1] = true; - } else if ('false' === parts[1]) { - parts[1] = false; - } else if ('null' === parts[1]) { - parts[1] = null; - } else if ('undefined' === parts[1]) { - parts[1] = undefined; - } - conf[parts[0]] = parts[1]; + function respondAndClose() { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ success: true })); + controlServer.close(function () { + console.info("[telebitd.js] server closed"); + setTimeout(function () { + // system daemon will restart the process + process.exit(22); // use non-success exit code + }, 100); }); + } + } - // TODO camelCase query - state.config.email = conf.email || state.config.email || ''; - if ('undefined' !== typeof conf.agreeTos - || 'undefined' !== typeof conf.agreeTos ) { - state.config.agreeTos = conf.agreeTos || conf.agree_tos; - } - state.otp = conf._otp; // this should only be done on the client side - state.config.relay = conf.relay || state.config.relay || ''; - console.log(); - console.log('conf.token', typeof conf.token, conf.token); - console.log('state.config.token', typeof state.config.token, state.config.token); - state.config.token = conf.token || state.config.token || null; - state.config.secret = conf.secret || state.config.secret || null; - state.pretoken = conf.pretoken || state.config.pretoken || null; - if (state.secret) { - console.log('state.secret'); - state.token = common.signToken(state); - } - if (!state.token) { - console.log('!state.token'); - state.token = conf._token; - } - console.log(); - console.log('JSON.stringify(conf)'); - console.log(JSON.stringify(conf)); - console.log(); - console.log('JSON.stringify(state)'); - console.log(JSON.stringify(state)); - console.log(); - if ('undefined' !== typeof conf.newsletter) { - state.config.newsletter = conf.newsletter; - } - if ('undefined' !== typeof conf.communityMember - || 'undefined' !== typeof conf.community_member) { - state.config.communityMember = conf.communityMember || conf.community_member; - } - if ('undefined' !== typeof conf.telemetry) { - state.config.telemetry = conf.telemetry; - } - if (conf._servernames) { - (conf._servernames||'').split(/,/g).forEach(function (key) { - if (!state.config.servernames[key]) { - state.config.servernames[key] = { sub: undefined }; - } - }); - } - if (conf._ports) { - (conf._ports||'').split(/,/g).forEach(function (key) { - if (!state.config.ports[key]) { - state.config.ports[key] = {}; - } - }); - } - - if (!state.config.relay || !state.config.email || !state.config.agreeTos) { - console.warn('missing config'); - res.statusCode = 400; + function invalidConfig() { + res.statusCode = 400; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ + error: { code: "E_CONFIG", message: "Invalid config file. Please run 'telebit init'" } + })); + } + function saveAndCommit() { + state.config.servernames = state.servernames; + state.config.ports = state.ports; + fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { + if (err) { + res.statusCode = 500; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ - error: { - code: "E_INIT" - , message: "Missing important config file params" - , _params: JSON.stringify(conf) - , _config: JSON.stringify(state.config) - , _body: JSON.stringify(opts.body) - } + "error":{"message":"Could not save config file. Perhaps you're not running as root?"} })); return; } - - // init also means enable - delete state.config.disable; - safeStartTelebitRemote(true).then(saveAndReport).catch(handleError); - } - - function restart() { - console.info("[telebitd.js] server closing..."); - state.keepAlive.state = false; - if (myRemote) { - myRemote.end(); - myRemote.on('end', respondAndClose); - // failsafe - setTimeout(function () { - console.info("[telebitd.js] closing too slowly, force quit"); - respondAndClose(); - }, 5 * 1000); - } else { - respondAndClose(); - } - - function respondAndClose() { - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ success: true })); - controlServer.close(function () { - console.info("[telebitd.js] server closed"); - setTimeout(function () { - // system daemon will restart the process - process.exit(22); // use non-success exit code - }, 100); - }); - } - } - - function invalidConfig() { - res.statusCode = 400; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ - error: { code: "E_CONFIG", message: "Invalid config file. Please run 'telebit init'" } - })); - } - - function saveAndCommit() { - state.config.servernames = state.servernames; - state.config.ports = state.ports; - fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { - if (err) { - res.statusCode = 500; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ - "error":{"message":"Could not save config file. Perhaps you're not running as root?"} - })); - return; - } - listSuccess(); - }); - } - - function handleError(err) { - res.statusCode = 500; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ - error: { message: err.message, code: err.code } - })); - } - - function enable() { - delete state.config.disable;// = undefined; - state.keepAlive.state = true; - - fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { - if (err) { - err.message = "Could not save config file. Perhaps you're user doesn't have permission?"; - handleError(err); - return; - } - // TODO XXX myRemote.active - if (myRemote) { - listSuccess(); - return; - } - safeStartTelebitRemote(true).then(listSuccess).catch(handleError); - }); - } - - function disable() { - state.config.disable = true; - state.keepAlive.state = false; - - if (myRemote) { myRemote.end(); myRemote = null; } - fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { - res.setHeader('Content-Type', 'application/json'); - if (err) { - err.message = "Could not save config file. Perhaps you're user doesn't have permission?"; - handleError(err); - return; - } - res.end('{"success":true}'); - }); - } - - function getStatus() { - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify( - { status: (state.config.disable ? 'disabled' : 'enabled') - , ready: ((state.config.relay && (state.config.token || state.config.agreeTos)) ? true : false) - , active: !!myRemote - , connected: 'maybe (todo)' - , version: pkg.version - , servernames: state.servernames - } - )); - } - - if (/\b(config)\b/.test(opts.pathname) && /get/i.test(req.method)) { - getConfigOnly(); - return; - } - if (/\b(init|config)\b/.test(opts.pathname)) { - initOrConfig(); - return; - } - if (/restart/.test(opts.pathname)) { - restart(); - return; - } - // - // Check for proper config - // - if (!state.config.relay || !state.config.email || !state.config.agreeTos) { - invalidConfig(); - return; - } - // - // With proper config - // - if (/http/.test(opts.pathname)) { - controllers.http(req, res, opts); - return; - } - if (/tcp/.test(opts.pathname)) { - controllers.tcp(req, res, opts); - return; - } - if (/save|commit/.test(opts.pathname)) { - saveAndCommit(); - return; - } - if (/ssh/.test(opts.pathname)) { - controllers.ssh(req, res, opts); - return; - } - if (/enable/.test(opts.pathname)) { - enable(); - return; - } - if (/disable/.test(opts.pathname)) { - disable(); - return; - } - if (/status/.test(opts.pathname)) { - getStatus(); - return; - } - if (/list/.test(opts.pathname)) { listSuccess(); - return; - } + }); + } + function handleError(err) { + res.statusCode = 500; res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({"error":{"message":"unrecognized rpc"}})); - }); + res.end(JSON.stringify({ + error: { message: err.message, code: err.code } + })); + } + + function enable() { + delete state.config.disable;// = undefined; + state.keepAlive.state = true; + + fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { + if (err) { + err.message = "Could not save config file. Perhaps you're user doesn't have permission?"; + handleError(err); + return; + } + // TODO XXX myRemote.active + if (myRemote) { + listSuccess(); + return; + } + safeStartTelebitRemote(true).then(listSuccess).catch(handleError); + }); + } + + function disable() { + state.config.disable = true; + state.keepAlive.state = false; + + if (myRemote) { myRemote.end(); myRemote = null; } + fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), function (err) { + res.setHeader('Content-Type', 'application/json'); + if (err) { + err.message = "Could not save config file. Perhaps you're user doesn't have permission?"; + handleError(err); + return; + } + res.end('{"success":true}'); + }); + } + + function getStatus() { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify( + { module: 'status' + , status: (state.config.disable ? 'disabled' : 'enabled') + , ready: ((state.config.relay && (state.config.token || state.config.agreeTos)) ? true : false) + , active: !!myRemote + , connected: 'maybe (todo)' + , version: pkg.version + , servernames: state.servernames + } + )); + } + + if (/\b(config)\b/.test(opts.pathname) && /get/i.test(req.method)) { + getConfigOnly(); + return; + } + if (/\b(init|config)\b/.test(opts.pathname)) { + initOrConfig(); + return; + } + if (/restart/.test(opts.pathname)) { + restart(); + return; + } + // + // Check for proper config + // + if (!state.config.relay || !state.config.email || !state.config.agreeTos) { + invalidConfig(); + return; + } + // + // With proper config + // + if (/http/.test(opts.pathname)) { + controllers.http(req, res, opts); + return; + } + if (/tcp/.test(opts.pathname)) { + controllers.tcp(req, res, opts); + return; + } + if (/save|commit/.test(opts.pathname)) { + saveAndCommit(); + return; + } + if (/ssh/.test(opts.pathname)) { + controllers.ssh(req, res, opts); + return; + } + if (/enable/.test(opts.pathname)) { + enable(); + return; + } + if (/disable/.test(opts.pathname)) { + disable(); + return; + } + if (/status/.test(opts.pathname)) { + getStatus(); + return; + } + if (/list/.test(opts.pathname)) { + listSuccess(); + return; + } + + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({"error":{"message":"unrecognized rpc"}})); +} +function serveControlsHelper() { + controlServer = http.createServer(handleRemoteClient); if (fs.existsSync(state._ipc.path)) { fs.unlinkSync(state._ipc.path); @@ -661,15 +671,30 @@ function serveControlsHelper() { , readableAll: true , exclusive: false }; + if (!state.config.ipc) { + state.config.ipc = {}; + } + if (!state.config.ipc.path) { + state.config.ipc.path = path.dirname(state._ipc.path); + } + require('mkdirp').sync(state.config.ipc.path); + if (!state.config.ipc.type) { + state.config.ipc.type = 'port'; + } + var portFile = path.join(state.config.ipc.path, 'telebit.port'); + if (fs.existsSync(portFile)) { + state._ipc.port = parseInt(fs.readFileSync(portFile, 'utf8').trim(), 10); + } + if ('socket' === state._ipc.type) { require('mkdirp').sync(path.dirname(state._ipc.path)); } // https://nodejs.org/api/net.html#net_server_listen_options_callback // path is ignore if port is defined // https://git.coolaj86.com/coolaj86/telebit.js/issues/23#issuecomment-326 - if (state._ipc.port) { + if ('port' === state.config.ipc.type) { serverOpts.host = 'localhost'; - serverOpts.port = state._ipc.port; + serverOpts.port = state._ipc.port || 0; } else { serverOpts.path = state._ipc.path; } @@ -682,6 +707,21 @@ function serveControlsHelper() { //console.log(this.address()); console.info("[info] Listening for commands on", address); }); + controlServer.on('error', function (err) { + if ('EADDRINUSE' === err.code) { + try { + fs.unlinkSync(portFile); + } catch(e) { + // nada + } + setTimeout(function () { + console.log("trying again"); + serveControlsHelper(); + }, 1000); + return; + } + console.error('failed to start c&c server:', err); + }); } function serveControls() { diff --git a/lib/admin/index.html b/lib/admin/index.html new file mode 100644 index 0000000..0b180dc --- /dev/null +++ b/lib/admin/index.html @@ -0,0 +1,60 @@ + + + + Telebit Admin + + +
+

Telebit Admin

+ +
+

GET /api/config

+
{{ config }}
+
+ +
+

GET /api/status

+
{{ status }}
+
+ +
+

POST /api/init

+
+ + + +
+ + +
+ + +
+ +
+
{{ init }}
+
+ +
+

POST /api/http

+
{{ http }}
+
+ +
+

POST /api/tcp

+
{{ tcp }}
+
+ +
+

POST /api/ssh

+
{{ ssh }}
+
+ +
+ + + + + diff --git a/lib/admin/js/app.js b/lib/admin/js/app.js new file mode 100644 index 0000000..cef624b --- /dev/null +++ b/lib/admin/js/app.js @@ -0,0 +1,50 @@ +;(function () { +'use strict'; + +console.log("hello"); + +var Vue = window.Vue; +var api = {}; + +api.config = function apiConfig() { + return window.fetch("/api/config", { method: "GET" }).then(function (resp) { + return resp.json().then(function (json) { + appData.config = json; + return json; + }); + }); +}; +api.status = function apiStatus() { + return window.fetch("/api/status", { method: "GET" }).then(function (resp) { + return resp.json().then(function (json) { + appData.status = json; + return json; + }); + }); +}; + +var appData = { + config: null +, status: null +, init: {} +, http: null +, tcp: null +, ssh: null +}; +var appMethods = { + initialize: function () { + console.log("call initialize"); + } +}; + +new Vue({ + el: ".v-app" +, data: appData +, methods: appMethods +}); + +api.config(); +api.status(); + +window.api = api; +}()); diff --git a/lib/admin/js/vue.js b/lib/admin/js/vue.js new file mode 100644 index 0000000..ef171b9 --- /dev/null +++ b/lib/admin/js/vue.js @@ -0,0 +1,10947 @@ +/*! + * Vue.js v2.5.17 + * (c) 2014-2018 Evan You + * Released under the MIT License. + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.Vue = factory()); +}(this, (function () { 'use strict'; + +/* */ + +var emptyObject = Object.freeze({}); + +// these helpers produces better vm code in JS engines due to their +// explicitness and function inlining +function isUndef (v) { + return v === undefined || v === null +} + +function isDef (v) { + return v !== undefined && v !== null +} + +function isTrue (v) { + return v === true +} + +function isFalse (v) { + return v === false +} + +/** + * Check if value is primitive + */ +function isPrimitive (value) { + return ( + typeof value === 'string' || + typeof value === 'number' || + // $flow-disable-line + typeof value === 'symbol' || + typeof value === 'boolean' + ) +} + +/** + * Quick object check - this is primarily used to tell + * Objects from primitive values when we know the value + * is a JSON-compliant type. + */ +function isObject (obj) { + return obj !== null && typeof obj === 'object' +} + +/** + * Get the raw type string of a value e.g. [object Object] + */ +var _toString = Object.prototype.toString; + +function toRawType (value) { + return _toString.call(value).slice(8, -1) +} + +/** + * Strict object type check. Only returns true + * for plain JavaScript objects. + */ +function isPlainObject (obj) { + return _toString.call(obj) === '[object Object]' +} + +function isRegExp (v) { + return _toString.call(v) === '[object RegExp]' +} + +/** + * Check if val is a valid array index. + */ +function isValidArrayIndex (val) { + var n = parseFloat(String(val)); + return n >= 0 && Math.floor(n) === n && isFinite(val) +} + +/** + * Convert a value to a string that is actually rendered. + */ +function toString (val) { + return val == null + ? '' + : typeof val === 'object' + ? JSON.stringify(val, null, 2) + : String(val) +} + +/** + * Convert a input value to a number for persistence. + * If the conversion fails, return original string. + */ +function toNumber (val) { + var n = parseFloat(val); + return isNaN(n) ? val : n +} + +/** + * Make a map and return a function for checking if a key + * is in that map. + */ +function makeMap ( + str, + expectsLowerCase +) { + var map = Object.create(null); + var list = str.split(','); + for (var i = 0; i < list.length; i++) { + map[list[i]] = true; + } + return expectsLowerCase + ? function (val) { return map[val.toLowerCase()]; } + : function (val) { return map[val]; } +} + +/** + * Check if a tag is a built-in tag. + */ +var isBuiltInTag = makeMap('slot,component', true); + +/** + * Check if a attribute is a reserved attribute. + */ +var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); + +/** + * Remove an item from an array + */ +function remove (arr, item) { + if (arr.length) { + var index = arr.indexOf(item); + if (index > -1) { + return arr.splice(index, 1) + } + } +} + +/** + * Check whether the object has the property. + */ +var hasOwnProperty = Object.prototype.hasOwnProperty; +function hasOwn (obj, key) { + return hasOwnProperty.call(obj, key) +} + +/** + * Create a cached version of a pure function. + */ +function cached (fn) { + var cache = Object.create(null); + return (function cachedFn (str) { + var hit = cache[str]; + return hit || (cache[str] = fn(str)) + }) +} + +/** + * Camelize a hyphen-delimited string. + */ +var camelizeRE = /-(\w)/g; +var camelize = cached(function (str) { + return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) +}); + +/** + * Capitalize a string. + */ +var capitalize = cached(function (str) { + return str.charAt(0).toUpperCase() + str.slice(1) +}); + +/** + * Hyphenate a camelCase string. + */ +var hyphenateRE = /\B([A-Z])/g; +var hyphenate = cached(function (str) { + return str.replace(hyphenateRE, '-$1').toLowerCase() +}); + +/** + * Simple bind polyfill for environments that do not support it... e.g. + * PhantomJS 1.x. Technically we don't need this anymore since native bind is + * now more performant in most browsers, but removing it would be breaking for + * code that was able to run in PhantomJS 1.x, so this must be kept for + * backwards compatibility. + */ + +/* istanbul ignore next */ +function polyfillBind (fn, ctx) { + function boundFn (a) { + var l = arguments.length; + return l + ? l > 1 + ? fn.apply(ctx, arguments) + : fn.call(ctx, a) + : fn.call(ctx) + } + + boundFn._length = fn.length; + return boundFn +} + +function nativeBind (fn, ctx) { + return fn.bind(ctx) +} + +var bind = Function.prototype.bind + ? nativeBind + : polyfillBind; + +/** + * Convert an Array-like object to a real Array. + */ +function toArray (list, start) { + start = start || 0; + var i = list.length - start; + var ret = new Array(i); + while (i--) { + ret[i] = list[i + start]; + } + return ret +} + +/** + * Mix properties into target object. + */ +function extend (to, _from) { + for (var key in _from) { + to[key] = _from[key]; + } + return to +} + +/** + * Merge an Array of Objects into a single Object. + */ +function toObject (arr) { + var res = {}; + for (var i = 0; i < arr.length; i++) { + if (arr[i]) { + extend(res, arr[i]); + } + } + return res +} + +/** + * Perform no operation. + * Stubbing args to make Flow happy without leaving useless transpiled code + * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/) + */ +function noop (a, b, c) {} + +/** + * Always return false. + */ +var no = function (a, b, c) { return false; }; + +/** + * Return same value + */ +var identity = function (_) { return _; }; + +/** + * Generate a static keys string from compiler modules. + */ +function genStaticKeys (modules) { + return modules.reduce(function (keys, m) { + return keys.concat(m.staticKeys || []) + }, []).join(',') +} + +/** + * Check if two values are loosely equal - that is, + * if they are plain objects, do they have the same shape? + */ +function looseEqual (a, b) { + if (a === b) { return true } + var isObjectA = isObject(a); + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + var isArrayA = Array.isArray(a); + var isArrayB = Array.isArray(b); + if (isArrayA && isArrayB) { + return a.length === b.length && a.every(function (e, i) { + return looseEqual(e, b[i]) + }) + } else if (!isArrayA && !isArrayB) { + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return keysA.length === keysB.length && keysA.every(function (key) { + return looseEqual(a[key], b[key]) + }) + } else { + /* istanbul ignore next */ + return false + } + } catch (e) { + /* istanbul ignore next */ + return false + } + } else if (!isObjectA && !isObjectB) { + return String(a) === String(b) + } else { + return false + } +} + +function looseIndexOf (arr, val) { + for (var i = 0; i < arr.length; i++) { + if (looseEqual(arr[i], val)) { return i } + } + return -1 +} + +/** + * Ensure a function is called only once. + */ +function once (fn) { + var called = false; + return function () { + if (!called) { + called = true; + fn.apply(this, arguments); + } + } +} + +var SSR_ATTR = 'data-server-rendered'; + +var ASSET_TYPES = [ + 'component', + 'directive', + 'filter' +]; + +var LIFECYCLE_HOOKS = [ + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'beforeDestroy', + 'destroyed', + 'activated', + 'deactivated', + 'errorCaptured' +]; + +/* */ + +var config = ({ + /** + * Option merge strategies (used in core/util/options) + */ + // $flow-disable-line + optionMergeStrategies: Object.create(null), + + /** + * Whether to suppress warnings. + */ + silent: false, + + /** + * Show production mode tip message on boot? + */ + productionTip: "development" !== 'production', + + /** + * Whether to enable devtools + */ + devtools: "development" !== 'production', + + /** + * Whether to record perf + */ + performance: false, + + /** + * Error handler for watcher errors + */ + errorHandler: null, + + /** + * Warn handler for watcher warns + */ + warnHandler: null, + + /** + * Ignore certain custom elements + */ + ignoredElements: [], + + /** + * Custom user key aliases for v-on + */ + // $flow-disable-line + keyCodes: Object.create(null), + + /** + * Check if a tag is reserved so that it cannot be registered as a + * component. This is platform-dependent and may be overwritten. + */ + isReservedTag: no, + + /** + * Check if an attribute is reserved so that it cannot be used as a component + * prop. This is platform-dependent and may be overwritten. + */ + isReservedAttr: no, + + /** + * Check if a tag is an unknown element. + * Platform-dependent. + */ + isUnknownElement: no, + + /** + * Get the namespace of an element + */ + getTagNamespace: noop, + + /** + * Parse the real tag name for the specific platform. + */ + parsePlatformTagName: identity, + + /** + * Check if an attribute must be bound using property, e.g. value + * Platform-dependent. + */ + mustUseProp: no, + + /** + * Exposed for legacy reasons + */ + _lifecycleHooks: LIFECYCLE_HOOKS +}) + +/* */ + +/** + * Check if a string starts with $ or _ + */ +function isReserved (str) { + var c = (str + '').charCodeAt(0); + return c === 0x24 || c === 0x5F +} + +/** + * Define a property. + */ +function def (obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true + }); +} + +/** + * Parse simple path. + */ +var bailRE = /[^\w.$]/; +function parsePath (path) { + if (bailRE.test(path)) { + return + } + var segments = path.split('.'); + return function (obj) { + for (var i = 0; i < segments.length; i++) { + if (!obj) { return } + obj = obj[segments[i]]; + } + return obj + } +} + +/* */ + +// can we use __proto__? +var hasProto = '__proto__' in {}; + +// Browser environment sniffing +var inBrowser = typeof window !== 'undefined'; +var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; +var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); +var UA = inBrowser && window.navigator.userAgent.toLowerCase(); +var isIE = UA && /msie|trident/.test(UA); +var isIE9 = UA && UA.indexOf('msie 9.0') > 0; +var isEdge = UA && UA.indexOf('edge/') > 0; +var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android'); +var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); +var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; + +// Firefox has a "watch" function on Object.prototype... +var nativeWatch = ({}).watch; + +var supportsPassive = false; +if (inBrowser) { + try { + var opts = {}; + Object.defineProperty(opts, 'passive', ({ + get: function get () { + /* istanbul ignore next */ + supportsPassive = true; + } + })); // https://github.com/facebook/flow/issues/285 + window.addEventListener('test-passive', null, opts); + } catch (e) {} +} + +// this needs to be lazy-evaled because vue may be required before +// vue-server-renderer can set VUE_ENV +var _isServer; +var isServerRendering = function () { + if (_isServer === undefined) { + /* istanbul ignore if */ + if (!inBrowser && !inWeex && typeof global !== 'undefined') { + // detect presence of vue-server-renderer and avoid + // Webpack shimming the process + _isServer = global['process'].env.VUE_ENV === 'server'; + } else { + _isServer = false; + } + } + return _isServer +}; + +// detect devtools +var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + +/* istanbul ignore next */ +function isNative (Ctor) { + return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) +} + +var hasSymbol = + typeof Symbol !== 'undefined' && isNative(Symbol) && + typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); + +var _Set; +/* istanbul ignore if */ // $flow-disable-line +if (typeof Set !== 'undefined' && isNative(Set)) { + // use native Set when available. + _Set = Set; +} else { + // a non-standard Set polyfill that only works with primitive keys. + _Set = (function () { + function Set () { + this.set = Object.create(null); + } + Set.prototype.has = function has (key) { + return this.set[key] === true + }; + Set.prototype.add = function add (key) { + this.set[key] = true; + }; + Set.prototype.clear = function clear () { + this.set = Object.create(null); + }; + + return Set; + }()); +} + +/* */ + +var warn = noop; +var tip = noop; +var generateComponentTrace = (noop); // work around flow check +var formatComponentName = (noop); + +{ + var hasConsole = typeof console !== 'undefined'; + var classifyRE = /(?:^|[-_])(\w)/g; + var classify = function (str) { return str + .replace(classifyRE, function (c) { return c.toUpperCase(); }) + .replace(/[-_]/g, ''); }; + + warn = function (msg, vm) { + var trace = vm ? generateComponentTrace(vm) : ''; + + if (config.warnHandler) { + config.warnHandler.call(null, msg, vm, trace); + } else if (hasConsole && (!config.silent)) { + console.error(("[Vue warn]: " + msg + trace)); + } + }; + + tip = function (msg, vm) { + if (hasConsole && (!config.silent)) { + console.warn("[Vue tip]: " + msg + ( + vm ? generateComponentTrace(vm) : '' + )); + } + }; + + formatComponentName = function (vm, includeFile) { + if (vm.$root === vm) { + return '' + } + var options = typeof vm === 'function' && vm.cid != null + ? vm.options + : vm._isVue + ? vm.$options || vm.constructor.options + : vm || {}; + var name = options.name || options._componentTag; + var file = options.__file; + if (!name && file) { + var match = file.match(/([^/\\]+)\.vue$/); + name = match && match[1]; + } + + return ( + (name ? ("<" + (classify(name)) + ">") : "") + + (file && includeFile !== false ? (" at " + file) : '') + ) + }; + + var repeat = function (str, n) { + var res = ''; + while (n) { + if (n % 2 === 1) { res += str; } + if (n > 1) { str += str; } + n >>= 1; + } + return res + }; + + generateComponentTrace = function (vm) { + if (vm._isVue && vm.$parent) { + var tree = []; + var currentRecursiveSequence = 0; + while (vm) { + if (tree.length > 0) { + var last = tree[tree.length - 1]; + if (last.constructor === vm.constructor) { + currentRecursiveSequence++; + vm = vm.$parent; + continue + } else if (currentRecursiveSequence > 0) { + tree[tree.length - 1] = [last, currentRecursiveSequence]; + currentRecursiveSequence = 0; + } + } + tree.push(vm); + vm = vm.$parent; + } + return '\n\nfound in\n\n' + tree + .map(function (vm, i) { return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm) + ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") + : formatComponentName(vm))); }) + .join('\n') + } else { + return ("\n\n(found in " + (formatComponentName(vm)) + ")") + } + }; +} + +/* */ + + +var uid = 0; + +/** + * A dep is an observable that can have multiple + * directives subscribing to it. + */ +var Dep = function Dep () { + this.id = uid++; + this.subs = []; +}; + +Dep.prototype.addSub = function addSub (sub) { + this.subs.push(sub); +}; + +Dep.prototype.removeSub = function removeSub (sub) { + remove(this.subs, sub); +}; + +Dep.prototype.depend = function depend () { + if (Dep.target) { + Dep.target.addDep(this); + } +}; + +Dep.prototype.notify = function notify () { + // stabilize the subscriber list first + var subs = this.subs.slice(); + for (var i = 0, l = subs.length; i < l; i++) { + subs[i].update(); + } +}; + +// the current target watcher being evaluated. +// this is globally unique because there could be only one +// watcher being evaluated at any time. +Dep.target = null; +var targetStack = []; + +function pushTarget (_target) { + if (Dep.target) { targetStack.push(Dep.target); } + Dep.target = _target; +} + +function popTarget () { + Dep.target = targetStack.pop(); +} + +/* */ + +var VNode = function VNode ( + tag, + data, + children, + text, + elm, + context, + componentOptions, + asyncFactory +) { + this.tag = tag; + this.data = data; + this.children = children; + this.text = text; + this.elm = elm; + this.ns = undefined; + this.context = context; + this.fnContext = undefined; + this.fnOptions = undefined; + this.fnScopeId = undefined; + this.key = data && data.key; + this.componentOptions = componentOptions; + this.componentInstance = undefined; + this.parent = undefined; + this.raw = false; + this.isStatic = false; + this.isRootInsert = true; + this.isComment = false; + this.isCloned = false; + this.isOnce = false; + this.asyncFactory = asyncFactory; + this.asyncMeta = undefined; + this.isAsyncPlaceholder = false; +}; + +var prototypeAccessors = { child: { configurable: true } }; + +// DEPRECATED: alias for componentInstance for backwards compat. +/* istanbul ignore next */ +prototypeAccessors.child.get = function () { + return this.componentInstance +}; + +Object.defineProperties( VNode.prototype, prototypeAccessors ); + +var createEmptyVNode = function (text) { + if ( text === void 0 ) text = ''; + + var node = new VNode(); + node.text = text; + node.isComment = true; + return node +}; + +function createTextVNode (val) { + return new VNode(undefined, undefined, undefined, String(val)) +} + +// optimized shallow clone +// used for static nodes and slot nodes because they may be reused across +// multiple renders, cloning them avoids errors when DOM manipulations rely +// on their elm reference. +function cloneVNode (vnode) { + var cloned = new VNode( + vnode.tag, + vnode.data, + vnode.children, + vnode.text, + vnode.elm, + vnode.context, + vnode.componentOptions, + vnode.asyncFactory + ); + cloned.ns = vnode.ns; + cloned.isStatic = vnode.isStatic; + cloned.key = vnode.key; + cloned.isComment = vnode.isComment; + cloned.fnContext = vnode.fnContext; + cloned.fnOptions = vnode.fnOptions; + cloned.fnScopeId = vnode.fnScopeId; + cloned.isCloned = true; + return cloned +} + +/* + * not type checking this file because flow doesn't play well with + * dynamically accessing methods on Array prototype + */ + +var arrayProto = Array.prototype; +var arrayMethods = Object.create(arrayProto); + +var methodsToPatch = [ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse' +]; + +/** + * Intercept mutating methods and emit events + */ +methodsToPatch.forEach(function (method) { + // cache original method + var original = arrayProto[method]; + def(arrayMethods, method, function mutator () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + var result = original.apply(this, args); + var ob = this.__ob__; + var inserted; + switch (method) { + case 'push': + case 'unshift': + inserted = args; + break + case 'splice': + inserted = args.slice(2); + break + } + if (inserted) { ob.observeArray(inserted); } + // notify change + ob.dep.notify(); + return result + }); +}); + +/* */ + +var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + +/** + * In some cases we may want to disable observation inside a component's + * update computation. + */ +var shouldObserve = true; + +function toggleObserving (value) { + shouldObserve = value; +} + +/** + * Observer class that is attached to each observed + * object. Once attached, the observer converts the target + * object's property keys into getter/setters that + * collect dependencies and dispatch updates. + */ +var Observer = function Observer (value) { + this.value = value; + this.dep = new Dep(); + this.vmCount = 0; + def(value, '__ob__', this); + if (Array.isArray(value)) { + var augment = hasProto + ? protoAugment + : copyAugment; + augment(value, arrayMethods, arrayKeys); + this.observeArray(value); + } else { + this.walk(value); + } +}; + +/** + * Walk through each property and convert them into + * getter/setters. This method should only be called when + * value type is Object. + */ +Observer.prototype.walk = function walk (obj) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + defineReactive(obj, keys[i]); + } +}; + +/** + * Observe a list of Array items. + */ +Observer.prototype.observeArray = function observeArray (items) { + for (var i = 0, l = items.length; i < l; i++) { + observe(items[i]); + } +}; + +// helpers + +/** + * Augment an target Object or Array by intercepting + * the prototype chain using __proto__ + */ +function protoAugment (target, src, keys) { + /* eslint-disable no-proto */ + target.__proto__ = src; + /* eslint-enable no-proto */ +} + +/** + * Augment an target Object or Array by defining + * hidden properties. + */ +/* istanbul ignore next */ +function copyAugment (target, src, keys) { + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + def(target, key, src[key]); + } +} + +/** + * Attempt to create an observer instance for a value, + * returns the new observer if successfully observed, + * or the existing observer if the value already has one. + */ +function observe (value, asRootData) { + if (!isObject(value) || value instanceof VNode) { + return + } + var ob; + if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { + ob = value.__ob__; + } else if ( + shouldObserve && + !isServerRendering() && + (Array.isArray(value) || isPlainObject(value)) && + Object.isExtensible(value) && + !value._isVue + ) { + ob = new Observer(value); + } + if (asRootData && ob) { + ob.vmCount++; + } + return ob +} + +/** + * Define a reactive property on an Object. + */ +function defineReactive ( + obj, + key, + val, + customSetter, + shallow +) { + var dep = new Dep(); + + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + return + } + + // cater for pre-defined getter/setters + var getter = property && property.get; + if (!getter && arguments.length === 2) { + val = obj[key]; + } + var setter = property && property.set; + + var childOb = !shallow && observe(val); + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter () { + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + dep.depend(); + if (childOb) { + childOb.dep.depend(); + if (Array.isArray(value)) { + dependArray(value); + } + } + } + return value + }, + set: function reactiveSetter (newVal) { + var value = getter ? getter.call(obj) : val; + /* eslint-disable no-self-compare */ + if (newVal === value || (newVal !== newVal && value !== value)) { + return + } + /* eslint-enable no-self-compare */ + if ("development" !== 'production' && customSetter) { + customSetter(); + } + if (setter) { + setter.call(obj, newVal); + } else { + val = newVal; + } + childOb = !shallow && observe(newVal); + dep.notify(); + } + }); +} + +/** + * Set a property on an object. Adds the new property and + * triggers change notification if the property doesn't + * already exist. + */ +function set (target, key, val) { + if ("development" !== 'production' && + (isUndef(target) || isPrimitive(target)) + ) { + warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.length = Math.max(target.length, key); + target.splice(key, 1, val); + return val + } + if (key in target && !(key in Object.prototype)) { + target[key] = val; + return val + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + "development" !== 'production' && warn( + 'Avoid adding reactive properties to a Vue instance or its root $data ' + + 'at runtime - declare it upfront in the data option.' + ); + return val + } + if (!ob) { + target[key] = val; + return val + } + defineReactive(ob.value, key, val); + ob.dep.notify(); + return val +} + +/** + * Delete a property and trigger change if necessary. + */ +function del (target, key) { + if ("development" !== 'production' && + (isUndef(target) || isPrimitive(target)) + ) { + warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.splice(key, 1); + return + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + "development" !== 'production' && warn( + 'Avoid deleting properties on a Vue instance or its root $data ' + + '- just set it to null.' + ); + return + } + if (!hasOwn(target, key)) { + return + } + delete target[key]; + if (!ob) { + return + } + ob.dep.notify(); +} + +/** + * Collect dependencies on array elements when the array is touched, since + * we cannot intercept array element access like property getters. + */ +function dependArray (value) { + for (var e = (void 0), i = 0, l = value.length; i < l; i++) { + e = value[i]; + e && e.__ob__ && e.__ob__.dep.depend(); + if (Array.isArray(e)) { + dependArray(e); + } + } +} + +/* */ + +/** + * Option overwriting strategies are functions that handle + * how to merge a parent option value and a child option + * value into the final value. + */ +var strats = config.optionMergeStrategies; + +/** + * Options with restrictions + */ +{ + strats.el = strats.propsData = function (parent, child, vm, key) { + if (!vm) { + warn( + "option \"" + key + "\" can only be used during instance " + + 'creation with the `new` keyword.' + ); + } + return defaultStrat(parent, child) + }; +} + +/** + * Helper that recursively merges two data objects together. + */ +function mergeData (to, from) { + if (!from) { return to } + var key, toVal, fromVal; + var keys = Object.keys(from); + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + toVal = to[key]; + fromVal = from[key]; + if (!hasOwn(to, key)) { + set(to, key, fromVal); + } else if (isPlainObject(toVal) && isPlainObject(fromVal)) { + mergeData(toVal, fromVal); + } + } + return to +} + +/** + * Data + */ +function mergeDataOrFn ( + parentVal, + childVal, + vm +) { + if (!vm) { + // in a Vue.extend merge, both should be functions + if (!childVal) { + return parentVal + } + if (!parentVal) { + return childVal + } + // when parentVal & childVal are both present, + // we need to return a function that returns the + // merged result of both functions... no need to + // check if parentVal is a function here because + // it has to be a function to pass previous merges. + return function mergedDataFn () { + return mergeData( + typeof childVal === 'function' ? childVal.call(this, this) : childVal, + typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal + ) + } + } else { + return function mergedInstanceDataFn () { + // instance merge + var instanceData = typeof childVal === 'function' + ? childVal.call(vm, vm) + : childVal; + var defaultData = typeof parentVal === 'function' + ? parentVal.call(vm, vm) + : parentVal; + if (instanceData) { + return mergeData(instanceData, defaultData) + } else { + return defaultData + } + } + } +} + +strats.data = function ( + parentVal, + childVal, + vm +) { + if (!vm) { + if (childVal && typeof childVal !== 'function') { + "development" !== 'production' && warn( + 'The "data" option should be a function ' + + 'that returns a per-instance value in component ' + + 'definitions.', + vm + ); + + return parentVal + } + return mergeDataOrFn(parentVal, childVal) + } + + return mergeDataOrFn(parentVal, childVal, vm) +}; + +/** + * Hooks and props are merged as arrays. + */ +function mergeHook ( + parentVal, + childVal +) { + return childVal + ? parentVal + ? parentVal.concat(childVal) + : Array.isArray(childVal) + ? childVal + : [childVal] + : parentVal +} + +LIFECYCLE_HOOKS.forEach(function (hook) { + strats[hook] = mergeHook; +}); + +/** + * Assets + * + * When a vm is present (instance creation), we need to do + * a three-way merge between constructor options, instance + * options and parent options. + */ +function mergeAssets ( + parentVal, + childVal, + vm, + key +) { + var res = Object.create(parentVal || null); + if (childVal) { + "development" !== 'production' && assertObjectType(key, childVal, vm); + return extend(res, childVal) + } else { + return res + } +} + +ASSET_TYPES.forEach(function (type) { + strats[type + 's'] = mergeAssets; +}); + +/** + * Watchers. + * + * Watchers hashes should not overwrite one + * another, so we merge them as arrays. + */ +strats.watch = function ( + parentVal, + childVal, + vm, + key +) { + // work around Firefox's Object.prototype.watch... + if (parentVal === nativeWatch) { parentVal = undefined; } + if (childVal === nativeWatch) { childVal = undefined; } + /* istanbul ignore if */ + if (!childVal) { return Object.create(parentVal || null) } + { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = {}; + extend(ret, parentVal); + for (var key$1 in childVal) { + var parent = ret[key$1]; + var child = childVal[key$1]; + if (parent && !Array.isArray(parent)) { + parent = [parent]; + } + ret[key$1] = parent + ? parent.concat(child) + : Array.isArray(child) ? child : [child]; + } + return ret +}; + +/** + * Other object hashes. + */ +strats.props = +strats.methods = +strats.inject = +strats.computed = function ( + parentVal, + childVal, + vm, + key +) { + if (childVal && "development" !== 'production') { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = Object.create(null); + extend(ret, parentVal); + if (childVal) { extend(ret, childVal); } + return ret +}; +strats.provide = mergeDataOrFn; + +/** + * Default strategy. + */ +var defaultStrat = function (parentVal, childVal) { + return childVal === undefined + ? parentVal + : childVal +}; + +/** + * Validate component names + */ +function checkComponents (options) { + for (var key in options.components) { + validateComponentName(key); + } +} + +function validateComponentName (name) { + if (!/^[a-zA-Z][\w-]*$/.test(name)) { + warn( + 'Invalid component name: "' + name + '". Component names ' + + 'can only contain alphanumeric characters and the hyphen, ' + + 'and must start with a letter.' + ); + } + if (isBuiltInTag(name) || config.isReservedTag(name)) { + warn( + 'Do not use built-in or reserved HTML elements as component ' + + 'id: ' + name + ); + } +} + +/** + * Ensure all props option syntax are normalized into the + * Object-based format. + */ +function normalizeProps (options, vm) { + var props = options.props; + if (!props) { return } + var res = {}; + var i, val, name; + if (Array.isArray(props)) { + i = props.length; + while (i--) { + val = props[i]; + if (typeof val === 'string') { + name = camelize(val); + res[name] = { type: null }; + } else { + warn('props must be strings when using array syntax.'); + } + } + } else if (isPlainObject(props)) { + for (var key in props) { + val = props[key]; + name = camelize(key); + res[name] = isPlainObject(val) + ? val + : { type: val }; + } + } else { + warn( + "Invalid value for option \"props\": expected an Array or an Object, " + + "but got " + (toRawType(props)) + ".", + vm + ); + } + options.props = res; +} + +/** + * Normalize all injections into Object-based format + */ +function normalizeInject (options, vm) { + var inject = options.inject; + if (!inject) { return } + var normalized = options.inject = {}; + if (Array.isArray(inject)) { + for (var i = 0; i < inject.length; i++) { + normalized[inject[i]] = { from: inject[i] }; + } + } else if (isPlainObject(inject)) { + for (var key in inject) { + var val = inject[key]; + normalized[key] = isPlainObject(val) + ? extend({ from: key }, val) + : { from: val }; + } + } else { + warn( + "Invalid value for option \"inject\": expected an Array or an Object, " + + "but got " + (toRawType(inject)) + ".", + vm + ); + } +} + +/** + * Normalize raw function directives into object format. + */ +function normalizeDirectives (options) { + var dirs = options.directives; + if (dirs) { + for (var key in dirs) { + var def = dirs[key]; + if (typeof def === 'function') { + dirs[key] = { bind: def, update: def }; + } + } + } +} + +function assertObjectType (name, value, vm) { + if (!isPlainObject(value)) { + warn( + "Invalid value for option \"" + name + "\": expected an Object, " + + "but got " + (toRawType(value)) + ".", + vm + ); + } +} + +/** + * Merge two option objects into a new one. + * Core utility used in both instantiation and inheritance. + */ +function mergeOptions ( + parent, + child, + vm +) { + { + checkComponents(child); + } + + if (typeof child === 'function') { + child = child.options; + } + + normalizeProps(child, vm); + normalizeInject(child, vm); + normalizeDirectives(child); + var extendsFrom = child.extends; + if (extendsFrom) { + parent = mergeOptions(parent, extendsFrom, vm); + } + if (child.mixins) { + for (var i = 0, l = child.mixins.length; i < l; i++) { + parent = mergeOptions(parent, child.mixins[i], vm); + } + } + var options = {}; + var key; + for (key in parent) { + mergeField(key); + } + for (key in child) { + if (!hasOwn(parent, key)) { + mergeField(key); + } + } + function mergeField (key) { + var strat = strats[key] || defaultStrat; + options[key] = strat(parent[key], child[key], vm, key); + } + return options +} + +/** + * Resolve an asset. + * This function is used because child instances need access + * to assets defined in its ancestor chain. + */ +function resolveAsset ( + options, + type, + id, + warnMissing +) { + /* istanbul ignore if */ + if (typeof id !== 'string') { + return + } + var assets = options[type]; + // check local registration variations first + if (hasOwn(assets, id)) { return assets[id] } + var camelizedId = camelize(id); + if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } + var PascalCaseId = capitalize(camelizedId); + if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } + // fallback to prototype chain + var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; + if ("development" !== 'production' && warnMissing && !res) { + warn( + 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, + options + ); + } + return res +} + +/* */ + +function validateProp ( + key, + propOptions, + propsData, + vm +) { + var prop = propOptions[key]; + var absent = !hasOwn(propsData, key); + var value = propsData[key]; + // boolean casting + var booleanIndex = getTypeIndex(Boolean, prop.type); + if (booleanIndex > -1) { + if (absent && !hasOwn(prop, 'default')) { + value = false; + } else if (value === '' || value === hyphenate(key)) { + // only cast empty string / same name to boolean if + // boolean has higher priority + var stringIndex = getTypeIndex(String, prop.type); + if (stringIndex < 0 || booleanIndex < stringIndex) { + value = true; + } + } + } + // check default value + if (value === undefined) { + value = getPropDefaultValue(vm, prop, key); + // since the default value is a fresh copy, + // make sure to observe it. + var prevShouldObserve = shouldObserve; + toggleObserving(true); + observe(value); + toggleObserving(prevShouldObserve); + } + { + assertProp(prop, key, value, vm, absent); + } + return value +} + +/** + * Get the default value of a prop. + */ +function getPropDefaultValue (vm, prop, key) { + // no default, return undefined + if (!hasOwn(prop, 'default')) { + return undefined + } + var def = prop.default; + // warn against non-factory defaults for Object & Array + if ("development" !== 'production' && isObject(def)) { + warn( + 'Invalid default value for prop "' + key + '": ' + + 'Props with type Object/Array must use a factory function ' + + 'to return the default value.', + vm + ); + } + // the raw prop value was also undefined from previous render, + // return previous default value to avoid unnecessary watcher trigger + if (vm && vm.$options.propsData && + vm.$options.propsData[key] === undefined && + vm._props[key] !== undefined + ) { + return vm._props[key] + } + // call factory function for non-Function types + // a value is Function if its prototype is function even across different execution context + return typeof def === 'function' && getType(prop.type) !== 'Function' + ? def.call(vm) + : def +} + +/** + * Assert whether a prop is valid. + */ +function assertProp ( + prop, + name, + value, + vm, + absent +) { + if (prop.required && absent) { + warn( + 'Missing required prop: "' + name + '"', + vm + ); + return + } + if (value == null && !prop.required) { + return + } + var type = prop.type; + var valid = !type || type === true; + var expectedTypes = []; + if (type) { + if (!Array.isArray(type)) { + type = [type]; + } + for (var i = 0; i < type.length && !valid; i++) { + var assertedType = assertType(value, type[i]); + expectedTypes.push(assertedType.expectedType || ''); + valid = assertedType.valid; + } + } + if (!valid) { + warn( + "Invalid prop: type check failed for prop \"" + name + "\"." + + " Expected " + (expectedTypes.map(capitalize).join(', ')) + + ", got " + (toRawType(value)) + ".", + vm + ); + return + } + var validator = prop.validator; + if (validator) { + if (!validator(value)) { + warn( + 'Invalid prop: custom validator check failed for prop "' + name + '".', + vm + ); + } + } +} + +var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; + +function assertType (value, type) { + var valid; + var expectedType = getType(type); + if (simpleCheckRE.test(expectedType)) { + var t = typeof value; + valid = t === expectedType.toLowerCase(); + // for primitive wrapper objects + if (!valid && t === 'object') { + valid = value instanceof type; + } + } else if (expectedType === 'Object') { + valid = isPlainObject(value); + } else if (expectedType === 'Array') { + valid = Array.isArray(value); + } else { + valid = value instanceof type; + } + return { + valid: valid, + expectedType: expectedType + } +} + +/** + * Use function string name to check built-in types, + * because a simple equality check will fail when running + * across different vms / iframes. + */ +function getType (fn) { + var match = fn && fn.toString().match(/^\s*function (\w+)/); + return match ? match[1] : '' +} + +function isSameType (a, b) { + return getType(a) === getType(b) +} + +function getTypeIndex (type, expectedTypes) { + if (!Array.isArray(expectedTypes)) { + return isSameType(expectedTypes, type) ? 0 : -1 + } + for (var i = 0, len = expectedTypes.length; i < len; i++) { + if (isSameType(expectedTypes[i], type)) { + return i + } + } + return -1 +} + +/* */ + +function handleError (err, vm, info) { + if (vm) { + var cur = vm; + while ((cur = cur.$parent)) { + var hooks = cur.$options.errorCaptured; + if (hooks) { + for (var i = 0; i < hooks.length; i++) { + try { + var capture = hooks[i].call(cur, err, vm, info) === false; + if (capture) { return } + } catch (e) { + globalHandleError(e, cur, 'errorCaptured hook'); + } + } + } + } + } + globalHandleError(err, vm, info); +} + +function globalHandleError (err, vm, info) { + if (config.errorHandler) { + try { + return config.errorHandler.call(null, err, vm, info) + } catch (e) { + logError(e, null, 'config.errorHandler'); + } + } + logError(err, vm, info); +} + +function logError (err, vm, info) { + { + warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); + } + /* istanbul ignore else */ + if ((inBrowser || inWeex) && typeof console !== 'undefined') { + console.error(err); + } else { + throw err + } +} + +/* */ +/* globals MessageChannel */ + +var callbacks = []; +var pending = false; + +function flushCallbacks () { + pending = false; + var copies = callbacks.slice(0); + callbacks.length = 0; + for (var i = 0; i < copies.length; i++) { + copies[i](); + } +} + +// Here we have async deferring wrappers using both microtasks and (macro) tasks. +// In < 2.4 we used microtasks everywhere, but there are some scenarios where +// microtasks have too high a priority and fire in between supposedly +// sequential events (e.g. #4521, #6690) or even between bubbling of the same +// event (#6566). However, using (macro) tasks everywhere also has subtle problems +// when state is changed right before repaint (e.g. #6813, out-in transitions). +// Here we use microtask by default, but expose a way to force (macro) task when +// needed (e.g. in event handlers attached by v-on). +var microTimerFunc; +var macroTimerFunc; +var useMacroTask = false; + +// Determine (macro) task defer implementation. +// Technically setImmediate should be the ideal choice, but it's only available +// in IE. The only polyfill that consistently queues the callback after all DOM +// events triggered in the same loop is by using MessageChannel. +/* istanbul ignore if */ +if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { + macroTimerFunc = function () { + setImmediate(flushCallbacks); + }; +} else if (typeof MessageChannel !== 'undefined' && ( + isNative(MessageChannel) || + // PhantomJS + MessageChannel.toString() === '[object MessageChannelConstructor]' +)) { + var channel = new MessageChannel(); + var port = channel.port2; + channel.port1.onmessage = flushCallbacks; + macroTimerFunc = function () { + port.postMessage(1); + }; +} else { + /* istanbul ignore next */ + macroTimerFunc = function () { + setTimeout(flushCallbacks, 0); + }; +} + +// Determine microtask defer implementation. +/* istanbul ignore next, $flow-disable-line */ +if (typeof Promise !== 'undefined' && isNative(Promise)) { + var p = Promise.resolve(); + microTimerFunc = function () { + p.then(flushCallbacks); + // in problematic UIWebViews, Promise.then doesn't completely break, but + // it can get stuck in a weird state where callbacks are pushed into the + // microtask queue but the queue isn't being flushed, until the browser + // needs to do some other work, e.g. handle a timer. Therefore we can + // "force" the microtask queue to be flushed by adding an empty timer. + if (isIOS) { setTimeout(noop); } + }; +} else { + // fallback to macro + microTimerFunc = macroTimerFunc; +} + +/** + * Wrap a function so that if any code inside triggers state change, + * the changes are queued using a (macro) task instead of a microtask. + */ +function withMacroTask (fn) { + return fn._withTask || (fn._withTask = function () { + useMacroTask = true; + var res = fn.apply(null, arguments); + useMacroTask = false; + return res + }) +} + +function nextTick (cb, ctx) { + var _resolve; + callbacks.push(function () { + if (cb) { + try { + cb.call(ctx); + } catch (e) { + handleError(e, ctx, 'nextTick'); + } + } else if (_resolve) { + _resolve(ctx); + } + }); + if (!pending) { + pending = true; + if (useMacroTask) { + macroTimerFunc(); + } else { + microTimerFunc(); + } + } + // $flow-disable-line + if (!cb && typeof Promise !== 'undefined') { + return new Promise(function (resolve) { + _resolve = resolve; + }) + } +} + +/* */ + +var mark; +var measure; + +{ + var perf = inBrowser && window.performance; + /* istanbul ignore if */ + if ( + perf && + perf.mark && + perf.measure && + perf.clearMarks && + perf.clearMeasures + ) { + mark = function (tag) { return perf.mark(tag); }; + measure = function (name, startTag, endTag) { + perf.measure(name, startTag, endTag); + perf.clearMarks(startTag); + perf.clearMarks(endTag); + perf.clearMeasures(name); + }; + } +} + +/* not type checking this file because flow doesn't play well with Proxy */ + +var initProxy; + +{ + var allowedGlobals = makeMap( + 'Infinity,undefined,NaN,isFinite,isNaN,' + + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + + 'require' // for Webpack/Browserify + ); + + var warnNonPresent = function (target, key) { + warn( + "Property or method \"" + key + "\" is not defined on the instance but " + + 'referenced during render. Make sure that this property is reactive, ' + + 'either in the data option, or for class-based components, by ' + + 'initializing the property. ' + + 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', + target + ); + }; + + var hasProxy = + typeof Proxy !== 'undefined' && isNative(Proxy); + + if (hasProxy) { + var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact'); + config.keyCodes = new Proxy(config.keyCodes, { + set: function set (target, key, value) { + if (isBuiltInModifier(key)) { + warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key)); + return false + } else { + target[key] = value; + return true + } + } + }); + } + + var hasHandler = { + has: function has (target, key) { + var has = key in target; + var isAllowed = allowedGlobals(key) || key.charAt(0) === '_'; + if (!has && !isAllowed) { + warnNonPresent(target, key); + } + return has || !isAllowed + } + }; + + var getHandler = { + get: function get (target, key) { + if (typeof key === 'string' && !(key in target)) { + warnNonPresent(target, key); + } + return target[key] + } + }; + + initProxy = function initProxy (vm) { + if (hasProxy) { + // determine which proxy handler to use + var options = vm.$options; + var handlers = options.render && options.render._withStripped + ? getHandler + : hasHandler; + vm._renderProxy = new Proxy(vm, handlers); + } else { + vm._renderProxy = vm; + } + }; +} + +/* */ + +var seenObjects = new _Set(); + +/** + * Recursively traverse an object to evoke all converted + * getters, so that every nested property inside the object + * is collected as a "deep" dependency. + */ +function traverse (val) { + _traverse(val, seenObjects); + seenObjects.clear(); +} + +function _traverse (val, seen) { + var i, keys; + var isA = Array.isArray(val); + if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { + return + } + if (val.__ob__) { + var depId = val.__ob__.dep.id; + if (seen.has(depId)) { + return + } + seen.add(depId); + } + if (isA) { + i = val.length; + while (i--) { _traverse(val[i], seen); } + } else { + keys = Object.keys(val); + i = keys.length; + while (i--) { _traverse(val[keys[i]], seen); } + } +} + +/* */ + +var normalizeEvent = cached(function (name) { + var passive = name.charAt(0) === '&'; + name = passive ? name.slice(1) : name; + var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first + name = once$$1 ? name.slice(1) : name; + var capture = name.charAt(0) === '!'; + name = capture ? name.slice(1) : name; + return { + name: name, + once: once$$1, + capture: capture, + passive: passive + } +}); + +function createFnInvoker (fns) { + function invoker () { + var arguments$1 = arguments; + + var fns = invoker.fns; + if (Array.isArray(fns)) { + var cloned = fns.slice(); + for (var i = 0; i < cloned.length; i++) { + cloned[i].apply(null, arguments$1); + } + } else { + // return handler return value for single handlers + return fns.apply(null, arguments) + } + } + invoker.fns = fns; + return invoker +} + +function updateListeners ( + on, + oldOn, + add, + remove$$1, + vm +) { + var name, def, cur, old, event; + for (name in on) { + def = cur = on[name]; + old = oldOn[name]; + event = normalizeEvent(name); + /* istanbul ignore if */ + if (isUndef(cur)) { + "development" !== 'production' && warn( + "Invalid handler for event \"" + (event.name) + "\": got " + String(cur), + vm + ); + } else if (isUndef(old)) { + if (isUndef(cur.fns)) { + cur = on[name] = createFnInvoker(cur); + } + add(event.name, cur, event.once, event.capture, event.passive, event.params); + } else if (cur !== old) { + old.fns = cur; + on[name] = old; + } + } + for (name in oldOn) { + if (isUndef(on[name])) { + event = normalizeEvent(name); + remove$$1(event.name, oldOn[name], event.capture); + } + } +} + +/* */ + +function mergeVNodeHook (def, hookKey, hook) { + if (def instanceof VNode) { + def = def.data.hook || (def.data.hook = {}); + } + var invoker; + var oldHook = def[hookKey]; + + function wrappedHook () { + hook.apply(this, arguments); + // important: remove merged hook to ensure it's called only once + // and prevent memory leak + remove(invoker.fns, wrappedHook); + } + + if (isUndef(oldHook)) { + // no existing hook + invoker = createFnInvoker([wrappedHook]); + } else { + /* istanbul ignore if */ + if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { + // already a merged invoker + invoker = oldHook; + invoker.fns.push(wrappedHook); + } else { + // existing plain hook + invoker = createFnInvoker([oldHook, wrappedHook]); + } + } + + invoker.merged = true; + def[hookKey] = invoker; +} + +/* */ + +function extractPropsFromVNodeData ( + data, + Ctor, + tag +) { + // we are only extracting raw values here. + // validation and default values are handled in the child + // component itself. + var propOptions = Ctor.options.props; + if (isUndef(propOptions)) { + return + } + var res = {}; + var attrs = data.attrs; + var props = data.props; + if (isDef(attrs) || isDef(props)) { + for (var key in propOptions) { + var altKey = hyphenate(key); + { + var keyInLowerCase = key.toLowerCase(); + if ( + key !== keyInLowerCase && + attrs && hasOwn(attrs, keyInLowerCase) + ) { + tip( + "Prop \"" + keyInLowerCase + "\" is passed to component " + + (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + + " \"" + key + "\". " + + "Note that HTML attributes are case-insensitive and camelCased " + + "props need to use their kebab-case equivalents when using in-DOM " + + "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." + ); + } + } + checkProp(res, props, key, altKey, true) || + checkProp(res, attrs, key, altKey, false); + } + } + return res +} + +function checkProp ( + res, + hash, + key, + altKey, + preserve +) { + if (isDef(hash)) { + if (hasOwn(hash, key)) { + res[key] = hash[key]; + if (!preserve) { + delete hash[key]; + } + return true + } else if (hasOwn(hash, altKey)) { + res[key] = hash[altKey]; + if (!preserve) { + delete hash[altKey]; + } + return true + } + } + return false +} + +/* */ + +// The template compiler attempts to minimize the need for normalization by +// statically analyzing the template at compile time. +// +// For plain HTML markup, normalization can be completely skipped because the +// generated render function is guaranteed to return Array. There are +// two cases where extra normalization is needed: + +// 1. When the children contains components - because a functional component +// may return an Array instead of a single root. In this case, just a simple +// normalization is needed - if any child is an Array, we flatten the whole +// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep +// because functional components already normalize their own children. +function simpleNormalizeChildren (children) { + for (var i = 0; i < children.length; i++) { + if (Array.isArray(children[i])) { + return Array.prototype.concat.apply([], children) + } + } + return children +} + +// 2. When the children contains constructs that always generated nested Arrays, +// e.g.