diff --git a/.yaspellerrc b/.yaspellerrc index aa540e49..559f677f 100644 --- a/.yaspellerrc +++ b/.yaspellerrc @@ -40,6 +40,7 @@ "io", "js", "maintainers", + "microservice", "minification", "mouseup", "named", diff --git a/HELP.md b/HELP.md index 9a1b23d0..0346ad47 100644 --- a/HELP.md +++ b/HELP.md @@ -94,23 +94,34 @@ Cloud Commander supports command line parameters: | `--terminal-path` | set terminal path | `--vim` | enable vim hot keys | `--columns` | set visible columns +| `--export` | enable export of config through a server +| `--export-token` | authorization token used by export server +| `--import` | enable import of config +| `--import-token` | authorization token used to connect to export server +| `--import-url` | url of an import server +| `--import-listen` | enable listen on config updates from import server +| `--log` | enable logging | `--no-server` | do not start server | `--no-auth` | disable authorization | `--no-online` | load scripts from local server | `--no-open` | do not open web browser when server started -| `--no-name` | set empty tab name in web browser -| `--no-one-file-panel` | show two file panels +| `--no-name` | set default tab name in web browser | `--no-keys-panel` | hide keys panel +| `--no-one-file-panel` | show two file panels | `--no-progress` | do not show progress of file operations | `--no-confirm-copy` | do not confirm copy | `--no-confirm-move` | do not confirm move -| `--no-contact` | disable contact | `--no-config-dialog` | disable config dialog | `--no-console` | disable console | `--no-sync-console-path` | do not sync console path +| `--no-contact` | disable contact | `--no-terminal` | disable terminal | `--no-vim` | disable vim hot keys -| `--no-columns` | set visible default columns +| `--no-columns` | set default visible columns +| `--no-export` | disable export config through a server +| `--no-import` | disable import of config +| `--no-import-listen` | disable listen on config updates from import server +| `--no-log` | disable logging If no parameters given Cloud Commander reads information from `~/.cloudcmd.json` and use port from it (`8000` default). if port variables `PORT` or `VCAP_APP_PORT` isn't exist. @@ -382,6 +393,13 @@ Here is description of options: "terminalPath" : '', /* path of a terminal */ "vim" : false, /* disable vim hot keys */ "columns" : "name-size-date-owner-mode", /* set visible columns */ + "export" : false, /* enable export of config through a server */ + "exportToken" : "root", /* token used by export server */ + "import" : false, /* enable import of config */ + "import-url" : "http://localhost:8000", /* url of an export server */ + "importToken" : "root", /* token used to connect to export server */ + "importListen" : false, /* listen on config updates from import server */ + "log" : true /* logging */ } ``` @@ -407,6 +425,72 @@ Some config options can be overridden with `environment variables` such: - `CLOUDCMD_VIM` - enable vim hot keys - `CLOUDCMD_CONFIRM_COPY` - confirm copy - `CLOUDCMD_CONFIRM_MOVE` - confirm move +- `CLOUDCMD_EXPORT` - enable export of config through a server +- `CLOUDCMD_EXPORT_TOKEN` - authorization token used by export server +- `CLOUDCMD_IMPORT` - enable import of config +- `CLOUDCMD_IMPORT_TOKEN` - authorization token used to connect to export server +- `CLOUDCMD_IMPORT_URL` - url of an import server +- `CLOUDCMD_IMPORT_LISTEN`- enable listen on config updates from import server + +### Distribute + +Being able to configure `Cloud Commander` remotely opens the doors to using it as microservice and that's what distribute options set out to do. +There is an `export server` and `import client` and they enabled with `--export` and `--import` accordingly. There is a `token` it should be the same +in `--import-token` and `export-token`. To use report you should provide `--import-url` to `import client` so it can connect to an `export server`. +There is 2 ways `import client` can receive config from an `export server`: + +- full config at startup (default) +- get every updated option (with help of `--import-listen` flag) + +There is an example of using distribute options in `Cloud Commander` to get config from remote instance. +Here is an `export server`: + +``` +coderaiser@cloudcmd:~$ cloudcmd --port 1234 --export --export-token=cloudcmd +``` + +And `import client`: +``` +coderaiser@cloudcmd:~$ cloudcmd --name importer --port 4321 --import-url http://127.0.0.1:1234 --import-token=cloudcmd --no-server --save +``` + +Here is the log of `export server`: + +``` +url: http://localhost:1234/ +2018.08.23 13:41:45 -> export: try to auth from importer [127.0.0.1:4321] +2018.08.23 13:41:45 -> export: connected to importer [127.0.0.1:4321] +2018.08.23 13:41:45 -> export: disconnected importer [127.0.0.1:4321] +``` + +And log of `import client`: + +``` +2018.08.23 13:47:36 -> import: try to auth to http://127.0.0.1:1234 +2018.08.23 13:47:36 -> import: connected to http://127.0.0.1:1234 +2018.08.23 13:47:36 -> import: config received from http://localhost:1234 +2018.08.23 13:47:36 -> import: disconnected from http://127.0.0.1:1234 +``` + +When `import client` uses `--import-listen` persistent connection used and client receives live updates from the `import server`. + +`Export server` omit next configuration fields: + +- `auth` +- `username` +- `password` +- `algo` +- `name` +- `ip` +- `port` +- `root` +- `import` +- `importUrl` +- `importToken` +- `export` +- `exportToken` +- `log` +- `configDialog` Menu --------------- diff --git a/app.json b/app.json index 56197d23..59a92d86 100644 --- a/app.json +++ b/app.json @@ -111,6 +111,35 @@ "description": "confirm move", "value": "true", "required": false + }, + "CLOUDCMD_EXPORT": { + "description": "enable export of config through a server", + "value": "false", + "required": false + }, + "CLOUDCMD_EXPORT_TOKEN": { + "description": "authorization token used by export server", + "value": "root", + "required": false + }, + "CLOUDCMD_IMPORT": { + "description": "enable import of config", + "value": "false", + "required": false + }, + "CLOUDCMD_IMPORT_TOKEN": { + "description": "authorization token used to connect to export server", + "value": "root", + "required": false + }, + "CLOUDCMD_IMPORT_URL": { + "description": "url of an import server", + "value": "http://localhost:8000", + "required": false + }, + "CLOUDCMD_IMPORT_LISTEN": { + "description": "enable listen on config updates from import server", + "value": "false", + "required": false } } -} diff --git a/bin/cloudcmd.js b/bin/cloudcmd.js index a5e9da42..4e9fb485 100755 --- a/bin/cloudcmd.js +++ b/bin/cloudcmd.js @@ -5,10 +5,15 @@ const Info = require('../package'); const DIR_SERVER = '../server/'; +const promisify = require('es6-promisify').promisify; +const wraptile = require('wraptile/legacy'); + const exit = require(DIR_SERVER + 'exit'); const config = require(DIR_SERVER + 'config'); const env = require(DIR_SERVER + 'env'); +const noop = () => {}; + const choose = (a, b) => { if (a === undefined) return b; @@ -30,6 +35,9 @@ const args = require('minimist')(argv.slice(2), { 'prefix', 'terminal-path', 'columns', + 'import-url', + 'import-token', + 'export-token', ], boolean: [ 'auth', @@ -50,6 +58,11 @@ const args = require('minimist')(argv.slice(2), { 'show-config', 'vim', 'keys-panel', + 'color', + 'export', + 'import', + 'import-listen', + 'log', ], default: { server : true, @@ -68,6 +81,14 @@ const args = require('minimist')(argv.slice(2), { console : choose(env.bool('console'), config('console')), contact : choose(env.bool('contact'), config('contact')), terminal : choose(env.bool('terminal'), config('terminal')), + columns : env('columns') || config('columns') || '', + vim : choose(env.bool('vim'), config('vim')), + log : config('log'), + + 'import-url': env('import_url') || config('importUrl'), + 'import-listen': choose(env.bool('import_listen'), config('importListen')), + import : choose(env.bool('import'), config('import')), + export : choose(env.bool('export'), config('export')), 'sync-console-path': choose(env.bool('sync_console_path'), config('syncConsolePath')), 'config-dialog': choose(env.bool('config_dialog'), config('configDialog')), @@ -75,9 +96,9 @@ const args = require('minimist')(argv.slice(2), { 'one-file-panel': choose(env.bool('one_file_panel'), config('oneFilePanel')), 'confirm-copy': choose(env.bool('confirm_copy'), config('confirmCopy')), 'confirm-move': choose(env.bool('confirm_move'), config('confirmMove')), - 'vim': choose(env.bool('vim'), config('vim')), - 'columns': env('columns') || config('columns') || '', 'keys-panel': env.bool('keys_panel') || config('keysPanel'), + 'import-token': env('import_token') || config('importToken'), + 'export-token': env('export_token') || config('exportToken'), }, alias: { v: 'version', @@ -106,7 +127,6 @@ function main() { repl(); checkUpdate(); - port(args.port); config('name', args.name); @@ -125,11 +145,18 @@ function main() { config('root', args.root); config('vim', args.vim); config('columns', args.columns); + config('log', args.log); config('confirmCopy', args['confirm-copy']); config('confirmMove', args['confirm-move']); config('oneFilePanel', args['one-file-panel']); config('configDialog', args['config-dialog']); config('keysPanel', args['keys-panel']); + config('export', args.export); + config('exportToken', args['export-token']); + config('import', args.import); + config('importToken', args['import-token']); + config('importListen', args['import-listen']); + config('importUrl', args['import-url']); readConfig(args.config); @@ -151,12 +178,14 @@ function main() { if (args['show-config']) showConfig(); - if (!args.save) - return start(options); + const startWraped = wraptile(start, options); + const distribute = require('../server/distribute'); + const importConfig = promisify(distribute.import); + const caller = (fn) => fn(); - config.save(() => { - start(options); - }); + importConfig() + .then(args.save ? caller(config.save) : noop) + .then(startWraped(options)); } function validateRoot(root) { @@ -166,7 +195,6 @@ function validateRoot(root) { function getPassword(password) { const criton = require('criton'); - return criton(password, config('algo')); } @@ -241,7 +269,6 @@ function repl() { function checkUpdate() { const load = require('package-json'); - const noop = () => {}; load(Info.name, 'latest') .then(showUpdateInfo) diff --git a/common/datetime.js b/common/datetime.js new file mode 100644 index 00000000..131c27fb --- /dev/null +++ b/common/datetime.js @@ -0,0 +1,33 @@ +'use strict'; + +const shortdate = require('shortdate'); + +module.exports = (date) => { + date = date || new Date(); + check(date); + + const timeStr = shorttime(date); + const dateStr = shortdate(date); + + return `${dateStr} ${timeStr}`; +}; + +const addZero = (a) => { + if (a > 9) + return a; + + return `0${a}`; +}; + +function shorttime(date) { + const seconds = addZero(date.getSeconds()); + const minutes = addZero(date.getMinutes()); + const hours = addZero(date.getHours()); + + return `${hours}:${minutes}:${seconds}`; +} + +function check(date) { + if (!(date instanceof Date)) + throw Error('date should be instanceof Date!'); +} diff --git a/common/datetime.spec.js b/common/datetime.spec.js new file mode 100644 index 00000000..25e17c39 --- /dev/null +++ b/common/datetime.spec.js @@ -0,0 +1,54 @@ +'use strict'; + +const test = require('tape'); +const sinon = require('sinon'); + +const datetime = require('./datetime'); + +test('common: datetime', (t) => { + const dateStr = 'Fri, 17 Aug 2018 10:56:48'; + const result = datetime(new Date(dateStr)); + + const expected = '2018.08.17 10:56:48'; + + t.equals(result, expected, 'should equal'); + t.end(); +}); + +test('common: datetime: no arg', (t) => { + const {Date} = global; + + let called = false; + const myDate = class extends Date { + constructor() { + super(); + called = true; + } + }; + + global.Date = myDate; + + datetime(); + + global.Date = Date; + t.ok(called, 'should call new Date'); + t.end(); +}); + +test('common: 0 before number', (t) => { + const dateStr = 'Fri, 17 Aug 2018 10:56:08'; + const result = datetime(new Date(dateStr)); + + const expected = '2018.08.17 10:56:08'; + + t.equals(result, expected, 'should equal'); + t.end(); +}); + +test('common: datetime: wrong args', (t) => { + const fn = () => datetime({}); + + t.throws(fn, /date should be instanceof Date!/, 'should throw'); + t.end(); +}); + diff --git a/json/config.json b/json/config.json index ddbb477b..625f4a34 100644 --- a/json/config.json +++ b/json/config.json @@ -29,6 +29,13 @@ "terminalPath": "", "showConfig": false, "vim": false, - "columns": "name-size-date-owner-mode" + "columns": "name-size-date-owner-mode", + "export": false, + "exportToken": "root", + "import": false, + "importToken": "root", + "importUrl": "http://localhost:8000", + "importListen": false, + "log": true } diff --git a/json/help.json b/json/help.json index 12d15938..dab34446 100644 --- a/json/help.json +++ b/json/help.json @@ -28,12 +28,19 @@ "--terminal-path ": "set terminal path", "--vim ": "enable vim hot keys", "--columns ": "set visible columns", + "--export ": "enable export of config through a server", + "--export-token ": "authorization token used by export server", + "--import ": "enable import of config", + "--import-url ": "url of an export server", + "--import-token ": "authorization token used to connect to export server", + "--import-listen ": "enable listen on config updates from import server", + "--log ": "enable logging", "--no-server ": "do not start server", "--no-auth ": "disable authorization", "--no-online ": "load scripts from local server", "--no-open ": "do not open web browser when server started", "--no-name ": "set default tab name in web browser", - "--no-one-panel ": "show two file panels", + "--no-one-file-panel ": "show two file panels", "--no-keys-panel ": "hide keys panel", "--no-one-file-panel ": "show two file panels", "--no-progress ": "do not show progress of file operations", @@ -45,5 +52,9 @@ "--no-contact ": "disable contact", "--no-terminal ": "disable terminal", "--no-vim ": "disable vim hot keys", - "--no-columns ": "set default visible columns" + "--no-columns ": "set default visible columns", + "--no-export ": "disable export config through a server", + "--no-import ": "disable import of config", + "--no-import-listen ": "disable listen on config updates from import server", + "--no-log ": "disable logging" } diff --git a/man/cloudcmd.1 b/man/cloudcmd.1 index c3c027f6..f9e1ca22 100644 --- a/man/cloudcmd.1 +++ b/man/cloudcmd.1 @@ -51,6 +51,13 @@ programs in browser from any computer, mobile or tablet device. --terminal-path set terminal path --vim enable vim hot keys --columns set visible columns + --export enable export of config through a server + --export-token authorization token used by export server + --import enable import of config + --import-url url of an import server + --import-token authorization token used to connect to export server + --import-listen enable listen on config updates from import server + --log enable logging --no-auth disable authorization --no-server do not start server --no-online load scripts from local server @@ -68,6 +75,11 @@ programs in browser from any computer, mobile or tablet device. --no-terminal disable terminal --no-vim disable vim hot keys --no-columns set visible default columns + --no-export disable export of config through a server + --no-import disable import of config + --no-import-url url of an import server + --no-import-listen disable listen on config updates from import server + --no-log disable logging .SH RESOURCES AND DOCUMENTATION diff --git a/now.json b/now.json index 7f156466..9dec87b4 100644 --- a/now.json +++ b/now.json @@ -4,7 +4,11 @@ "env": { "cloudcmd_config_dialog": "false", "cloudcmd_terminal": "true", - "cloudcmd_terminal_path": "gritty" + "cloudcmd_terminal_path": "gritty", + "cloudcmd_import": true, + "cloudcmd_import_listen": true, + "cloudcmd_import_token": "hello-world", + "cloudcmd_import_url": "http://config.cloudcmd.io" }, "engines": { "node": "8" diff --git a/package.json b/package.json index 447fb5b2..fd769229 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,7 @@ "mellow": "^2.0.0", "minimist": "^1.2.0", "nomine": "^2.0.0", + "object.omit": "^3.0.0", "onezip": "^2.0.0", "opn": "^5.1.0", "package-json": "^4.0.1", @@ -149,7 +150,9 @@ "pullout": "^2.0.0", "rendy": "^2.0.0", "restafary": "^3.0.0", + "shortdate": "^1.2.0", "socket.io": "^2.0.3", + "socket.io-client": "^2.1.1", "squad": "^2.0.0", "table": "^4.0.1", "try-catch": "^2.0.0", @@ -202,11 +205,9 @@ "rimraf": "^2.5.4", "scroll-into-view-if-needed": "^2.2.5", "serviceworker-webpack-plugin": "^1.0.1", - "shortdate": "^1.0.1", "sinon": "^6.0.4", "sinon-called-with-diff": "^2.0.0", "smalltalk": "^3.1.0", - "socket.io-client": "^2.0.1", "style-loader": "^0.22.0", "stylelint": "^9.0.0", "stylelint-config-standard": "^18.0.0", diff --git a/server/cloudcmd.js b/server/cloudcmd.js index 9bef0068..c7256869 100644 --- a/server/cloudcmd.js +++ b/server/cloudcmd.js @@ -17,6 +17,7 @@ const validate = require(DIR + 'validate'); const prefixer = require(DIR + 'prefixer'); const pluginer = require(DIR + 'plugins'); const terminal = require(DIR + 'terminal'); +const distribute = require(DIR + 'distribute'); const currify = require('currify/legacy'); const apart = require('apart'); @@ -147,6 +148,8 @@ function listen(prefix, socket) { auth, prefix: prefix + '/gritty', }); + + distribute.export(socket); } function cloudcmd(prefix, plugins, modules) { diff --git a/server/config.js b/server/config.js index 3cfd895f..e0f47406 100644 --- a/server/config.js +++ b/server/config.js @@ -6,6 +6,7 @@ const DIR = DIR_SERVER + '../'; const path = require('path'); const fs = require('fs'); +const Emitter = require('events'); const exit = require(DIR_SERVER + 'exit'); const CloudFunc = require(DIR_COMMON + 'cloudfunc'); @@ -35,6 +36,7 @@ const send = swap(ponse.send); const formatMsg = currify((a, b) => CloudFunc.formatMsg(a, b)); const apiURL = CloudFunc.apiURL; +const changeEmitter = new Emitter(); const ConfigPath = path.join(DIR, 'json/config.json'); const ConfigHome = path.join(HOME, '.cloudcmd.json'); @@ -59,8 +61,16 @@ const config = Object.assign({}, rootConfig, configHome); const connectionWraped = wraptile(connection); module.exports = manage; -module.exports.save = _save; +module.exports.save = save; module.exports.middle = middle; +module.exports.subscribe = (fn) => { + changeEmitter.on('change', fn); +}; + +module.exports.unsubscribe = (fn) => { + changeEmitter.removeListener('change', fn); +}; + module.exports.listen = (socket, auth) => { check(socket, auth); @@ -83,6 +93,10 @@ function manage(key, value) { return config[key]; config[key] = value; + + changeEmitter.emit('change', key, value); + + return `${key} = ${value}`; } function _save(callback) { diff --git a/server/distribute/export.js b/server/distribute/export.js new file mode 100644 index 00000000..7d6ecd20 --- /dev/null +++ b/server/distribute/export.js @@ -0,0 +1,114 @@ +'use strict'; + +const currify = require('currify/legacy'); +const wraptile = require('wraptile/legacy'); +const squad = require('squad/legacy'); +const omit = require('object.omit'); + +const config = require('../config'); +const log = require('./log'); + +const exportStr = log.exportStr; +const connectedStr = log.connectedStr; +const disconnectedStr = log.disconnectedStr; +const authTryStr = log.authTryStr; + +const makeColor = log.makeColor; +const getMessage = log.getMessage; +const getDescription = log.getDescription; +const logWraped = log.logWraped; + +const omitList = [ + 'auth', + 'username', + 'password', + 'algo', + 'name', + 'ip', + 'port', + 'root', + 'import', + 'importUrl', + 'importToken', + 'export', + 'exportToken', + 'log', + 'configDialog', +]; + +const omitConfig = wraptile((config) => omit(config, omitList)); + +module.exports = (socket) => { + if (!config('export')) + return; + + const prefix = config('prefix'); + const distributePrefix = `${prefix}/distribute`; + + const onError = squad(logWraped(exportStr), getMessage); + const onConnectError = squad(logWraped(exportStr), getDescription); + + socket.of(distributePrefix) + .on('connection', onConnection(push)) + .on('error', onError) + .on('connect_error', onConnectError); +}; + +const push = currify((socket, key, value) => { + if (omitList.includes(key)) + return; + + socket.emit('change', key, value); +}); + +function getHost(socket) { + const remoteAddress = socket.request.connection.remoteAddress; + const name = socket.handshake.query.name; + const port = socket.handshake.query.port; + const color = socket.handshake.query.color; + + if (!name) + return `${remoteAddress}:${port}`; + + const colorName = makeColor(name, color); + + return `${colorName} [${remoteAddress}:${port}]`; +} + +const connectPush = wraptile((push, socket) => { + socket.emit('accept'); + + const host = getHost(socket); + const subscription = push(socket); + + socket.on('disconnect', onDisconnect(subscription, host)); + + log(exportStr, `${connectedStr} to ${host}`); + socket.emit('config', omitConfig(config('*'))); + + config.subscribe(subscription); +}); + +const onConnection = currify((push, socket) => { + const host = getHost(socket); + const reject = () => { + socket.emit('reject'); + socket.disconnect(); + }; + + log(exportStr, `${authTryStr} from ${host}`); + socket.on('auth', auth(connectPush(push, socket), reject)); +}); + +const auth = currify((fn, reject, token) => { + if (token === config('exportToken')) + return fn(); + + reject(); +}); + +const onDisconnect = wraptile((subscription, host) => { + config.unsubscribe(subscription); + log(exportStr, `${disconnectedStr} from ${host}`); +}); + diff --git a/server/distribute/export.spec.js b/server/distribute/export.spec.js new file mode 100644 index 00000000..7fc2a5ab --- /dev/null +++ b/server/distribute/export.spec.js @@ -0,0 +1,45 @@ +'use strict'; + +const {promisify} = require('util'); + +const test = require('tape'); +const io = require('socket.io-client'); + +const {connect} = require('../../test/before'); +const config = require('../config'); + +test('distribute: export', async (t) => { + const defaultConfig = { + export: true, + exportToken: 'a', + vim: true, + log: false, + }; + + const {port, done} = await connect({ + config: defaultConfig + }); + + const url = `http://localhost:${port}/distribute?port=${1111}`; + const socket = io.connect(url); + + const name = config('name'); + + socket.on('connect', () => { + socket.emit('auth', 'a'); + }); + + socket.on('accept', () => { + config('vim', false); + config('auth', true); + }); + + socket.on('change', async () => { + socket.close(); + await done(); + + t.pass('should emit change'); + t.end(); + }); +}); + diff --git a/server/distribute/import.js b/server/distribute/import.js new file mode 100644 index 00000000..cec1ce28 --- /dev/null +++ b/server/distribute/import.js @@ -0,0 +1,131 @@ +'use strict'; + +const currify = require('currify/legacy'); +const wraptile = require('wraptile/legacy'); +const squad = require('squad/legacy'); +const fullstore = require('fullstore/legacy'); + +const io = require('socket.io-client'); +const forEachKey = currify(require('for-each-key/legacy')); + +const config = require('../config'); +const log = require('./log'); + +const importStr = log.importStr; +const connectedStr = log.connectedStr; +const disconnectedStr = log.disconnectedStr; +const tokenRejectedStr = log.tokenRejectedStr; +const authTryStr = log.authTryStr; + +const makeColor = log.makeColor; +const stringToRGB = log.stringToRGB; +const getMessage = log.getMessage; +const getDescription = log.getDescription; +const logWraped = log.logWraped; + +const equal = (a, b) => `${a}=${b}`; +const append = currify((obj, a, b) => obj.value += b && equal(a, b) + '&'); +const wrapApply = (f, disconnect) => (status) => () => f(null, { + status, + disconnect, +}); + +const closeIfNot = wraptile((socket, is) => !is && socket.close()); +const addUrl = currify((url, a) => `${url}: ${a}`); + +const getColorUrl = (url, name) => { + if (!name) + return url; + + return makeColor(url, stringToRGB(name)); +}; + +const rmListeners = wraptile((socket, listeners) => { + socket.removeListener('connect', listeners.onConnect); + socket.removeListener('config', listeners.onConfig); + socket.removeListener('error', listeners.onError); + socket.removeListener('connection_error', listeners.onError); +}); + +const canceled = (f) => f(null, { + status: 'canceled', + disconnect: () => {}, +}); + +const done = wraptile((fn, store) => fn(null, { + status: store() +})); + +const emitAuth = wraptile((importUrl, socket) => { + log(importStr, `${authTryStr} to ${importUrl}`); + socket.emit('auth', config('importToken')); +}); + +module.exports = (options, fn) => { + fn = fn || options; + + if (!config('import')) + return canceled(fn); + + const importUrl = config('importUrl'); + const importListen = config('importListen'); + const name = config('name'); + const port = config('port'); + + const query = toLine({ + name, + port, + }); + + const url = `${importUrl}/distribute?${query}`; + const socket = io.connect(url, Object.assign({}, { + rejectUnauthorized: false, + }, options)); + + const superFn = wrapApply(fn, socket.close.bind(socket)); + const colorUrl = getColorUrl(importUrl, name); + const close = closeIfNot(socket, importListen); + + const statusStore = fullstore(); + const statusStoreWraped = wraptile(statusStore); + + const onConfig = squad(close, logWraped(importStr, `config received from ${colorUrl}`), statusStoreWraped('received'), forEachKey(config)); + const onError = squad(superFn('error'), logWraped(importStr), addUrl(colorUrl), getMessage); + const onConnectError = squad(superFn('connect_error'), logWraped(importStr), addUrl(colorUrl), getDescription); + const onConnect = emitAuth(importUrl, socket); + const onAccept = logWraped(importStr,`${connectedStr} to ${colorUrl}`); + const onDisconnect = squad(done(fn, statusStore), logWraped(importStr, `${disconnectedStr} from ${colorUrl}`), rmListeners(socket, { + onError, + onConnect, + onConfig, + })); + + const onChange = squad(logWraped(importStr), config); + const onReject = squad(superFn('reject'), logWraped(importStr, tokenRejectedStr)); + + socket.on('connect', onConnect); + socket.on('accept', onAccept); + socket.on('disconnect', onDisconnect); + socket.on('config', onConfig); + socket.on('error', onError); + socket.on('connect_error', onConnectError); + socket.on('reject', onReject); + + if (config('importListen')) + socket.on('change', onChange); +}; + +function toLine(obj) { + const result = { + value: '', + }; + + forEachKey(append(result), obj); + + const start = 0; + const end = 1; + const backward = -1; + + return result.value.slice(start, backward * end); +} + diff --git a/server/distribute/import.spec.js b/server/distribute/import.spec.js new file mode 100644 index 00000000..f1ff761c --- /dev/null +++ b/server/distribute/import.spec.js @@ -0,0 +1,183 @@ +'use strict'; + +const test = require('tape'); +const {promisify} = require('util'); +const tryToCatch = require('try-to-catch'); +const io = require('socket.io-client'); +const mockRequire = require('mock-require'); + +const {connect} = require('../../test/before'); + +const config = require('../config'); +const distribute = { + import: promisify(require('./import')), +}; + +test('distribute: import: canceled', async (t) => { + const {done, port} = await connect({ + config: { + export: false, + import: false, + importListen: false, + log: false, + } + }); + + const {status} = await distribute.import(); + + await done(); + + t.equal(status, 'canceled', 'should equal'); + t.end(); +}); + +test('distribute: import: received: no error', async (t) => { + const {done, port} = await connect({ + config: { + import: true, + importListen: false, + export: true, + log: false, + } + }); + + config('importUrl', `http://localhost:${port}`); + + const [e] = await tryToCatch(distribute.import); + + await done(); + + t.notOk(e, 'should not be error'); + t.end(); +}); + +test('distribute: import: received', async (t) => { + const {done, port} = await connect({ + config: { + name: 'bill', + import: true, + importToken: 'a', + exportToken: 'a', + export: true, + importListen: false, + log: false, + } + }); + + config('importUrl', `http://localhost:${port}`); + + const {status} = await distribute.import(); + await done(); + + t.equal(status, 'received','should equal'); + t.end(); +}); + +test('distribute: import: received: auth: reject', async (t) => { + const {done, port} = await connect({ + config: { + name: 'bill', + import: true, + importToken: 'xxxxx', + exportToken: 'bbbbb', + export: true, + importListen: false, + log: false, + } + }); + + config('importUrl', `http://localhost:${port}`); + + const {status} = await distribute.import(); + await done(); + + t.equal(status, 'reject','should equal'); + t.end(); +}); + +test('distribute: import: received: auth: accept', async (t) => { + const {done, port} = await connect({ + config: { + name: 'bill', + import: true, + importToken: 'xxxxx', + exportToken: 'xxxxx', + export: true, + importListen: false, + log: false, + } + }); + + config('importUrl', `http://localhost:${port}`); + + const {status} = await distribute.import(); + await done(); + + t.equal(status, 'received','should equal'); + t.end(); +}); + +test('distribute: import: received: no name', async (t) => { + const {done, port} = await connect({ + config: { + name: '', + import: true, + export: true, + importListen: false, + log: false, + } + }); + + config('importUrl', `http://localhost:${port}`); + + const {status} = await distribute.import(); + await done(); + + t.equal(status, 'received','should equal'); + t.end(); +}); + +test('distribute: import: error', async (t) => { + const {done, port} = await connect({ + config: { + import: true, + export: false, + importListen: false, + log: false, + } + }); + + config('importUrl', `http://localhost:0`); + + const {status} = await distribute.import({ + reconnection: false, + }); + + await done(); + + t.equal(status, 'connect_error','should equal'); + t.end(); +}); + +test('distribute: import: config:change: no export', async (t) => { + const {done, port} = await connect({ + config: { + import: true, + export: false, + importListen: true, + log: false, + } + }); + + const {status} = await distribute.import({ + reconnection: false, + }); + + await done(); + + t.equal(status, 'connect_error','should equal'); + t.end(); +}); + +process.on('unhandledRejection', console.log); + diff --git a/server/distribute/index.js b/server/distribute/index.js new file mode 100644 index 00000000..9fae4b04 --- /dev/null +++ b/server/distribute/index.js @@ -0,0 +1,4 @@ +'use strict'; + +module.exports.import = require('./import'); +module.exports.export = require('./export'); diff --git a/server/distribute/log.js b/server/distribute/log.js new file mode 100644 index 00000000..b76d04d1 --- /dev/null +++ b/server/distribute/log.js @@ -0,0 +1,45 @@ +'use strict'; + +const wraptile = require('wraptile/legacy'); +const chalk = require('chalk'); + +const config = require('../config'); +const datetime = require('../../common/datetime'); + +const log = (name, msg) => config('log') && console.log(`${datetime()} -> ${name}: ${msg}`); +const makeColor = (a, color) => chalk.rgb(color || stringToRGB(a))(a); +const getMessage = (e) => e.message || e; +const getDescription = (e) => `${e.type}: ${e.description}`; + +module.exports = log; +module.exports.logWraped = wraptile(log); +module.exports.stringToRGB = stringToRGB; +module.exports.makeColor = makeColor; +module.exports.getMessage = getMessage; +module.exports.getDescription = getDescription; + +module.exports.importStr = 'import'; +module.exports.exportStr = 'export'; +module.exports.connectedStr = chalk.green('connected'); +module.exports.disconnectedStr = chalk.red('disconnected'); +module.exports.tokenRejectedStr = chalk.red('token rejected'); +module.exports.authTryStr = chalk.yellow('try to auth'); + +function stringToRGB(a) { + return [ + a.charCodeAt(0), + a.length, + crc(a), + ]; +} + +const add = (a, b) => { + return a + b.charCodeAt(0); +}; + +function crc(a) { + return a + .split('') + .reduce(add, 0); +} + diff --git a/server/distribute/log.spec.js b/server/distribute/log.spec.js new file mode 100644 index 00000000..650de213 --- /dev/null +++ b/server/distribute/log.spec.js @@ -0,0 +1,32 @@ +'use strict'; + +const test = require('tape'); +const log = require('./log'); +const config = require('../config'); + +test('distribute: log: getMessage', (t) => { + const e = 'hello'; + const result = log.getMessage(e) + + t.equal(e, result, 'should equal'); + t.end(); +}); + +test('distribute: log: getMessage: message', (t) => { + const message = 'hello'; + const result = log.getMessage({ + message + }) + + t.equal(result, message, 'should equal'); + t.end(); +}); + +test('distribute: log: config', (t) => { + const logOriginal = config('log'); + config('log', true); + log('log', 'test message'); + config('log', logOriginal); + + t.end(); +});