├── .editorconfig ├── .eslintrc ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── bin ├── actions.js ├── index.js ├── runner.js └── utils.js ├── dist └── client │ └── spreadable.client.js ├── package-lock.json ├── package.json ├── src ├── approval │ └── transports │ │ ├── approval │ │ └── index.js │ │ ├── captcha │ │ ├── fonts │ │ │ └── ipag.ttf │ │ └── index.js │ │ └── client │ │ └── index.js ├── behavior │ └── transports │ │ ├── behavior │ │ └── index.js │ │ └── fail │ │ └── index.js ├── browser │ ├── client │ │ ├── .babelrc │ │ └── index.js │ └── mock │ │ └── index.js ├── cache │ └── transports │ │ ├── cache │ │ └── index.js │ │ └── database │ │ └── index.js ├── client.js ├── db │ └── transports │ │ ├── database │ │ └── index.js │ │ └── loki │ │ └── index.js ├── errors.js ├── index.js ├── logger │ ├── index.js │ └── transports │ │ ├── adapter │ │ └── index.js │ │ ├── console │ │ └── index.js │ │ ├── file │ │ └── index.js │ │ └── logger │ │ └── index.js ├── node.js ├── schema.js ├── server │ └── transports │ │ ├── express │ │ ├── api │ │ │ ├── butler │ │ │ │ ├── controllers.js │ │ │ │ └── routes.js │ │ │ ├── master │ │ │ │ ├── controllers.js │ │ │ │ └── routes.js │ │ │ ├── node │ │ │ │ ├── controllers.js │ │ │ │ └── routes.js │ │ │ ├── routes.js │ │ │ └── slave │ │ │ │ ├── controllers.js │ │ │ │ └── routes.js │ │ ├── client │ │ │ ├── controllers.js │ │ │ └── routes.js │ │ ├── controllers.js │ │ ├── index.js │ │ ├── midds.js │ │ └── routes.js │ │ └── server │ │ └── index.js ├── service.js ├── task │ └── transports │ │ ├── cron │ │ └── index.js │ │ ├── interval │ │ └── index.js │ │ └── task │ │ └── index.js └── utils.js ├── test ├── .eslintrc ├── approval │ ├── approval.js │ ├── captcha.js │ └── client.js ├── behavior │ ├── behavior.js │ └── fail.js ├── cache │ ├── cache.js │ └── database.js ├── client.js ├── db │ ├── database.js │ └── loki.js ├── group.js ├── index.js ├── logger │ ├── adapter.js │ ├── console.js │ ├── file.js │ └── logger.js ├── node.js ├── routes.js ├── server │ ├── express.js │ └── server.js ├── service.js ├── services.js ├── task │ ├── cron.js │ ├── interval.js │ └── task.js ├── tools.js └── utils.js ├── webpack.client.js └── webpack.common.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = false 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2021, 5 | "sourceType": "module", 6 | "requireConfigFile": false, 7 | "babelOptions": { 8 | "plugins": [ 9 | "@babel/plugin-syntax-import-assertions" 10 | ] 11 | } 12 | }, 13 | "env": { 14 | "browser": true, 15 | "node": true, 16 | "es6": true 17 | }, 18 | "extends": "eslint:recommended", 19 | "rules": { 20 | "no-console": "warn" 21 | }, 22 | "globals": {} 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [20] 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Run tests with ${{ matrix.node-version }} 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm i -g npm@latest 20 | - run: npm ci 21 | - run: npm ddp 22 | - run: npm run build-ci 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | push: 4 | branches: [ master ] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-node@v4 11 | with: 12 | node-version: 20 13 | - run: npm i -g npm@latest 14 | - run: npm ci 15 | - run: npm ddp 16 | - run: npm run build-ci 17 | - uses: JS-DevTools/npm-publish@v3 18 | with: 19 | token: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *~ 3 | 4 | *.log 5 | *.bat 6 | *.git 7 | *.suo 8 | *.sln 9 | *.swp 10 | *.swo 11 | .vscode 12 | .idea 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Alexander Balasyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 8 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions 11 | of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 14 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 16 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /bin/actions.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import yargs from "yargs"; 3 | import srcUtils from "../src/utils.js"; 4 | 5 | const argv = yargs(process.argv).argv; 6 | const actions = {}; 7 | 8 | /** 9 | * Show the node status info 10 | */ 11 | actions.status = async (node) => { 12 | const info = await node.getStatusInfo(true); 13 | //eslint-disable-next-line no-console 14 | console.log(chalk.cyan(JSON.stringify(info, null, 2))); 15 | }; 16 | 17 | /** 18 | * Get all banlist 19 | */ 20 | actions.getBanlist = async (node) => { 21 | const fullInfo = argv.fullInfo || argv.f; 22 | const list = await node.db.getBanlist(); 23 | 24 | if (!list.length) { 25 | //eslint-disable-next-line no-console 26 | console.log(chalk.cyan(`The banlist is empty`)); 27 | return; 28 | } 29 | 30 | for (let i = 0; i < list.length; i++) { 31 | const result = list[i]; 32 | //eslint-disable-next-line no-console 33 | console.log(chalk.cyan(fullInfo ? JSON.stringify(result, null, 2) : result.address)); 34 | } 35 | }; 36 | 37 | /** 38 | * Get the banlist address info 39 | */ 40 | actions.getBanlistAddress = async (node) => { 41 | const address = argv.address || argv.n; 42 | 43 | if (!address) { 44 | throw new Error(`Address is required`); 45 | } 46 | 47 | const result = await node.db.getBanlistAddress(address); 48 | 49 | if (!result) { 50 | throw new Error(`Address "${address}" not found`); 51 | } 52 | 53 | //eslint-disable-next-line no-console 54 | console.log(chalk.cyan(JSON.stringify(result, null, 2))); 55 | }; 56 | 57 | /** 58 | * Add the address to the banlist 59 | */ 60 | actions.addBanlistAddress = async (node) => { 61 | const address = argv.address || argv.n; 62 | const lifetime = srcUtils.getMs(argv.lifetime || argv.t); 63 | const reason = argv.reason || argv.r; 64 | 65 | if (!srcUtils.isValidAddress(address)) { 66 | throw new Error(`Address is invalid`); 67 | } 68 | 69 | if (!lifetime) { 70 | throw new Error(`Lifetime is required`); 71 | } 72 | 73 | await node.db.addBanlistAddress(address, +lifetime, reason); 74 | //eslint-disable-next-line no-console 75 | console.log(chalk.cyan(`Address "${address}" has been added to the banlist`)); 76 | }; 77 | 78 | /** 79 | * Remove the address from the banlist 80 | */ 81 | actions.removeBanlistAddress = async (node) => { 82 | const address = argv.address || argv.n; 83 | 84 | if (!address) { 85 | throw new Error(`Address is required`); 86 | } 87 | 88 | await node.db.removeBanlistAddress(address); 89 | //eslint-disable-next-line no-console 90 | console.log(chalk.cyan(`Address "${address}" has been removed from the banlist`)); 91 | }; 92 | 93 | /** 94 | * Empty the banlist 95 | */ 96 | actions.emptyBanlist = async (node) => { 97 | await node.db.emptyBanlist(); 98 | //eslint-disable-next-line no-console 99 | console.log(chalk.cyan(`The banlist has been cleaned`)); 100 | }; 101 | 102 | /** 103 | * Create a backup 104 | */ 105 | actions.backup = async (node) => { 106 | await node.db.backup(); 107 | //eslint-disable-next-line no-console 108 | console.log(chalk.cyan(`The backup has been created`)); 109 | }; 110 | 111 | /** 112 | * Restore the database 113 | */ 114 | actions.restore = async (node) => { 115 | const index = argv.index || argv.i; 116 | await node.db.restore(index); 117 | //eslint-disable-next-line no-console 118 | console.log(chalk.cyan(`The database has been restored`)); 119 | }; 120 | 121 | export default actions; 122 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import runner from "./runner.js"; 3 | import { Node } from "../src/index.js"; 4 | import actions from "./actions.js"; 5 | 6 | runner('spreadable', Node, actions); 7 | -------------------------------------------------------------------------------- /bin/runner.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import yargs from "yargs"; 3 | import path from "path"; 4 | import merge from "lodash-es/merge.js"; 5 | import { Spinner } from "cli-spinner"; 6 | import utils from "./utils.js"; 7 | 8 | const argv = yargs(process.argv).argv; 9 | 10 | export default async (name, Node, actions) => { 11 | let node; 12 | ['SIGINT', 'SIGQUIT', 'SIGTERM', 'uncaughtException'].forEach((sig) => { 13 | process.on(sig, async () => { 14 | try { 15 | await node.deinit(); 16 | process.exit(); 17 | } 18 | catch (err) { 19 | //eslint-disable-next-line no-console 20 | console.error(err.stack); 21 | process.exit(1); 22 | } 23 | }); 24 | }); 25 | 26 | try { 27 | const action = argv.action || argv.a; 28 | const logger = argv.logger === undefined ? argv.l : argv.logger; 29 | const server = argv.server === undefined ? argv.s : argv.server; 30 | let configPath = argv.configPath || argv.c; 31 | let config; 32 | let spinner; 33 | 34 | if (configPath) { 35 | configPath = utils.getAbsolutePath(configPath); 36 | } 37 | else { 38 | configPath = path.join(process.cwd(), name + '.config'); 39 | } 40 | 41 | try { 42 | config = (await import(configPath)).default; 43 | } 44 | catch (err) { 45 | throw new Error(`Not found the config file ${ configPath }`); 46 | } 47 | 48 | config = merge(config, { 49 | logger: logger === false ? false : config.logger, 50 | server: server === false ? false : config.server 51 | }); 52 | node = new Node(config); 53 | 54 | if (!node.options.logger.level) { 55 | spinner = new Spinner('Initializing... %s'); 56 | spinner.start(); 57 | } 58 | 59 | await node.init(); 60 | spinner && spinner.stop(true); 61 | //eslint-disable-next-line no-console 62 | console.log(chalk.cyan('Node has been initialized')); 63 | 64 | if (action) { 65 | if (!actions[action]) { 66 | throw new Error(`Not found action "${action}"`); 67 | } 68 | 69 | await actions[action](node); 70 | await node.deinit(); 71 | process.exit(); 72 | } 73 | } 74 | catch (err) { 75 | //eslint-disable-next-line no-console 76 | console.error(chalk.red(err.stack)); 77 | node && await node.deinit(); 78 | process.exit(1); 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /bin/utils.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | const utils = {}; 3 | 4 | /** 5 | * Get the file absolute path 6 | * 7 | * @param {string} file 8 | * @param {string} [entry] - the entry point 9 | * @returns {string} 10 | */ 11 | utils.getAbsolutePath = function (file, entry = process.cwd()) { 12 | return path.isAbsolute(file) ? file : path.resolve(entry, file); 13 | }; 14 | 15 | export default utils; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spreadable", 3 | "version": "0.3.12", 4 | "description": "Decentralized network mechanism", 5 | "bin": { 6 | "spreadable": "./bin/index.js" 7 | }, 8 | "type": "module", 9 | "main": "./src/index.js", 10 | "author": { 11 | "name": "Alexander Balasyan", 12 | "email": "mywebstreet@gmail.com" 13 | }, 14 | "homepage": "https://github.com/ortexx/spreadable", 15 | "scripts": { 16 | "eslint": "eslint src bin test", 17 | "test": "mocha ./test/index.js --timeout=30000", 18 | "build-client": "webpack --config=webpack.client.js", 19 | "build-client-prod": "cross-env NODE_ENV=production webpack --config=webpack.client.js", 20 | "build-ci": "npm run eslint && npm run test && npm run build-client-prod" 21 | }, 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "npm run build-ci && git add ./dist/*" 25 | } 26 | }, 27 | "keywords": [ 28 | "spreadable", 29 | "protocol", 30 | "network", 31 | "distributed", 32 | "decentralized", 33 | "decentralization", 34 | "distribution", 35 | "information", 36 | "data" 37 | ], 38 | "license": "MIT", 39 | "devDependencies": { 40 | "@babel/core": "^7.23.7", 41 | "@babel/eslint-parser": "^7.23.10", 42 | "@babel/plugin-syntax-import-assertions": "^7.23.3", 43 | "@babel/plugin-transform-runtime": "^7.23.7", 44 | "@babel/preset-env": "^7.23.8", 45 | "babel-loader": "^9.1.3", 46 | "chai": "^5.0.0", 47 | "cross-env": "^7.0.3", 48 | "css-minimizer-webpack-plugin": "^5.0.1", 49 | "eslint": "^8.56.0", 50 | "eslint-webpack-plugin": "^4.0.1", 51 | "husky": "^4.3.8", 52 | "mini-css-extract-plugin": "^2.7.7", 53 | "mocha": "^10.2.0", 54 | "node-mocks-http": "^1.14.1", 55 | "node-polyfill-webpack-plugin": "^3.0.0", 56 | "selfsigned": "^2.4.1", 57 | "terser-webpack-plugin": "^5.3.10", 58 | "webpack": "^5.90.1", 59 | "webpack-cli": "^5.1.4" 60 | }, 61 | "dependencies": { 62 | "basic-auth": "^2.0.1", 63 | "bytes": "^3.1.2", 64 | "chalk": "^5.3.0", 65 | "cli-spinner": "^0.2.10", 66 | "compression": "^1.7.4", 67 | "cookies": "^0.9.1", 68 | "cors": "^2.8.5", 69 | "cron": "^3.1.6", 70 | "express": "^4.18.2", 71 | "form-data": "^4.0.0", 72 | "formdata-node": "^6.0.3", 73 | "fs-extra": "^11.2.0", 74 | "get-port": "^7.0.0", 75 | "ip6addr": "^0.2.5", 76 | "is-png": "^3.0.1", 77 | "lodash-es": "^4.17.21", 78 | "lokijs": "^1.5.12", 79 | "ms": "^2.1.3", 80 | "node-fetch": "^2.7.0", 81 | "sharp": "^0.33.2", 82 | "signal-exit": "^4.1.0", 83 | "tcp-port-used": "^1.0.2", 84 | "text-to-svg": "^3.1.5", 85 | "validate-ip-node": "^1.0.8", 86 | "yargs": "^17.7.2" 87 | }, 88 | "repository": { 89 | "type": "git", 90 | "url": "https://github.com/ortexx/spreadable" 91 | }, 92 | "engines": { 93 | "node": ">=20.0.0" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/approval/transports/approval/index.js: -------------------------------------------------------------------------------- 1 | import Service from "../../../service.js"; 2 | import utils from "../../../utils.js"; 3 | import errors from "../../../errors.js"; 4 | 5 | export default (Parent) => { 6 | /** 7 | * Approval transport 8 | */ 9 | return class Approval extends (Parent || Service) { 10 | /** 11 | * @param {object} options 12 | */ 13 | constructor(options = {}) { 14 | super(...arguments); 15 | Object.assign(this, { 16 | approversCount: 'r-6', 17 | decisionLevel: '66.6%', 18 | period: '5m' 19 | }, options); 20 | } 21 | 22 | /** 23 | * @see Approval.prototype.init 24 | */ 25 | async init() { 26 | this.period = utils.getMs(this.period); 27 | super.init.apply(this, arguments); 28 | } 29 | 30 | /** 31 | * Test the approval client info 32 | * 33 | * @param {*} info 34 | */ 35 | clientInfoTest(info) { 36 | utils.validateSchema(this.getClientInfoSchema(), info); 37 | } 38 | 39 | /** 40 | * Test the approval time 41 | * 42 | * @param {number} time 43 | */ 44 | async startTimeTest(time) { 45 | if (typeof time != 'number' || isNaN(time)) { 46 | throw new errors.WorkError(`Approval time must be an integer`, 'ERR_SPREADABLE_WRONG_APPROVAL_TIME'); 47 | } 48 | 49 | const closest = utils.getClosestPeriodTime(Date.now(), this.period); 50 | 51 | if (time !== closest && time !== closest - this.period) { 52 | throw new errors.WorkError(`Incorrect approval time`, 'ERR_SPREADABLE_WRONG_APPROVAL_TIME'); 53 | } 54 | } 55 | 56 | /** 57 | * Calculate the approvers count 58 | * 59 | * @returns {integer} 60 | */ 61 | async calculateApproversCount() { 62 | return await this.node.getValueGivenNetworkSize(this.approversCount); 63 | } 64 | 65 | /** 66 | * Calculate the decision level 67 | * 68 | * @async 69 | * @returns {number} 70 | */ 71 | async calculateDescisionLevel() { 72 | let level = this.decisionLevel; 73 | const count = await this.calculateApproversCount(); 74 | 75 | if (typeof level == 'string' && level.match('%')) { 76 | level = Math.ceil(count * parseFloat(level) / 100); 77 | } 78 | 79 | return level; 80 | } 81 | 82 | /** 83 | * Test the approver decisions count 84 | * 85 | * @async 86 | * @param {number} count 87 | */ 88 | async approversDecisionCountTest(count) { 89 | const decistionLevel = await this.calculateDescisionLevel(count); 90 | 91 | if (count < decistionLevel) { 92 | throw new errors.WorkError('Not enough approvers to make a decision', 'ERR_SPREADABLE_NOT_ENOUGH_APPROVER_DECISIONS'); 93 | } 94 | } 95 | 96 | /** 97 | * Create the info 98 | * 99 | * @async 100 | * @param {object} approver 101 | * @returns {object} 102 | */ 103 | async createInfo() { 104 | throw new Error('Method "createInfo" is required for approval transport'); 105 | } 106 | 107 | /** 108 | * Create the question 109 | * 110 | * @async 111 | * @param {array} data 112 | * @param {*} info 113 | * @param {string} clientIp 114 | * @returns {object} 115 | */ 116 | async createQuestion() { 117 | throw new Error('Method "createQuestion" is required for approval transport'); 118 | } 119 | 120 | /** 121 | * Check the answer 122 | * 123 | * @async 124 | * @param {object} approver 125 | * @param {string} answer 126 | * @param {string[]} approvers 127 | * @returns {boolean} 128 | */ 129 | async checkAnswer() { 130 | throw new Error('Method "checkAnswer" is required for approval transport'); 131 | } 132 | 133 | /** 134 | * Get the client info schema 135 | * 136 | * @returns {object} 137 | */ 138 | getClientInfoSchema() { 139 | throw new Error('Method "getClientInfoSchema" is required for approval transport'); 140 | } 141 | 142 | /** 143 | * Get the client answer schema 144 | * 145 | * @returns {object} 146 | */ 147 | getClientAnswerSchema() { 148 | throw new Error('Method "getClientAnswerSchema" is required for approval transport'); 149 | } 150 | 151 | /** 152 | * Get the approver info schema 153 | * 154 | * @returns {object} 155 | */ 156 | getApproverInfoSchema() { 157 | throw new Error('Method "getApproverInfoSchema" is required for approval transport'); 158 | } 159 | }; 160 | }; 161 | -------------------------------------------------------------------------------- /src/approval/transports/captcha/fonts/ipag.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ortexx/spreadable/7472e8ceb349517c2e9a10bb74056252582ed805/src/approval/transports/captcha/fonts/ipag.ttf -------------------------------------------------------------------------------- /src/approval/transports/captcha/index.js: -------------------------------------------------------------------------------- 1 | import isPng from "is-png"; 2 | import random from "lodash-es/random.js"; 3 | import path from "path"; 4 | import sharp from "sharp"; 5 | import textToSvg from "text-to-svg"; 6 | import { fileURLToPath } from 'url'; 7 | import utils from "../../../utils.js"; 8 | import approval from "../approval/index.js"; 9 | 10 | const Approval = approval(); 11 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 12 | 13 | export default (Parent) => { 14 | /** 15 | * Captcha approval transport 16 | */ 17 | return class ApprovalCaptcha extends (Parent || Approval) { 18 | /** 19 | * @param {object} options 20 | */ 21 | constructor(options = {}) { 22 | super(...arguments); 23 | Object.assign(this, { 24 | decisionLevel: '75%', 25 | captchaShadows: 1, 26 | captchaLength: 6, 27 | captchaWidth: 240, 28 | captchaBackground: 'transparent', 29 | captchaColor: 'random' 30 | }, options); 31 | } 32 | 33 | /** 34 | * @see Approval.prototype.init 35 | */ 36 | async init() { 37 | this.approversCount = this.captchaLength; 38 | this.svgHandler = await this.createSvgHandler(); 39 | super.init.apply(this, arguments); 40 | } 41 | 42 | /** 43 | * Create SVG handler 44 | * 45 | * @async 46 | * @returns {object} 47 | */ 48 | async createSvgHandler() { 49 | return new Promise((resolve, reject) => { 50 | textToSvg.load(path.join(__dirname, '/fonts/ipag.ttf'), (err, handler) => { 51 | if (err) { 52 | return reject(err); 53 | } 54 | 55 | resolve(handler); 56 | }); 57 | }); 58 | } 59 | 60 | /** 61 | * @see Approval.prototype.createInfo 62 | */ 63 | async createInfo(approver) { 64 | const options = Object.assign({}, this, approver.info || {}); 65 | const answer = this.createText(); 66 | const bgImg = await this.createImage(answer, options); 67 | const buffer = await bgImg.toBuffer(); 68 | return { info: buffer.toString('base64'), answer }; 69 | } 70 | 71 | /** 72 | * @see Approval.prototype.createQuestion 73 | */ 74 | async createQuestion(data, info = {}) { 75 | const options = Object.assign({}, this, info); 76 | const size = Math.floor(options.captchaWidth / this.captchaLength); 77 | const width = options.captchaWidth; 78 | const bgImg = sharp({ 79 | create: { 80 | width, 81 | height: size, 82 | channels: 4, 83 | background: 'transparent' 84 | } 85 | }); 86 | bgImg.png(); 87 | let length = this.captchaLength; 88 | const comp = []; 89 | 90 | for (let i = 0; i < data.length; i++) { 91 | const img = sharp(Buffer.from(data[i], 'base64')); 92 | const count = Math.floor(length / (data.length - i)); 93 | img.extract({ left: 0, top: 0, height: size, width: size * count }); 94 | comp.push({ 95 | input: await img.toBuffer(), 96 | left: size * (this.captchaLength - length), 97 | top: 0 98 | }); 99 | length -= count; 100 | } 101 | 102 | bgImg.composite(comp); 103 | const buffer = await bgImg.toBuffer(); 104 | return `data:image/png;base64,${buffer.toString('base64')}`; 105 | } 106 | 107 | /** 108 | * @see Approval.prototype.checkAnswer 109 | */ 110 | async checkAnswer(approver, answer, approvers) { 111 | let length = this.captchaLength; 112 | 113 | for (let i = 0; i < approvers.length; i++) { 114 | const address = approvers[i]; 115 | const count = Math.floor(length / (approvers.length - i)); 116 | const cLength = this.captchaLength - length; 117 | const from = String(answer).slice(cLength, cLength + count).toLowerCase(); 118 | const to = String(approver.answer).slice(0, count).toLowerCase(); 119 | 120 | if (address == this.node.address) { 121 | return from === to; 122 | } 123 | 124 | length -= count; 125 | } 126 | 127 | return false; 128 | } 129 | 130 | /** 131 | * Create an image 132 | * 133 | * @param {string} text 134 | * @param {options} options 135 | * @param {string} options.captchaBackground 136 | * @param {number} options.captchaWidth 137 | * @param {string} options.captchaColor 138 | * @returns {object} 139 | */ 140 | async createImage(text, options) { 141 | const length = this.captchaLength; 142 | const bg = options.captchaBackground; 143 | const size = Math.floor(options.captchaWidth / length); 144 | const width = options.captchaWidth; 145 | const bgImg = sharp({ 146 | create: { 147 | width, 148 | height: size, 149 | channels: 4, 150 | background: bg 151 | } 152 | }); 153 | bgImg.png(); 154 | const comp = []; 155 | const maxFontSize = size * 0.9; 156 | const minFontSize = size * 0.5; 157 | 158 | for (let i = 0; i < length; i++) { 159 | const color = (options.captchaColor == 'random') ? utils.getRandomHexColor() : options.captchaColor; 160 | const strokeColor = utils.invertHexColor(color); 161 | const fillOpacity = random(0.25, 1, true); 162 | const strokeOpacity = fillOpacity * 0.5; 163 | const strokeWidth = random(1, 1.3, true); 164 | 165 | for (let k = 0; k < options.captchaShadows + 1; k++) { 166 | const fontSize = random(minFontSize, maxFontSize); 167 | const dev = Math.floor(size - fontSize); 168 | const svg = this.svgHandler.getSVG(text[i], { 169 | fontSize, 170 | anchor: 'left top', 171 | attributes: { 172 | fill: color, 173 | stroke: strokeColor, 174 | 'stroke-opacity': strokeOpacity, 175 | 'fill-opacity': fillOpacity, 176 | 'stroke-width': strokeWidth 177 | } 178 | }); 179 | const txtImg = sharp(Buffer.from(svg)); 180 | txtImg.rotate(random(-75, 75), { background: 'transparent' }); 181 | comp.push({ 182 | input: await txtImg.toBuffer(), 183 | left: size * i + random(0, dev), 184 | top: random(0, dev) 185 | }); 186 | } 187 | } 188 | 189 | bgImg.composite(comp); 190 | return bgImg; 191 | } 192 | 193 | /** 194 | * Create a random captcha text 195 | * 196 | * @returns {string} 197 | */ 198 | createText() { 199 | return [...Array(this.captchaLength)].map(() => (~~(Math.random() * 36)).toString(36)).join(''); 200 | } 201 | 202 | /** 203 | * @see Approval.prototype.getClientInfoSchema 204 | */ 205 | getClientInfoSchema() { 206 | return [ 207 | { 208 | type: 'object', 209 | props: { 210 | captchaWidth: { 211 | type: 'number', 212 | value: val => val >= 100 && val <= 500 && Number.isInteger(val) 213 | }, 214 | captchaBackground: { 215 | type: 'string', 216 | value: val => val == 'transparent' || utils.isHexColor(val) 217 | }, 218 | captchaColor: { 219 | type: 'string', 220 | value: val => val == 'random' || utils.isHexColor(val) 221 | } 222 | }, 223 | expected: true 224 | }, 225 | 'undefined' 226 | ]; 227 | } 228 | 229 | /** 230 | * @see Approval.prototype.getClientAnswerSchema 231 | */ 232 | getClientAnswerSchema() { 233 | return { 234 | type: 'string' 235 | }; 236 | } 237 | 238 | /** 239 | * @see Approval.prototype.getApproverInfoSchema 240 | */ 241 | getApproverInfoSchema() { 242 | return { 243 | type: 'string', 244 | value: val => Buffer.byteLength(val) < 1024 * 32 && isPng(Buffer.from(val, 'base64')) 245 | }; 246 | } 247 | }; 248 | }; 249 | -------------------------------------------------------------------------------- /src/approval/transports/client/index.js: -------------------------------------------------------------------------------- 1 | import approval from "../approval/index.js"; 2 | import utils from "../../../utils.js"; 3 | import schema from "../../../schema.js"; 4 | const Approval = approval(); 5 | 6 | export default (Parent) => { 7 | /** 8 | * Client approval transport 9 | */ 10 | return class ApprovalClient extends (Parent || Approval) { 11 | /** 12 | * @see Approval.prototype.createInfo 13 | */ 14 | async createInfo(approver) { 15 | return { 16 | info: approver.clientIp, 17 | answer: approver.clientIp 18 | }; 19 | } 20 | 21 | /** 22 | * @see Approval.prototype.createQuestion 23 | */ 24 | async createQuestion(data, info, clientIp) { 25 | return clientIp; 26 | } 27 | 28 | /** 29 | * @see Approval.prototype.checkAnswer 30 | */ 31 | async checkAnswer(approver, answer) { 32 | return utils.isIpEqual(approver.answer, answer); 33 | } 34 | 35 | /** 36 | * @see Approval.prototype.getClientInfoSchema 37 | */ 38 | getClientInfoSchema() { 39 | return { 40 | type: 'undefined' 41 | }; 42 | } 43 | 44 | /** 45 | * @see Approval.prototype.getClientAnswerSchema 46 | */ 47 | getClientAnswerSchema() { 48 | return schema.getClientIp(); 49 | } 50 | 51 | /** 52 | * @see Approval.prototype.getApproverInfoSchema 53 | */ 54 | getApproverInfoSchema() { 55 | return schema.getClientIp(); 56 | } 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /src/behavior/transports/behavior/index.js: -------------------------------------------------------------------------------- 1 | import Service from "../../../service.js"; 2 | 3 | export default (Parent) => { 4 | /** 5 | * Behavior transport 6 | */ 7 | return class Behavior extends (Parent || Service) { 8 | /** 9 | * @param {object} [options] 10 | */ 11 | constructor(options = {}) { 12 | super(...arguments); 13 | Object.assign(this, options); 14 | } 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/behavior/transports/fail/index.js: -------------------------------------------------------------------------------- 1 | import behavior from "../behavior/index.js"; 2 | import utils from "../../../utils.js"; 3 | const Behavior = behavior(); 4 | 5 | export default (Parent) => { 6 | /** 7 | * Fail behavior transport 8 | */ 9 | return class BehaviorlFail extends (Parent || Behavior) { 10 | /** 11 | * @param {object} [options] 12 | */ 13 | constructor(options) { 14 | super(...arguments); 15 | Object.assign(this, { 16 | ban: true, 17 | banLifetime: '18d', 18 | banDelay: 'auto', 19 | failLifetime: 'auto', 20 | failSuspicionLevel: 30 21 | }, options); 22 | } 23 | 24 | /** 25 | * Create a step 26 | * 27 | * @param {boolean} add 28 | * @param {number|boolean[]} step 29 | * @param {object} [options] 30 | * @param {boolean} [options.exp] 31 | * @returns {number|function} 32 | */ 33 | createStep(add, step = 1, options = {}) { 34 | options = Object.assign({ exp: this.exp }, options); 35 | 36 | if (Array.isArray(step)) { 37 | step = step.map(s => !!s).reduce((p, c, i, a) => !c ? (p += 1 / a.length) : p, 0); 38 | } 39 | 40 | if (typeof step == 'function') { 41 | return step; 42 | } 43 | 44 | if (!options.exp) { 45 | return step; 46 | } 47 | 48 | return behavior => { 49 | if (!behavior) { 50 | return step; 51 | } 52 | 53 | const coef = Math.sqrt(add ? behavior.up : behavior.down) || 1; 54 | return add ? step * coef : step / coef; 55 | }; 56 | } 57 | 58 | /** 59 | * @see Behavior.prototype.init 60 | */ 61 | async init() { 62 | this.banDelay = utils.getMs(this.banDelay); 63 | this.banLifetime = utils.getMs(this.banLifetime); 64 | this.failLifetime = utils.getMs(this.failLifetime); 65 | super.init.apply(this, arguments); 66 | } 67 | 68 | /** 69 | * Get the fail 70 | * 71 | * @param {string} address 72 | * @returns {object} 73 | */ 74 | async get(address) { 75 | return await this.node.db.getBehaviorFail(this.action, address); 76 | } 77 | 78 | /** 79 | * Add the fail 80 | * 81 | * @see BehaviorlFail.prototype.createStep 82 | * @returns {object} 83 | */ 84 | async add(address, step, options) { 85 | const behavior = await this.node.db.addBehaviorFail(this.name, address, this.createStep(true, step, options)); 86 | this.node.logger.warn(`Behavior fail "${this.name}" for "${this.node.address}" as ${JSON.stringify(behavior, null, 2)}`); 87 | return behavior; 88 | } 89 | 90 | /** 91 | * Subtract the fail 92 | * 93 | * @see BehaviorlFail.prototype.createStep 94 | * @returns {object} 95 | */ 96 | async sub(address, step, options) { 97 | return await this.node.db.subBehaviorFail(this.name, address, this.createStep(false, step, options)); 98 | } 99 | }; 100 | }; 101 | -------------------------------------------------------------------------------- /src/browser/client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": ["@babel/plugin-transform-runtime"] 4 | } -------------------------------------------------------------------------------- /src/browser/client/index.js: -------------------------------------------------------------------------------- 1 | import client from "../../client.js"; 2 | 3 | export default client(); 4 | -------------------------------------------------------------------------------- /src/browser/mock/index.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/cache/transports/cache/index.js: -------------------------------------------------------------------------------- 1 | import merge from "lodash-es/merge.js"; 2 | import Service from "../../../service.js"; 3 | import utils from "../../../utils.js"; 4 | 5 | export default (Parent) => { 6 | /** 7 | * Cache transport 8 | */ 9 | return class Cache extends (Parent || Service) { 10 | /** 11 | * @param {object} options 12 | */ 13 | constructor(options = {}) { 14 | super(...arguments); 15 | this.options = merge({ 16 | limit: 50000 17 | }, options); 18 | 19 | if (this.options.lifetime !== undefined) { 20 | this.options.lifetime = utils.getMs(this.options.lifetime); 21 | } 22 | } 23 | 24 | /** 25 | * Get the cache 26 | * 27 | * @async 28 | * @param {string} key 29 | */ 30 | async get() { 31 | throw new Error('Method "get" is required for cache transport'); 32 | } 33 | 34 | /** 35 | * Get the cache 36 | * 37 | * @async 38 | * @param {string} key 39 | * @param {object} value 40 | */ 41 | async set() { 42 | throw new Error('Method "set" is required for cache transport'); 43 | } 44 | 45 | /** 46 | * Remove the cache 47 | * 48 | * @async 49 | * @param {string} key 50 | */ 51 | async remove() { 52 | throw new Error('Method "remove" is required for cache transport'); 53 | } 54 | 55 | /** 56 | * Normalize the cache 57 | * 58 | * @async 59 | */ 60 | async normalize() { 61 | throw new Error('Method "normalize" is required for cache transport'); 62 | } 63 | 64 | /** 65 | * Flush the cache 66 | * 67 | * @async 68 | */ 69 | async flush() { 70 | throw new Error('Method "flush" is required for cache transport'); 71 | } 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /src/cache/transports/database/index.js: -------------------------------------------------------------------------------- 1 | import cache from "../cache/index.js"; 2 | const Cache = cache(); 3 | 4 | export default (Parent) => { 5 | /** 6 | * Database cache transport 7 | */ 8 | return class CacheDatabase extends (Parent || Cache) { 9 | /** 10 | * @see Cache.prototype.get 11 | */ 12 | async get(key) { 13 | const cache = await this.node.db.getCache(this.name, key); 14 | return cache ? { key: cache.key, value: cache.value } : null; 15 | } 16 | 17 | /** 18 | * @see Cache.prototype.set 19 | */ 20 | async set(key, value) { 21 | return await this.node.db.setCache(this.name, key, value, this.options); 22 | } 23 | 24 | /** 25 | * @see Cache.prototype.remove 26 | */ 27 | async remove(key) { 28 | return await this.node.db.removeCache(this.name, key); 29 | } 30 | 31 | /** 32 | * @see Cache.prototype.normalize 33 | */ 34 | async normalize() { 35 | return await this.node.db.normalizeCache(this.name, this.options); 36 | } 37 | 38 | /** 39 | * @see Cache.prototype.flush 40 | */ 41 | async flush() { 42 | return await this.node.db.flushCache(this.name); 43 | } 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import merge from "lodash-es/merge.js"; 2 | import shuffle from "lodash-es/shuffle.js"; 3 | import FormData from "form-data"; 4 | import https from "https"; 5 | import fetch from "node-fetch"; 6 | import qs from "querystring"; 7 | import utils from "./utils.js"; 8 | import errors from "./errors.js"; 9 | import ms from "ms"; 10 | import loggerConsole from "./logger/transports/console/index.js"; 11 | import taskInterval from "./task/transports/interval/index.js"; 12 | import Service from "./service.js"; 13 | import pack from "../package.json" with { type: "json" }; 14 | 15 | const LoggerConsole = loggerConsole(); 16 | const TaskInterval = taskInterval(); 17 | 18 | export default (Parent) => { 19 | /** 20 | * Class to manage client requests to the network 21 | */ 22 | return class Client extends (Parent || Service) { 23 | static get version() { return pack.version; } 24 | static get codename() { return pack.name; } 25 | static get utils() { return utils; } 26 | static get errors() { return errors; } 27 | static get LoggerTransport() { return LoggerConsole; } 28 | static get TaskTransport() { return TaskInterval; } 29 | 30 | /** 31 | * Get the auth cookie value 32 | * 33 | * @returns {object} 34 | */ 35 | static getAuthCookieValue() { 36 | if (typeof location != 'object' || !location.hostname) { 37 | return null; 38 | } 39 | 40 | const address = this.getPageAddress(); 41 | const name = `spreadableNetworkAuth[${address}]`; 42 | const value = "; " + document.cookie; 43 | const parts = value.split("; " + name + "="); 44 | const res = parts.length == 2 && parts.pop().split(";").shift(); 45 | return res ? JSON.parse(atob(res)) : null; 46 | } 47 | 48 | /** 49 | * Get the page address 50 | * 51 | * @returns {string} 52 | */ 53 | static getPageAddress() { 54 | if (typeof location != 'object' || !location.hostname) { 55 | return ''; 56 | } 57 | 58 | return `${location.hostname}:${location.port || (this.getPageProtocol() == 'https' ? 443 : 80)}`; 59 | } 60 | 61 | /** 62 | * Get the page protocol 63 | * 64 | * @returns {string} 65 | */ 66 | static getPageProtocol() { 67 | if (typeof location != 'object' || !location.protocol) { 68 | return ''; 69 | } 70 | 71 | return location.protocol.split(':')[0]; 72 | } 73 | 74 | /** 75 | * @param {object} options 76 | * @param {string|string[]} options.address 77 | */ 78 | constructor(options = {}) { 79 | super(...arguments); 80 | this.options = merge({ 81 | request: { 82 | pingTimeout: '1s', 83 | clientTimeout: '10s', 84 | approvalQuestionTimeout: '20s', 85 | ignoreVersion: false 86 | }, 87 | auth: this.constructor.getAuthCookieValue(), 88 | address: this.constructor.getPageAddress(), 89 | https: this.constructor.getPageProtocol() == 'https', 90 | logger: { 91 | level: 'info' 92 | }, 93 | task: { 94 | workerChangeInterval: '30s' 95 | } 96 | }, options); 97 | !this.options.logger && (this.options.logger = { level: false }); 98 | typeof this.options.logger == 'string' && (this.options.logger = { level: this.options.logger }); 99 | this.LoggerTransport = this.constructor.LoggerTransport; 100 | this.TaskTransport = this.constructor.TaskTransport; 101 | this.address = this.options.address; 102 | this.__isMasterService = true; 103 | this.prepareOptions(); 104 | } 105 | 106 | /** 107 | * Initialize the client 108 | * 109 | * @async 110 | */ 111 | async init() { 112 | if (!this.address) { 113 | throw new Error('You must pass the node address'); 114 | } 115 | 116 | await this.prepareServices(); 117 | await super.init.apply(this, arguments); 118 | let address = this.address; 119 | Array.isArray(address) && (address = shuffle(address)); 120 | this.availableAddress = await this.getAvailableAddress(address); 121 | 122 | if (!this.availableAddress) { 123 | throw new Error('Provided addresses are not available'); 124 | } 125 | 126 | this.workerAddress = this.availableAddress; 127 | } 128 | 129 | /** 130 | * Prepare the services 131 | * 132 | * @async 133 | */ 134 | async prepareServices() { 135 | await this.prepareLogger(); 136 | await this.prepareTask(); 137 | } 138 | 139 | /** 140 | * Prepare the logger service 141 | * 142 | * @async 143 | */ 144 | async prepareLogger() { 145 | this.logger = await this.addService('logger', new this.LoggerTransport(this.options.logger)); 146 | } 147 | 148 | /** 149 | * Prepare the task service 150 | * 151 | * @async 152 | */ 153 | async prepareTask() { 154 | this.options.task && (this.task = await this.addService('task', new this.TaskTransport(this.options.task))); 155 | 156 | if (!this.task) { 157 | return; 158 | } 159 | 160 | if (this.options.task.workerChangeInterval) { 161 | await this.task.add('workerChange', this.options.task.workerChangeInterval, () => this.changeWorker()); 162 | } 163 | } 164 | 165 | /** 166 | * Get an available address from the list 167 | * 168 | * @async 169 | * @param {string|string[]} addresses 170 | * @returns {string} 171 | */ 172 | async getAvailableAddress(addresses) { 173 | !Array.isArray(addresses) && (addresses = [addresses]); 174 | let availableAddress; 175 | 176 | for (let i = 0; i < addresses.length; i++) { 177 | const address = addresses[i]; 178 | 179 | try { 180 | await fetch(`${this.getRequestProtocol()}://${address}/ping`, this.createDefaultRequestOptions({ 181 | method: 'GET', 182 | timeout: this.options.request.pingTimeout 183 | })); 184 | availableAddress = address; 185 | break; 186 | } 187 | catch (err) { 188 | this.logger.warn(err.stack); 189 | } 190 | } 191 | 192 | return availableAddress || null; 193 | } 194 | 195 | /** 196 | * Change the worker address 197 | * 198 | * @async 199 | */ 200 | async changeWorker() { 201 | const lastAddress = this.workerAddress; 202 | const result = await this.request('get-available-node', { address: this.availableAddress }); 203 | const address = result.address; 204 | 205 | if (address == lastAddress) { 206 | return; 207 | } 208 | 209 | try { 210 | await fetch(`${this.getRequestProtocol()}://${address}/ping`, this.createDefaultRequestOptions({ 211 | method: 'GET', 212 | timeout: this.options.request.pingTimeout 213 | })); 214 | this.workerAddress = address; 215 | } 216 | catch (err) { 217 | this.logger.warn(err.stack); 218 | this.workerAddress = lastAddress; 219 | } 220 | } 221 | 222 | /** 223 | * Get the approval question 224 | * 225 | * @param {string} action 226 | * @param {object} [info] 227 | * @param {object} [options] 228 | * @returns {object} 229 | */ 230 | async getApprovalQuestion(action, info, options = {}) { 231 | const timeout = options.timeout || this.options.request.approvalQuestionTimeout; 232 | const timer = this.createRequestTimer(timeout); 233 | const result = await this.request('request-approval-key', Object.assign({}, options, { 234 | body: { action }, 235 | timeout: timer() 236 | })); 237 | const approvers = result.approvers; 238 | const key = result.key; 239 | const startedAt = result.startedAt; 240 | const clientIp = result.clientIp; 241 | const confirmedAddresses = []; 242 | const targets = approvers.map(address => ({ address })); 243 | const results = await this.requestGroup(targets, 'add-approval-info', Object.assign({}, options, { 244 | includeErrors: true, 245 | timeout: timer(this.options.request.clientTimeout), 246 | body: { 247 | action, 248 | key, 249 | info, 250 | startedAt 251 | } 252 | })); 253 | 254 | for (let i = 0; i < results.length; i++) { 255 | const result = results[i]; 256 | 257 | if (result instanceof Error) { 258 | continue; 259 | } 260 | 261 | confirmedAddresses.push(targets[i].address); 262 | } 263 | 264 | const res = await this.request('request-approval-question', Object.assign({}, options, { 265 | body: { 266 | action, 267 | key, 268 | info, 269 | confirmedAddresses 270 | }, 271 | timeout: timer() 272 | })); 273 | return { 274 | action, 275 | key, 276 | question: res.question, 277 | approvers: confirmedAddresses, 278 | startedAt, 279 | clientIp 280 | }; 281 | } 282 | 283 | /** 284 | * Make a group request 285 | * 286 | * @async 287 | * @param {array} arr 288 | * @param {string} action 289 | * @param {object} [options] 290 | * @returns {object} 291 | */ 292 | async requestGroup(arr, action, options = {}) { 293 | const requests = []; 294 | 295 | for (let i = 0; i < arr.length; i++) { 296 | const item = arr[i]; 297 | const address = item.address; 298 | requests.push(new Promise(resolve => { 299 | this.request(action, merge({ address }, options, item.options)) 300 | .then(resolve) 301 | .catch(resolve); 302 | })); 303 | } 304 | 305 | let results = await Promise.all(requests); 306 | !options.includeErrors && (results = results.filter(r => !(r instanceof Error))); 307 | return results; 308 | } 309 | 310 | /** 311 | * Make a request to the api 312 | * 313 | * @async 314 | * @param {string} endpoint 315 | * @param {object} [options] 316 | * @returns {object} 317 | */ 318 | async request(endpoint, options = {}) { 319 | options = merge(this.createDefaultRequestOptions(), options); 320 | let body = options.formData || options.body || {}; 321 | body.timeout = options.timeout; 322 | body.timestamp = Date.now(); 323 | 324 | if (options.approvalInfo) { 325 | const approvalInfo = options.approvalInfo; 326 | delete approvalInfo.question; 327 | 328 | if (!Object.prototype.hasOwnProperty.call(approvalInfo, 'answer')) { 329 | throw new Error('Request "approvalInfo" option must include "answer" property'); 330 | } 331 | 332 | body.approvalInfo = options.formData ? JSON.stringify(approvalInfo) : approvalInfo; 333 | } 334 | 335 | if (options.formData) { 336 | const form = new FormData(); 337 | 338 | for (let key in body) { 339 | let val = body[key]; 340 | 341 | if (typeof val == 'object') { 342 | form.append(key, val.value, val.options); 343 | } 344 | else { 345 | form.append(key, val); 346 | } 347 | } 348 | 349 | options.body = form; 350 | delete options.formData; 351 | } 352 | else { 353 | options.headers['content-type'] = 'application/json'; 354 | options.body = JSON.stringify(body); 355 | } 356 | 357 | if(options.timeout && !options.signal) { 358 | options.signal = AbortSignal.timeout(Math.floor(options.timeout)); 359 | } 360 | 361 | options.url = this.createRequestUrl(endpoint, options); 362 | const start = Date.now(); 363 | let response = {}; 364 | 365 | try { 366 | response = await fetch(options.url, options); 367 | this.logger.info(`Request to "${options.url}": ${ms(Date.now() - start)}`); 368 | 369 | if (response.ok) { 370 | return options.getFullResponse ? response : await response.json(); 371 | } 372 | 373 | const type = (response.headers.get('content-type') || '').match('application/json') ? 'json' : 'text'; 374 | const body = await response[type](); 375 | 376 | if (!body || typeof body != 'object') { 377 | throw new Error(body || 'Unknown error'); 378 | } 379 | 380 | if (!body.code) { 381 | throw new Error(body.message || body); 382 | } 383 | 384 | throw new errors.WorkError(body.message, body.code); 385 | } 386 | catch (err) { 387 | options.timeout && err.type == 'aborted' && (err.type = 'request-timeout'); 388 | //eslint-disable-next-line no-ex-assign 389 | utils.isRequestTimeoutError(err) && (err = utils.createRequestTimeoutError()); 390 | err.response = response; 391 | err.requestOptions = options; 392 | throw err; 393 | } 394 | } 395 | 396 | /** 397 | * Create a api request url 398 | * 399 | * @param {string} endpoint 400 | * @param {object} options 401 | */ 402 | createRequestUrl(endpoint, options = {}) { 403 | const query = options.query ? qs.stringify(options.query) : null; 404 | const address = options.address || this.workerAddress; 405 | let url = `${this.getRequestProtocol()}://${address}/client/${endpoint}`; 406 | query && (url += '?' + query); 407 | return url; 408 | } 409 | 410 | /** 411 | * Create default request options 412 | * 413 | * @param {object} options 414 | * @returns {object} 415 | */ 416 | createDefaultRequestOptions(options = {}) { 417 | const defaults = { 418 | method: 'POST', 419 | timeout: this.options.request.clientTimeout, 420 | headers: {} 421 | }; 422 | 423 | if(!this.options.request.ignoreVersion) { 424 | defaults.headers = { 425 | 'client-version': this.getVersion() 426 | } 427 | } 428 | 429 | if (this.options.auth) { 430 | const username = this.options.auth.username; 431 | const password = this.options.auth.password; 432 | let header = 'Basic '; 433 | 434 | if (typeof Buffer == 'function') { 435 | header += Buffer.from(username + ":" + password).toString('base64'); 436 | } 437 | else { 438 | header += btoa(username + ":" + password); 439 | } 440 | 441 | defaults.headers.authorization = header; 442 | } 443 | 444 | if (options.timeout) { 445 | options.timeout = utils.getMs(options.timeout); 446 | } 447 | 448 | if (typeof this.options.https == 'object' && this.options.https.ca) { 449 | if (!https.Agent) { 450 | options.agent = options.agent || {}; 451 | options.agent.ca = this.options.https.ca; 452 | } 453 | else { 454 | options.agent = options.agent || new https.Agent(); 455 | options.agent.options.ca = this.options.https.ca; 456 | } 457 | } 458 | 459 | return merge({}, defaults, options); 460 | } 461 | 462 | /** 463 | * Create a request timer 464 | * 465 | * @param {number} timeout 466 | * @param {object} [options] 467 | * @returns {function} 468 | */ 469 | createRequestTimer(timeout, options = {}) { 470 | options = Object.assign({ 471 | min: this.options.request.pingTimeout 472 | }, options); 473 | return utils.getRequestTimer(timeout, options); 474 | } 475 | 476 | /** 477 | * Prepare the options 478 | */ 479 | prepareOptions() { 480 | this.options.request.pingTimeout = utils.getMs(this.options.request.pingTimeout); 481 | this.options.request.clientTimeout = utils.getMs(this.options.request.clientTimeout); 482 | this.options.request.approvalQuestionTimeout = utils.getMs(this.options.request.approvalQuestionTimeout); 483 | } 484 | 485 | /** 486 | * Get the request protocol 487 | * 488 | * @returns {string} 489 | */ 490 | getRequestProtocol() { 491 | return this.options.https ? 'https' : 'http'; 492 | } 493 | 494 | /** 495 | * Check the environment 496 | */ 497 | envTest(isBrowser, name) { 498 | const isBrowserEnv = utils.isBrowserEnv(); 499 | 500 | if (isBrowser && !isBrowserEnv) { 501 | throw new Error(`You can't use "${name}" method in the nodejs environment`); 502 | } 503 | 504 | if (!isBrowser && isBrowserEnv) { 505 | throw new Error(`You can't use "${name}" method in the browser environment`); 506 | } 507 | } 508 | }; 509 | }; 510 | -------------------------------------------------------------------------------- /src/db/transports/database/index.js: -------------------------------------------------------------------------------- 1 | import Service from "../../../service.js"; 2 | import merge from "lodash-es/merge.js"; 3 | import path from "path"; 4 | 5 | export default (Parent) => { 6 | /** 7 | * Database transport interface 8 | */ 9 | return class Database extends (Parent || Service) { 10 | /** 11 | * @param {object} options 12 | */ 13 | constructor(options = {}) { 14 | super(...arguments); 15 | this.options = merge({ 16 | backups: { 17 | limit: 3 18 | } 19 | }, options); 20 | } 21 | 22 | /** 23 | * @see Service.prototype.init 24 | */ 25 | async init() { 26 | if (this.options.backups && !this.options.backups.folder) { 27 | this.options.backups.folder = path.join(this.node.storagePath, 'backups', 'db'); 28 | } 29 | 30 | super.init.apply(this, arguments); 31 | } 32 | 33 | /** 34 | * Get the current database path 35 | * 36 | * @async 37 | * @returns {string} 38 | */ 39 | async backup() { 40 | throw new Error('Method "backup" is required for database transport'); 41 | } 42 | 43 | /** 44 | * Restore the database 45 | * 46 | * @async 47 | * @param {number} index 48 | */ 49 | async restore() { 50 | throw new Error('Method "restore" is required for database transport'); 51 | } 52 | 53 | /** 54 | * Set the data 55 | * 56 | * @async 57 | * @param {string} name 58 | * @param {*} value 59 | */ 60 | async setData() { 61 | throw new Error('Method "setData" is required for database transport'); 62 | } 63 | 64 | /** 65 | * Get the data 66 | * 67 | * @async 68 | * @param {string} name 69 | * @returns {*} 70 | */ 71 | async getData() { 72 | throw new Error('Method "getData" is required for database transport'); 73 | } 74 | 75 | /** 76 | * Check the node is master 77 | * 78 | * @async 79 | * @returns {boolean} 80 | */ 81 | async isMaster() { 82 | throw new Error('Method "isMaster" is required for database transport'); 83 | } 84 | 85 | /** 86 | * Get the server 87 | * 88 | * @async 89 | * @param {string} address 90 | * @returns {object} 91 | */ 92 | async getServer() { 93 | throw new Error('Method "getServer" is required for database transport'); 94 | } 95 | 96 | /** 97 | * Get all servers 98 | * 99 | * @async 100 | * @returns {object[]} 101 | */ 102 | async getServers() { 103 | throw new Error('Method "getServers" is required for database transport'); 104 | } 105 | 106 | /** 107 | * Check the node has the slave 108 | * 109 | * @async 110 | * @returns {boolean} 111 | */ 112 | async hasSlave() { 113 | throw new Error('Method "hasSlave" is required for database transport'); 114 | } 115 | 116 | /** 117 | * Get all slaves 118 | * 119 | * @async 120 | * @returns {object[]} 121 | */ 122 | async getSlaves() { 123 | throw new Error('Method "getSlaves" is required for database transport'); 124 | } 125 | 126 | /** 127 | * Get the master 128 | * 129 | * @async 130 | * @param {string} address 131 | * @returns {object} 132 | */ 133 | async getMaster() { 134 | throw new Error('Method "getMaster" is required for database transport'); 135 | } 136 | 137 | /** 138 | * Get all masters 139 | * 140 | * @async 141 | * @returns {object[]} 142 | */ 143 | async getMasters() { 144 | throw new Error('Method "getMasters" is required for database transport'); 145 | } 146 | 147 | /** 148 | * Get the backlink 149 | * 150 | * @async 151 | * @returns {object} 152 | */ 153 | async getBacklink() { 154 | throw new Error('Method "getBacklink" is required for database transport'); 155 | } 156 | 157 | /** 158 | * Get the masters count 159 | * 160 | * @async 161 | * @returns {integer} 162 | */ 163 | async getMastersCount() { 164 | throw new Error('Method "getMastersCount" is required for database transport'); 165 | } 166 | 167 | /** 168 | * Get the slaves count 169 | * 170 | * @async 171 | * @returns {integer} 172 | */ 173 | async getSlavesCount() { 174 | throw new Error('Method "getSlavesCount" is required for database transport'); 175 | } 176 | 177 | /** 178 | * Add the master 179 | * 180 | * @async 181 | * @param {string} address 182 | * @param {integer} size 183 | * @param {boolean} isAccepted 184 | * @param {integer} updatedAt 185 | * @returns {object} 186 | */ 187 | async addMaster() { 188 | throw new Error('Method "addMaster" is required for database transport'); 189 | } 190 | 191 | /** 192 | * Add the slave 193 | * 194 | * @async 195 | * @param {string} address 196 | * @param {string} availability 197 | * @returns {object} 198 | */ 199 | async addSlave() { 200 | throw new Error('Method "addSlave" is required for database transport'); 201 | } 202 | 203 | /** 204 | * Add the backlink 205 | * 206 | * @async 207 | * @param {string} address 208 | * @param {string[]} chain 209 | * @returns {object} 210 | */ 211 | async addBacklink() { 212 | throw new Error('Method "addBacklink" is required for database transport'); 213 | } 214 | 215 | /** 216 | * Remove the master 217 | * 218 | * @async 219 | * @param {string} address 220 | */ 221 | async removeMaster() { 222 | throw new Error('Method "removeMaster" is required for database transport'); 223 | } 224 | 225 | /** 226 | * Remove the server 227 | * 228 | * @async 229 | * @param {string} address 230 | */ 231 | async removeServer() { 232 | throw new Error('Method "removeServer" is required for database transport'); 233 | } 234 | 235 | /** 236 | * Remove the slave 237 | * 238 | * @async 239 | * @param {string} address 240 | */ 241 | async removeSlave() { 242 | throw new Error('Method "removeSlave" is required for database transport'); 243 | } 244 | 245 | /** 246 | * Remove all slaves 247 | * 248 | * @async 249 | */ 250 | async removeSlaves() { 251 | throw new Error('Method "removeSlaves" is required for database transport'); 252 | } 253 | 254 | /** 255 | * Shift the slaves 256 | * 257 | * @async 258 | * @param {integer} [limit=1] 259 | */ 260 | async shiftSlaves() { 261 | throw new Error('Method "shiftSlaves" is required for database transport'); 262 | } 263 | 264 | /** 265 | * Remove the backlink 266 | * 267 | * @async 268 | * @param {string} address 269 | */ 270 | async removeBacklink() { 271 | throw new Error('Method "removeBacklink" is required for database transport'); 272 | } 273 | 274 | /** 275 | * Normalize the servers 276 | * 277 | * @async 278 | */ 279 | async normalizeServers() { 280 | throw new Error('Method "normalizeServers" is required for database transport'); 281 | } 282 | 283 | /** 284 | * Mark the server as successed 285 | * 286 | * @async 287 | * @param {string} address 288 | */ 289 | async successServerAddress() { 290 | throw new Error('Method "successServerAddress" is required for database transport'); 291 | } 292 | 293 | /** 294 | * Mark the server as failed 295 | * 296 | * @async 297 | * @param {string} address 298 | */ 299 | async failedServerAddress() { 300 | throw new Error('Method "failedServerAddress" is required for database transport'); 301 | } 302 | 303 | /** 304 | * Get suspicious candidades 305 | * 306 | * @async 307 | * @param {string} action 308 | * @returns {object[]} 309 | */ 310 | async getBehaviorCandidates() { 311 | throw new Error('Method "getBehaviorCandidates" is required for database transport'); 312 | } 313 | 314 | /** 315 | * Add the candidade 316 | * 317 | * @async 318 | * @param {string} action 319 | * @param {string} address 320 | */ 321 | async addBehaviorCandidate() { 322 | throw new Error('Method "addBehaviorCandidate" is required for database transport'); 323 | } 324 | 325 | /** 326 | * Normalize the candidates 327 | * 328 | * @async 329 | */ 330 | async normalizeBehaviorCandidates() { 331 | throw new Error('Method "normalizeBehaviorCandidates" is required for database transport'); 332 | } 333 | 334 | /** 335 | * Add the delay behavior 336 | * 337 | * @async 338 | * @param {string} action 339 | * @param {string} address 340 | */ 341 | async addBehaviorDelay() { 342 | throw new Error('Method "addBehaviorDelay" is required for database transport'); 343 | } 344 | 345 | /** 346 | * Get the delay behavior 347 | * 348 | * @async 349 | * @param {string} action 350 | * @param {string} address 351 | * @returns {object} 352 | */ 353 | async getBehaviorDelay() { 354 | throw new Error('Method "getBehaviorDelay" is required for database transport'); 355 | } 356 | 357 | /** 358 | * Remove the delay behavior 359 | * 360 | * @async 361 | * @param {string} action 362 | * @param {string} address 363 | */ 364 | async removeBehaviorDelay() { 365 | throw new Error('Method "removeBehaviorDelay" is required for database transport'); 366 | } 367 | 368 | /** 369 | * Clear the delay behaviors 370 | * 371 | * @async 372 | * @param {string} action 373 | */ 374 | async cleanBehaviorDelays() { 375 | throw new Error('Method "cleanBehaviorDelays" is required for database transport'); 376 | } 377 | 378 | /** 379 | * Get the fail behavior 380 | * 381 | * @async 382 | * @param {string} action 383 | * @param {string} address 384 | * @returns {object} 385 | */ 386 | async getBehaviorFail() { 387 | throw new Error('Method "getBehaviorFail" is required for database transport'); 388 | } 389 | 390 | /** 391 | * Add the fail behavior 392 | * 393 | * @async 394 | * @param {string} action 395 | * @param {string} address 396 | * @param {number} [step=1] 397 | */ 398 | async addBehaviorFail() { 399 | throw new Error('Method "addBehaviorFail" is required for database transport'); 400 | } 401 | 402 | /** 403 | * Subtract the fail behavior 404 | * 405 | * @async 406 | * @param {string} action 407 | * @param {string} address 408 | * @param {number} [step=1] 409 | */ 410 | async subBehaviorFail() { 411 | throw new Error('Method "subBehaviorFail" is required for database transport'); 412 | } 413 | 414 | /** 415 | * Clean the fail behavior 416 | * 417 | * @async 418 | * @param {string} action 419 | * @param {string} address 420 | */ 421 | async cleanBehaviorFail() { 422 | throw new Error('Method "cleanBehaviorFail" is required for database transport'); 423 | } 424 | 425 | /** 426 | * Normalize the behavior fails 427 | * 428 | * @async 429 | */ 430 | async normalizeBehaviorFails() { 431 | throw new Error('Method "normalizeBehaviorFails" is required for database transport'); 432 | } 433 | 434 | /** 435 | * Add the approval 436 | * 437 | * @async 438 | * @param {string} action 439 | * @param {string} clientIp 440 | * @param {string} key 441 | * @param {number} startedAt 442 | * @param {*} [info] 443 | * @returns {object} 444 | */ 445 | async addApproval() { 446 | throw new Error('Method "addApproval" is required for database transport'); 447 | } 448 | 449 | /** 450 | * Get the approval 451 | * 452 | * @param {string} key 453 | * @returns {object} 454 | */ 455 | async getApproval() { 456 | throw new Error('Method "getApproval" is required for database transport'); 457 | } 458 | 459 | /** 460 | * Use the approval 461 | * 462 | * @param {string} key 463 | * @param {string} address 464 | */ 465 | async useApproval() { 466 | throw new Error('Method "useApproval" is required for database transport'); 467 | } 468 | 469 | /** 470 | * Start the approval 471 | * 472 | * @param {string} key 473 | * @param {*} answer 474 | */ 475 | async startApproval() { 476 | throw new Error('Method "startApproval" is required for database transport'); 477 | } 478 | 479 | /** 480 | * Normalize the approval 481 | * 482 | * @async 483 | */ 484 | async normalizeApproval() { 485 | throw new Error('Method "normalizeApproval" is required for database transport'); 486 | } 487 | 488 | /** 489 | * Get all banlist 490 | * 491 | * @async 492 | * @returns {object[]} 493 | */ 494 | async getBanlist() { 495 | throw new Error('Method "getBanlist" is required for database transport'); 496 | } 497 | 498 | /** 499 | * Get the banlist address 500 | * 501 | * @async 502 | * @param {string} address 503 | * @returns {object} 504 | */ 505 | async getBanlistAddress() { 506 | throw new Error('Method "getBanlistAddress" is required for database transport'); 507 | } 508 | 509 | /** 510 | * Check the ip is in the banlist 511 | * 512 | * @async 513 | * @param {string} ip - ipv6 address 514 | * @returns {boolean} 515 | */ 516 | async checkBanlistIp(ip) { 517 | return !!this.col.banlist.findOne({ ip }); 518 | } 519 | 520 | /** 521 | * Add the banlist address 522 | * 523 | * @async 524 | * @param {string} address 525 | * @param {number} lifetime 526 | * @param {string} [reason] 527 | */ 528 | async addBanlistAddress() { 529 | throw new Error('Method "addBanlistAddress" is required for database transport'); 530 | } 531 | 532 | /** 533 | * Remove the banlist address 534 | * 535 | * @async 536 | * @param {string} address 537 | */ 538 | async removeBanlistAddress() { 539 | throw new Error('Method "removeBanlistAddress" is required for database transport'); 540 | } 541 | 542 | /** 543 | * Empty the banlist 544 | * 545 | * @async 546 | */ 547 | async emptyBanlist() { 548 | throw new Error('Method "emptyBanlist" is required for database transport'); 549 | } 550 | 551 | /** 552 | * Normalize the banlist 553 | * 554 | * @async 555 | */ 556 | async normalizeBanlist() { 557 | throw new Error('Method "normalizeBanlist" is required for database transport'); 558 | } 559 | 560 | /** 561 | * Get the cache 562 | * 563 | * @async 564 | * @param {string} type 565 | * @param {string} key 566 | */ 567 | async getCache() { 568 | throw new Error('Method "getCache" is required for database transport'); 569 | } 570 | 571 | /** 572 | * Set the cache 573 | * 574 | * @async 575 | * @param {string} type 576 | * @param {string} key 577 | * @param {string} link 578 | */ 579 | async setCache() { 580 | throw new Error('Method "setCache" is required for database transport'); 581 | } 582 | 583 | /** 584 | * Get the cache 585 | * 586 | * @async 587 | * @param {string} type 588 | * @param {string} key 589 | */ 590 | async removeCache() { 591 | throw new Error('Method "removeCache" is required for database transport'); 592 | } 593 | 594 | /** 595 | * Get the cache 596 | * 597 | * @async 598 | * @param {string} type 599 | * @param {object} [options] 600 | */ 601 | async normalizeCache() { 602 | throw new Error('Method "normalizeCache" is required for database transport'); 603 | } 604 | 605 | /** 606 | * Flush the cache 607 | * 608 | * @async 609 | * @param {string} type 610 | */ 611 | async flushCache() { 612 | throw new Error('Method "flushCache" is required for database transport'); 613 | } 614 | }; 615 | }; 616 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | export class WorkError extends Error { 2 | constructor(message, code) { 3 | super(message); 4 | this.code = code; 5 | } 6 | } 7 | 8 | export class AuthError extends Error { 9 | constructor(message) { 10 | super(message); 11 | this.statusCode = 401; 12 | } 13 | } 14 | 15 | export class AccessError extends Error { 16 | constructor(message) { 17 | super(message); 18 | this.statusCode = 403; 19 | } 20 | } 21 | 22 | export class NotFoundError extends Error { 23 | constructor(message) { 24 | super(message); 25 | this.statusCode = 404; 26 | } 27 | } 28 | 29 | export default { 30 | WorkError, 31 | AuthError, 32 | AccessError, 33 | NotFoundError 34 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import node from "./node.js"; 2 | import client from "./client.js"; 3 | 4 | const Node = node(); 5 | const Client = client(); 6 | 7 | export { Client }; 8 | export { Node }; 9 | export default { Client, Node }; 10 | -------------------------------------------------------------------------------- /src/logger/index.js: -------------------------------------------------------------------------------- 1 | import logger from "./transports/logger/index.js"; 2 | import loggerConsole from "./transports/console/index.js"; 3 | import loggerFile from "./transports/file/index.js"; 4 | 5 | const Logger = logger(); 6 | const LoggerConsole = loggerConsole(); 7 | const LoggerFile = loggerFile(); 8 | 9 | export default { 10 | Logger, 11 | LoggerConsole, 12 | LoggerFile 13 | } 14 | -------------------------------------------------------------------------------- /src/logger/transports/adapter/index.js: -------------------------------------------------------------------------------- 1 | import logger from "../logger/index.js"; 2 | import transports from "../../index.js"; 3 | 4 | const Logger = logger(); 5 | 6 | export default (Parent) => { 7 | /** 8 | * Console logger transport 9 | */ 10 | return class LoggerAdapter extends (Parent || Logger) { 11 | static transports = transports; 12 | 13 | constructor() { 14 | super(...arguments); 15 | this.transports = []; 16 | } 17 | 18 | /** 19 | * @see Logger.prototype.init 20 | */ 21 | async init() { 22 | const arr = this.options.transports || []; 23 | 24 | for(let i = 0; i < arr.length; i++) { 25 | const obj = arr[i]; 26 | const transports = this.constructor.transports[obj.transport]; 27 | const CurrentLogger = typeof obj.transport == 'string'? transports: obj.transport; 28 | const logger = new CurrentLogger(obj.options); 29 | logger.node = this.node; 30 | await logger.init(); 31 | this.addTransport(logger); 32 | } 33 | 34 | return await super.init.apply(this, arguments); 35 | } 36 | 37 | /** 38 | * @see Logger.prototype.deinit 39 | */ 40 | async deinit() { 41 | for (let i = 0; i < this.transports.length; i++) { 42 | await this.transports[i].deinit(); 43 | } 44 | 45 | this.transports = []; 46 | return await super.deinit.apply(this, arguments); 47 | } 48 | 49 | /** 50 | * @see Logger.prototype.destroy 51 | */ 52 | async destroy() { 53 | for (let i = 0; i < this.transports.length; i++) { 54 | await this.transports[i].destroy(); 55 | } 56 | 57 | return await super.destroy.apply(this, arguments); 58 | } 59 | 60 | /** 61 | * @see Logger.prototype.log 62 | */ 63 | async log(level, message) { 64 | if (!this.isLevelActive(level)) { 65 | return; 66 | } 67 | 68 | for (let i = 0; i < this.transports.length; i++) { 69 | await this.transports[i].log(level, message); 70 | } 71 | } 72 | 73 | /** 74 | * Add a new transport 75 | * 76 | * @param {Logger} transport 77 | */ 78 | addTransport(transport) { 79 | this.transports.push(transport); 80 | } 81 | 82 | /** 83 | * remove the transport 84 | * 85 | * @param {Logger} transport 86 | */ 87 | removeTransport(transport) { 88 | this.transports.splice(this.transports.indexOf(transport), 1); 89 | } 90 | }; 91 | }; 92 | -------------------------------------------------------------------------------- /src/logger/transports/console/index.js: -------------------------------------------------------------------------------- 1 | import logger from "../logger/index.js"; 2 | import chalk from "chalk"; 3 | import utils from "../../../utils.js"; 4 | 5 | const Logger = logger(); 6 | 7 | export default (Parent) => { 8 | /** 9 | * Console logger transport 10 | */ 11 | return class LoggerConsole extends (Parent || Logger) { 12 | constructor() { 13 | super(...arguments); 14 | this.colors = { 15 | info: 'white', 16 | warn: 'yellow', 17 | error: 'red' 18 | }; 19 | } 20 | 21 | /** 22 | * @see Logger.prototype.log 23 | */ 24 | async log(level, message) { 25 | if (!this.isLevelActive(level)) { 26 | return; 27 | } 28 | 29 | //eslint-disable-next-line no-console 30 | (console[level] || console.log)(utils.isBrowserEnv() ? message : chalk[this.colors[level]](message)); 31 | } 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/logger/transports/file/index.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fse from "fs-extra"; 3 | import merge from "lodash-es/merge.js"; 4 | import logger from "../logger/index.js"; 5 | import utils from "../../../utils.js"; 6 | 7 | const Logger = logger(); 8 | 9 | export default (Parent) => { 10 | /** 11 | * File logger transport 12 | */ 13 | return class LoggerFile extends (Parent || Logger) { 14 | constructor(options) { 15 | options = merge({ 16 | filesCount: 5, 17 | fileMaxSize: '10mb' 18 | }, options); 19 | super(options); 20 | this.defaultLevel = 'warn'; 21 | this.prepareOptions(); 22 | } 23 | 24 | /** 25 | * @see Logger.prototype.init 26 | */ 27 | async init() { 28 | !this.options.folder && (this.options.folder = path.join(this.node.storagePath, 'logs')); 29 | this.__filesQueue = new utils.FilesQueue(this.options.folder, { 30 | limit: this.options.filesCount, 31 | ext: 'log' 32 | }); 33 | await this.normalizeFilesCount(); 34 | return await super.init.apply(this, arguments); 35 | } 36 | 37 | /** 38 | * @see Logger.prototype.deinit 39 | */ 40 | async deinit() { 41 | this.__queue = []; 42 | return await super.deinit.apply(this, arguments); 43 | } 44 | 45 | /** 46 | * @see Logger.prototype.destroy 47 | */ 48 | async destroy() { 49 | await fse.remove(this.options.folder); 50 | return await super.destroy.apply(this, arguments); 51 | } 52 | 53 | /** 54 | * @see Logger.prototype.log 55 | */ 56 | async log(level, message) { 57 | if (!this.isLevelActive(level)) { 58 | return; 59 | } 60 | 61 | return await this.__filesQueue.blocking(async () => { 62 | let lastFile = this.getLastFile(); 63 | message = this.prepareMessage(message, level); 64 | 65 | if (!lastFile) { 66 | lastFile = await this.addNewFile(); 67 | } 68 | 69 | if (lastFile.stat.size + this.getMessageSize(message) > this.options.fileMaxSize) { 70 | lastFile = await this.addNewFile(); 71 | } 72 | 73 | await this.addNewMessage(message, lastFile.filePath); 74 | }); 75 | } 76 | 77 | /** 78 | * Add a new message 79 | * 80 | * @param {string} message 81 | * @param {string} filePath 82 | */ 83 | async addNewMessage(message, filePath) { 84 | await fse.appendFile(filePath, message + '\n'); 85 | } 86 | 87 | /** 88 | * Add a new file 89 | * 90 | * @async 91 | * @returns {Object} 92 | */ 93 | async addNewFile() { 94 | const filePath = path.join(this.__filesQueue.folderPath, this.__filesQueue.createNewName()); 95 | await fse.ensureFile(filePath); 96 | await this.__filesQueue.normalize(); 97 | return this.getLastFile(); 98 | } 99 | 100 | /** 101 | * Normalize the files count 102 | * 103 | * @async 104 | */ 105 | async normalizeFilesCount() { 106 | await this.__filesQueue.normalize(); 107 | 108 | if (!this.__filesQueue.files.length) { 109 | return await this.addNewFile(); 110 | } 111 | } 112 | 113 | /** 114 | * Get the last file 115 | * 116 | * @returns {object} 117 | */ 118 | getLastFile() { 119 | return this.__filesQueue.getLast(); 120 | } 121 | 122 | /** 123 | * Get the message size 124 | * 125 | * @param {string} message 126 | * @returns {number} 127 | */ 128 | getMessageSize(message) { 129 | return Buffer.byteLength(message, 'utf8'); 130 | } 131 | 132 | /** 133 | * Prepare the message 134 | * 135 | * @param {string} message 136 | * @param {string} level 137 | * @returns {number} 138 | */ 139 | prepareMessage(message, level) { 140 | message = { message, date: new Date().toUTCString(), level }; 141 | return JSON.stringify(message, null, 2); 142 | } 143 | 144 | /** 145 | * Prepare the options 146 | */ 147 | prepareOptions() { 148 | this.options.fileMaxSize = utils.getBytes(this.options.fileMaxSize); 149 | } 150 | }; 151 | }; 152 | -------------------------------------------------------------------------------- /src/logger/transports/logger/index.js: -------------------------------------------------------------------------------- 1 | import Service from "../../../service.js"; 2 | 3 | export default (Parent) => { 4 | 5 | /** 6 | * Logger transport interface 7 | */ 8 | return class Logger extends (Parent || Service) { 9 | 10 | /** 11 | * @param {object} options 12 | */ 13 | constructor(options = {}) { 14 | super(...arguments); 15 | this.options = options; 16 | this.levels = ['info', 'warn', 'error']; 17 | this.defaultLevel = 'info'; 18 | } 19 | 20 | /** 21 | * Initialize the logger 22 | * 23 | * @async 24 | */ 25 | async init() { 26 | this.setLevel(this.options.level === undefined ? this.defaultLevel : this.options.level); 27 | await super.init.apply(this, arguments); 28 | } 29 | 30 | /** 31 | * Deinitialize the logger 32 | * 33 | * @async 34 | */ 35 | async deinit() { 36 | this.setLevel(false); 37 | await super.deinit.apply(this, arguments); 38 | } 39 | 40 | /** 41 | * Log by levels 42 | * 43 | * @async 44 | * @param {string} level 45 | */ 46 | async log() { 47 | throw new Error('Method "log" is required for logger transport'); 48 | } 49 | 50 | /** 51 | * Log info 52 | * 53 | * @async 54 | */ 55 | async info(...args) { 56 | await this.log('info', ...args); 57 | } 58 | 59 | /** 60 | * Log a warning 61 | * 62 | * @async 63 | */ 64 | async warn(...args) { 65 | await this.log('warn', ...args); 66 | } 67 | 68 | /** 69 | * Log an error 70 | * 71 | * @async 72 | */ 73 | async error(...args) { 74 | await this.log('error', ...args); 75 | } 76 | 77 | /** 78 | * Check the log level is active 79 | * 80 | * @param {string} level 81 | */ 82 | isLevelActive(level) { 83 | if (!this.level) { 84 | return false; 85 | } 86 | return this.levels.indexOf(level) >= this.levels.indexOf(this.level); 87 | } 88 | 89 | /** 90 | * Set the active level 91 | * 92 | * @param {string} level 93 | */ 94 | setLevel(level) { 95 | if (level === false) { 96 | return this.level = false; 97 | } 98 | if (this.levels.indexOf(level) == -1) { 99 | throw new Error(`Wrong logger level "${level}"`); 100 | } 101 | this.level = level; 102 | } 103 | }; 104 | }; 105 | -------------------------------------------------------------------------------- /src/schema.js: -------------------------------------------------------------------------------- 1 | import utils from "./utils.js"; 2 | import merge from "lodash-es/merge.js"; 3 | 4 | const schema = {}; 5 | 6 | schema.getAddress = function () { 7 | return { 8 | type: 'string', 9 | value: utils.isValidAddress.bind(utils) 10 | }; 11 | }; 12 | 13 | schema.getClientIp = function () { 14 | return { 15 | type: 'string', 16 | value: utils.isValidIp.bind(utils) 17 | }; 18 | }; 19 | 20 | schema.getApprovers = function () { 21 | return { 22 | type: 'array', 23 | uniq: true, 24 | items: this.getAddress() 25 | }; 26 | }; 27 | 28 | schema.getStatusResponse = function () { 29 | return { 30 | type: 'object', 31 | props: { 32 | root: 'string', 33 | availability: 'number', 34 | syncAvgTime: 'number', 35 | isMaster: 'boolean', 36 | isNormalized: 'boolean', 37 | isRegistered: 'boolean', 38 | networkSize: 'number' 39 | }, 40 | strict: true 41 | }; 42 | }; 43 | 44 | schema.getStatusPrettyResponse = function () { 45 | return merge(this.getStatusResponse(), { 46 | props: { 47 | availability: 'string', 48 | syncAvgTime: 'string' 49 | } 50 | }); 51 | }; 52 | 53 | schema.getAvailableNodeResponse = function () { 54 | return { 55 | type: 'object', 56 | props: { 57 | address: this.getAddress() 58 | }, 59 | strict: true 60 | }; 61 | }; 62 | 63 | schema.getRequestApprovalKeyResponse = function () { 64 | return { 65 | type: 'object', 66 | props: { 67 | key: 'string', 68 | startedAt: 'number', 69 | clientIp: this.getClientIp(), 70 | approvers: this.getApprovers() 71 | }, 72 | strict: true 73 | }; 74 | }; 75 | 76 | schema.getStructureResponse = function () { 77 | const address = this.getAddress(); 78 | return { 79 | type: 'object', 80 | props: { 81 | address, 82 | masters: { 83 | type: 'array', 84 | items: { 85 | type: 'object', 86 | uniq: 'address', 87 | props: { 88 | address, 89 | size: 'number' 90 | }, 91 | strict: true 92 | } 93 | }, 94 | slaves: { 95 | type: 'array', 96 | uniq: 'address', 97 | items: { 98 | type: 'object', 99 | props: { 100 | address 101 | }, 102 | strict: true 103 | } 104 | }, 105 | backlink: { 106 | type: 'object', 107 | props: { 108 | address 109 | }, 110 | canBeNull: true, 111 | strict: true 112 | } 113 | }, 114 | strict: true 115 | }; 116 | }; 117 | 118 | schema.getProvideRegistrationResponse = function () { 119 | const address = this.getAddress(); 120 | return { 121 | type: 'object', 122 | props: { 123 | address, 124 | networkSize: 'number', 125 | syncLifetime: 'number', 126 | results: { 127 | type: 'array', 128 | items: { 129 | type: 'object', 130 | props: { 131 | networkSize: 'number', 132 | address, 133 | candidates: { 134 | type: 'array', 135 | uniq: 'address', 136 | items: { 137 | type: 'object', 138 | props: { 139 | address 140 | }, 141 | strict: true 142 | } 143 | } 144 | }, 145 | strict: true 146 | } 147 | } 148 | }, 149 | strict: true 150 | }; 151 | }; 152 | 153 | schema.getRegisterResponse = function () { 154 | const address = this.getAddress(); 155 | return { 156 | type: 'object', 157 | props: { 158 | address, 159 | size: 'number' 160 | }, 161 | strict: true 162 | }; 163 | }; 164 | 165 | schema.getInterviewSummaryResponse = function () { 166 | const address = this.getAddress(); 167 | return { 168 | type: 'object', 169 | props: { 170 | address, 171 | summary: { 172 | type: 'object', 173 | props: { 174 | address 175 | }, 176 | strict: true 177 | } 178 | }, 179 | strict: true 180 | }; 181 | }; 182 | 183 | schema.getApprovalApproverInfoResponse = function (infoSchema) { 184 | const address = this.getAddress(); 185 | return { 186 | type: 'object', 187 | props: { 188 | address, 189 | info: infoSchema 190 | }, 191 | strict: true 192 | }; 193 | }; 194 | 195 | schema.getApprovalInfoRequest = function (answerSchema) { 196 | return { 197 | type: 'object', 198 | props: { 199 | action: 'string', 200 | key: 'string', 201 | startedAt: 'number', 202 | clientIp: this.getClientIp(), 203 | approvers: this.getApprovers(), 204 | answer: answerSchema 205 | }, 206 | strict: true 207 | }; 208 | }; 209 | 210 | export default schema; 211 | -------------------------------------------------------------------------------- /src/server/transports/express/api/butler/controllers.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/server/transports/express/api/butler/routes.js: -------------------------------------------------------------------------------- 1 | export default []; 2 | -------------------------------------------------------------------------------- /src/server/transports/express/api/master/controllers.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/server/transports/express/api/master/routes.js: -------------------------------------------------------------------------------- 1 | export default []; 2 | -------------------------------------------------------------------------------- /src/server/transports/express/api/node/controllers.js: -------------------------------------------------------------------------------- 1 | import errors from "../../../../../errors.js"; 2 | import utils from "../../../../../utils.js"; 3 | import schema from "../../../../../schema.js"; 4 | import { pick } from "lodash-es"; 5 | 6 | export const register = node => { 7 | return async (req, res, next) => { 8 | try { 9 | const target = req.body.target; 10 | 11 | if ( 12 | target == node.address || 13 | !utils.isValidAddress(target) || 14 | !utils.isIpEqual(await utils.getHostIp(utils.splitAddress(target)[0]), req.clientIp) 15 | ) { 16 | throw new errors.WorkError('"target" field is invalid', 'ERR_SPREADABLE_INVALID_TARGET_FIELD'); 17 | } 18 | 19 | let size = await node.db.getSlavesCount(); 20 | 21 | if (await node.db.hasSlave(target)) { 22 | return res.send({ size }); 23 | } 24 | 25 | const timer = node.createRequestTimer(node.createRequestTimeout(req.body)); 26 | let result; 27 | 28 | try { 29 | result = await node.requestNode(target, 'get-interview-summary', { 30 | timeout: timer(), 31 | responseSchema: schema.getInterviewSummaryResponse() 32 | }); 33 | } 34 | catch (err) { 35 | node.logger.warn(err.stack); 36 | throw new errors.WorkError('Interviewee unavailable', 'ERR_SPREADABLE_INTERVIEW_INTERVIEWEE_NOT_AVAILABLE'); 37 | } 38 | 39 | if (!result.summary || typeof result.summary != 'object') { 40 | throw new errors.WorkError('Interviewee sent invalid summary', 'ERR_SPREADABLE_INTERVIEW_INVALID_SUMMARY'); 41 | } 42 | 43 | await node.interview(result.summary); 44 | await node.db.addSlave(target); 45 | size++; 46 | res.send({ size }); 47 | } 48 | catch (err) { 49 | next(err); 50 | } 51 | }; 52 | }; 53 | 54 | export const structure = node => { 55 | return async (req, res, next) => { 56 | try { 57 | return res.send(await node.createStructure()); 58 | } 59 | catch (err) { 60 | next(err); 61 | } 62 | }; 63 | }; 64 | 65 | export const getInterviewSummary = node => { 66 | return async (req, res, next) => { 67 | try { 68 | return res.send({ summary: await node.getInterviewSummary() }); 69 | } 70 | catch (err) { 71 | next(err); 72 | } 73 | }; 74 | }; 75 | 76 | export const provideRegistration = node => { 77 | return async (req, res, next) => { 78 | try { 79 | const target = req.body.target; 80 | 81 | if (!utils.isValidAddress(target)) { 82 | throw new errors.WorkError('"target" field is invalid', 'ERR_SPREADABLE_INVALID_TARGET_FIELD'); 83 | } 84 | 85 | const timer = node.createRequestTimer(node.createRequestTimeout(req.body)); 86 | let masters = await node.db.getMasters(); 87 | !masters.length && (masters = [{ address: node.address }]); 88 | let results = await node.requestGroup(masters, 'structure', { timeout: timer() }); 89 | 90 | for (let i = results.length - 1; i >= 0; i--) { 91 | const result = results[i]; 92 | if (!result.slaves.length && result.address != node.address) { 93 | results.splice(i, 1); 94 | continue; 95 | } 96 | let info = {}; 97 | info.address = result.address; 98 | info.networkSize = await node.getNetworkSize(result.masters); 99 | info.candidates = result.slaves.length ? result.slaves.map(s => pick(s, ['address'])) : [{ address: node.address }]; 100 | results[i] = info; 101 | } 102 | 103 | const syncLifetime = await node.getSyncLifetime(); 104 | const networkSize = await node.getNetworkSize(); 105 | res.send({ results, syncLifetime, networkSize }); 106 | } 107 | catch (err) { 108 | next(err); 109 | } 110 | }; 111 | }; 112 | 113 | export const getApprovalInfo = node => { 114 | return async (req, res, next) => { 115 | try { 116 | const action = req.body.action; 117 | const key = req.body.key; 118 | await node.approvalActionTest(action); 119 | const approval = await node.getApproval(action); 120 | const approver = await node.db.getApproval(key); 121 | 122 | if (!approver) { 123 | throw new errors.WorkError(`Unsuitable approval key "${key}"`, 'ERR_SPREADABLE_UNSUITABLE_APPROVAL_KEY'); 124 | } 125 | 126 | const result = await approval.createInfo(approver); 127 | await node.db.startApproval(key, result.answer); 128 | res.send({ info: result.info }); 129 | } 130 | catch (err) { 131 | next(err); 132 | } 133 | }; 134 | }; 135 | 136 | export const checkApprovalAnswer = node => { 137 | return async (req, res, next) => { 138 | try { 139 | const answer = req.body.answer; 140 | const key = req.body.key; 141 | const clientIp = req.body.clientIp; 142 | const approvers = req.body.approvers; 143 | const approver = await node.db.getApproval(key); 144 | 145 | if (!approver || approver.usedBy.includes(req.clientAddress) || !utils.isIpEqual(approver.clientIp, clientIp)) { 146 | throw new errors.WorkError(`Unsuitable approval key "${key}"`, 'ERR_SPREADABLE_UNSUITABLE_APPROVAL_KEY'); 147 | } 148 | 149 | const approval = await node.getApproval(approver.action); 150 | const result = await approval.checkAnswer(approver, answer, approvers); 151 | await node.db.useApproval(key, req.clientAddress); 152 | 153 | if (!result) { 154 | throw new errors.WorkError(`Wrong approval answer`, 'ERR_SPREADABLE_WRONG_APPROVAL_ANSWER'); 155 | } 156 | 157 | res.send({ success: true }); 158 | } 159 | catch (err) { 160 | next(err); 161 | } 162 | }; 163 | }; 164 | -------------------------------------------------------------------------------- /src/server/transports/express/api/node/routes.js: -------------------------------------------------------------------------------- 1 | import * as controllers from "./controllers.js"; 2 | 3 | export default [ 4 | /** 5 | * Register the address 6 | * 7 | * @api {post} /api/node/register 8 | * @apiParam {string} target 9 | * @apiSuccess {object} - { size, chain: [...] } 10 | */ 11 | { 12 | name: 'register', 13 | method: 'post', 14 | url: '/register', 15 | fn: controllers.register 16 | }, 17 | 18 | /** 19 | * Get the node structure 20 | * 21 | * @api {post} /api/node/structure 22 | * @apiSuccess {object} - { slaves: [...], masters: [...], backlink: {...} } 23 | */ 24 | { 25 | name: 'structure', 26 | method: 'post', 27 | url: '/structure', 28 | fn: controllers.structure 29 | }, 30 | 31 | /** 32 | * Get an interview summary 33 | * 34 | * @api {post} /api/node/get-interview-summary 35 | * @apiSuccess {object} - { summary: {...} } 36 | */ 37 | { 38 | name: 'getInterviewSummary', 39 | method: 'post', 40 | url: '/get-interview-summary', 41 | fn: controllers.getInterviewSummary 42 | }, 43 | 44 | /** 45 | * Provide the registartion 46 | * 47 | * @api {post} /api/node/provide-registration 48 | * @apiParam {string} target 49 | * @apiSuccess {object} - { results: [...] } 50 | */ 51 | { 52 | name: 'provideRegistration', 53 | method: 'post', 54 | url: '/provide-registration', 55 | fn: controllers.provideRegistration 56 | }, 57 | 58 | /** 59 | * Get the approval info 60 | * 61 | * @api {post} /api/node/get-approval-info 62 | * @apiParam {string} action 63 | * @apiParam {string} key 64 | * @apiSuccess {object} - { info } 65 | */ 66 | { 67 | name: 'getApprovalInfo', 68 | method: 'post', 69 | url: '/get-approval-info', 70 | fn: controllers.getApprovalInfo 71 | }, 72 | 73 | /** 74 | * Check the approval answer 75 | * 76 | * @api {post} /api/node/check-approval-answer 77 | * @apiParam {string} answer 78 | * @apiParam {string} key 79 | * @apiParam {string} clientIp 80 | * @apiParam {string[]} approvers 81 | * @apiSuccess {object} - { success } 82 | */ 83 | { 84 | name: 'checkApprovalAnswer', 85 | method: 'post', 86 | url: '/check-approval-answer', 87 | fn: controllers.checkApprovalAnswer 88 | } 89 | ]; 90 | -------------------------------------------------------------------------------- /src/server/transports/express/api/routes.js: -------------------------------------------------------------------------------- 1 | import midds from "../midds.js"; 2 | 3 | export default [ 4 | { name: 'networkAccess', fn: node => midds.networkAccess(node, { address: true, version: true, root: true }) }, 5 | { name: 'updateClientInfo', fn: midds.updateClientInfo }, 6 | { name: 'master', url: '/master', fn: node => node.server.createRouter(node.server.getApiMasterRoutes()) }, 7 | { name: 'butler', url: '/butler', fn: node => node.server.createRouter(node.server.getApiButlerRoutes()) }, 8 | { name: 'slave', url: '/slave', fn: node => node.server.createRouter(node.server.getApiSlaveRoutes()) }, 9 | { name: 'node', url: '/node', fn: node => node.server.createRouter(node.server.getApiNodeRoutes()) } 10 | ]; 11 | -------------------------------------------------------------------------------- /src/server/transports/express/api/slave/controllers.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/server/transports/express/api/slave/routes.js: -------------------------------------------------------------------------------- 1 | export default []; 2 | -------------------------------------------------------------------------------- /src/server/transports/express/client/controllers.js: -------------------------------------------------------------------------------- 1 | export const getAvailableNode = node => { 2 | return async (req, res, next) => { 3 | try { 4 | const address = await node.getAvailableNode(node.prepareClientMessageOptions(req.body)); 5 | res.send({ address }); 6 | } 7 | catch (err) { 8 | next(err); 9 | } 10 | }; 11 | }; 12 | 13 | export const requestApprovalKey = node => { 14 | return async (req, res, next) => { 15 | try { 16 | const action = req.body.action; 17 | const options = node.prepareClientMessageOptions(req.body); 18 | const result = await node.requestApprovalKey(action, req.clientIp, options); 19 | res.send(result); 20 | } 21 | catch (err) { 22 | next(err); 23 | } 24 | }; 25 | }; 26 | 27 | export const requestApprovalQuestion = node => { 28 | return async (req, res, next) => { 29 | try { 30 | const action = req.body.action; 31 | const key = req.body.key; 32 | const info = req.body.info; 33 | const confirmedAddresses = req.body.confirmedAddresses; 34 | const options = node.prepareClientMessageOptions(req.body); 35 | const question = await node.requestApprovalQuestion(action, req.clientIp, key, info, confirmedAddresses, options); 36 | res.send({ question }); 37 | } 38 | catch (err) { 39 | next(err); 40 | } 41 | }; 42 | }; 43 | 44 | export const addApprovalInfo = node => { 45 | return async (req, res, next) => { 46 | try { 47 | const action = req.body.action; 48 | const key = req.body.key; 49 | const info = req.body.info; 50 | const startedAt = req.body.startedAt; 51 | await node.addApprovalInfo(action, req.clientIp, key, startedAt, info); 52 | res.send({ success: true }); 53 | } 54 | catch (err) { 55 | next(err); 56 | } 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /src/server/transports/express/client/routes.js: -------------------------------------------------------------------------------- 1 | import * as controllers from "./controllers.js"; 2 | import midds from "../midds.js"; 3 | 4 | export default [ 5 | { name: 'networkAccess', fn: node => midds.networkAccess(node, { version: true }) }, 6 | 7 | /** 8 | * Get the available node from the network 9 | * 10 | * @api {post} /client/get-available-node 11 | * @apiSuccess {object} { address } 12 | */ 13 | { 14 | name: 'getAvailableNode', 15 | method: 'post', 16 | url: '/get-available-node', 17 | fn: [ 18 | midds.requestQueueClient, 19 | controllers.getAvailableNode 20 | ] 21 | }, 22 | 23 | /** 24 | * Request the approval key 25 | * 26 | * @api {post} /client/request-approval-key 27 | * @apiParam {string} action 28 | * @apiSuccess {object} { key, startedAt, approvers, clientIp } 29 | */ 30 | { 31 | name: 'requestApprovalKey', 32 | method: 'post', 33 | url: '/request-approval-key', 34 | fn: [ 35 | midds.requestQueueClient, 36 | controllers.requestApprovalKey 37 | ] 38 | }, 39 | 40 | /** 41 | * Request the approval question 42 | * 43 | * @api {post} /client/request-approval-question 44 | * @apiParam {string} action 45 | * @apiParam {string} key 46 | * @apiParam {string[]} confirmedAddresses 47 | * @apiSuccess {object} { question } 48 | */ 49 | { 50 | name: 'requestApprovalQuestion', 51 | method: 'post', 52 | url: '/request-approval-question', 53 | fn: [ 54 | midds.requestQueueClient, 55 | controllers.requestApprovalQuestion 56 | ] 57 | }, 58 | 59 | /** 60 | * Add the approval info 61 | * 62 | * @api {post} /client/add-approval-info 63 | * @apiParam {string} action 64 | * @apiParam {string} key 65 | * @apiParam {object} info 66 | * @apiParam {number} startedAt 67 | * @apiSuccess {object} - { info } 68 | */ 69 | { 70 | name: 'addApprovalInfo', 71 | method: 'post', 72 | url: '/add-approval-info', 73 | fn: controllers.addApprovalInfo 74 | }, 75 | ]; 76 | -------------------------------------------------------------------------------- /src/server/transports/express/controllers.js: -------------------------------------------------------------------------------- 1 | import compression from "compression"; 2 | import express from "express"; 3 | import cors from "cors"; 4 | import errors from "../../../errors.js"; 5 | import utils from "../../../utils.js"; 6 | 7 | const compressionFn = (node) => compression({ level: node.options.server.compressionLevel }); 8 | const corsFn = () => cors(); 9 | 10 | export const clientInfo = (node) => { 11 | return (req, res, next) => { 12 | const trusted = [...new Set(['127.0.0.1', node.ip, node.externalIp, node.localIp])]; 13 | req.clientIp = utils.getRemoteIp(req, { trusted }); 14 | 15 | if (!req.clientIp) { 16 | return next(new Error(`Client ip address can't be specified`)); 17 | } 18 | 19 | req.clientAddress = req.headers['original-address'] || `${req.clientIp}:0`; 20 | next(); 21 | }; 22 | }; 23 | 24 | export const timeout = () => { 25 | return (req, res, next) => (req.setTimeout(0), next()); 26 | }; 27 | 28 | export const bodyParser = node => { 29 | return [ 30 | express.urlencoded({ extended: false, limit: node.options.server.maxBodySize }), 31 | express.json({ limit: node.options.server.maxBodySize }) 32 | ]; 33 | }; 34 | 35 | export const ping = node => { 36 | return (req, res) => res.send({ 37 | version: node.getVersion(), 38 | address: node.address 39 | }); 40 | }; 41 | 42 | export const status = node => { 43 | return async (req, res, next) => { 44 | try { 45 | res.send(await node.getStatusInfo(req.query.pretty !== undefined)); 46 | } 47 | catch (err) { 48 | next(err); 49 | } 50 | }; 51 | }; 52 | 53 | export const indexPage = () => { 54 | return (req, res) => res.send({ success: true }); 55 | }; 56 | 57 | export const notFound = () => { 58 | return (req, res, next) => next(new errors.NotFoundError(`Not found route "${req.originalUrl}"`)); 59 | }; 60 | 61 | export const handleErrors = node => { 62 | // eslint-disable-next-line no-unused-vars 63 | return (err, req, res, next) => { 64 | if (err instanceof errors.WorkError) { 65 | res.status(422); 66 | return res.send({ message: err.message, code: err.code || 'ERR_SPREADABLE_API' }); 67 | } 68 | 69 | if (err.statusCode) { 70 | res.status(err.statusCode); 71 | return res.send({ message: err.message }); 72 | } 73 | 74 | node.logger.error(err.stack); 75 | res.status(500); 76 | res.send({ message: err.message }); 77 | }; 78 | }; 79 | 80 | export { compressionFn as compression }; 81 | export { corsFn as cors }; 82 | -------------------------------------------------------------------------------- /src/server/transports/express/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import server from "../server/index.js"; 3 | import routes from "./routes.js"; 4 | import routesClient from "./client/routes.js"; 5 | import routesApi from "./api/routes.js"; 6 | import routesApiMaster from "./api/master/routes.js"; 7 | import routesApiButler from "./api/butler/routes.js"; 8 | import routesApiSlave from "./api/slave/routes.js"; 9 | import routesApiNode from "./api/node/routes.js"; 10 | 11 | const Server = server(); 12 | 13 | export default (Parent) => { 14 | return class ServerExpress extends (Parent || Server) { 15 | /** 16 | * @see Server.prototype.init 17 | */ 18 | async init() { 19 | this.app = express(); 20 | this.app.use(this.createRouter(this.getMainRoutes())); 21 | await super.init.apply(this, arguments); 22 | } 23 | 24 | getServerHandler() { 25 | return this.app; 26 | } 27 | 28 | /** 29 | * Get the main routes 30 | * 31 | * @returns {array} 32 | */ 33 | getMainRoutes() { 34 | return [...routes]; 35 | } 36 | 37 | /** 38 | * Get the client routes 39 | * 40 | * @returns {array} 41 | */ 42 | getClientRoutes() { 43 | return [...routesClient]; 44 | } 45 | 46 | /** 47 | * Get the api routes 48 | * 49 | * @returns {array} 50 | */ 51 | getApiRoutes() { 52 | return [...routesApi]; 53 | } 54 | 55 | /** 56 | * Get the api master routes 57 | * 58 | * @returns {array} 59 | */ 60 | getApiMasterRoutes() { 61 | return [...routesApiMaster]; 62 | } 63 | 64 | /** 65 | * Get the api butler routes 66 | * 67 | * @returns {array} 68 | */ 69 | getApiButlerRoutes() { 70 | return [...routesApiButler]; 71 | } 72 | 73 | /** 74 | * Get the api slave routes 75 | * 76 | * @returns {array} 77 | */ 78 | getApiSlaveRoutes() { 79 | return [...routesApiSlave]; 80 | } 81 | 82 | /** 83 | * Get the api node routes 84 | * 85 | * @returns {array} 86 | */ 87 | getApiNodeRoutes() { 88 | return [...routesApiNode]; 89 | } 90 | 91 | /** 92 | * Create a router 93 | * 94 | * @param {fn[]} routes 95 | * @returns {express.Router} 96 | */ 97 | createRouter(routes) { 98 | const router = express.Router(); 99 | routes.forEach(route => { 100 | const fn = Array.isArray(route.fn) ? route.fn.map(fn => fn(this.node)) : route.fn(this.node); 101 | const rfn = router[route.method || 'use'].bind(router); 102 | route.url ? rfn(route.url, fn) : rfn(fn); 103 | }); 104 | return router; 105 | } 106 | }; 107 | }; 108 | -------------------------------------------------------------------------------- /src/server/transports/express/midds.js: -------------------------------------------------------------------------------- 1 | import errors from "../../../errors.js"; 2 | import utils from "../../../utils.js"; 3 | import schema from "../../../schema.js"; 4 | import { merge, intersection } from "lodash-es"; 5 | import crypto from "crypto"; 6 | import Cookies from "cookies"; 7 | import basicAuth from "basic-auth"; 8 | 9 | const midds = {}; 10 | 11 | /** 12 | * Handle the approval request 13 | */ 14 | midds.approval = node => { 15 | return async (req, res, next) => { 16 | try { 17 | const invErrCode = 'ERR_SPREADABLE_INVALID_APPROVAL_INFO'; 18 | const timeout = node.createRequestTimeout(req.body); 19 | const timer = node.createRequestTimer(timeout); 20 | let info = req.body.approvalInfo || req.query.approvalInfo; 21 | 22 | if (!info) { 23 | throw new errors.WorkError(`Request to "${req.originalUrl}" requires confirmation`, 'ERR_SPREADABLE_APPROVAL_INFO_REQUIRED'); 24 | } 25 | 26 | try { 27 | typeof info == 'string' && (info = JSON.parse(info)); 28 | } 29 | catch (err) { 30 | throw new errors.WorkError(err.message, invErrCode); 31 | } 32 | 33 | const action = info.action; 34 | const startedAt = info.startedAt; 35 | const key = info.key; 36 | const clientApprovers = info.approvers; 37 | const clientIp = info.clientIp; 38 | const answer = info.answer; 39 | await node.approvalActionTest(action); 40 | const approval = await node.getApproval(action); 41 | await approval.startTimeTest(startedAt); 42 | const answerSchema = approval.getClientAnswerSchema(); 43 | 44 | try { 45 | utils.validateSchema(schema.getApprovalInfoRequest(answerSchema), info); 46 | } 47 | catch (err) { 48 | throw new errors.WorkError(err.message, invErrCode); 49 | } 50 | 51 | const time = utils.getClosestPeriodTime(startedAt, approval.period); 52 | const approversCount = await approval.calculateApproversCount(); 53 | let approvers = await node.getApprovalApprovers(time, approversCount, { timeout: timer() }); 54 | const targets = intersection(clientApprovers, approvers).map(address => ({ address })); 55 | await approval.approversDecisionCountTest(targets.length); 56 | const results = await node.requestGroup(targets, 'check-approval-answer', { 57 | includeErrors: false, 58 | timeout: timer(await node.getRequestServerTimeout()), 59 | body: { key, answer, approvers: clientApprovers, clientIp } 60 | }); 61 | 62 | try { 63 | await approval.approversDecisionCountTest(results.length); 64 | } 65 | catch (err) { 66 | throw new errors.WorkError('Wrong answer, try again', 'ERR_SPREADABLE_WRONG_APPROVAL_ANSWER'); 67 | } 68 | 69 | req.approvalInfo = info; 70 | next(); 71 | } 72 | catch (err) { 73 | next(err); 74 | } 75 | }; 76 | }; 77 | 78 | /** 79 | * Update the client info 80 | */ 81 | midds.updateClientInfo = node => { 82 | return async (req, res, next) => { 83 | try { 84 | await node.db.successServerAddress(req.clientAddress); 85 | next(); 86 | } 87 | catch (err) { 88 | next(err); 89 | } 90 | }; 91 | }; 92 | 93 | /** 94 | * Control the current request client access 95 | */ 96 | midds.networkAccess = (node, checks = {}) => { 97 | checks = merge({ 98 | auth: true, 99 | root: false, 100 | address: false, 101 | version: false 102 | }, checks); 103 | return async (req, res, next) => { 104 | try { 105 | await node.addressFilter(req.clientAddress); 106 | await node.networkAccess(req); 107 | 108 | if (checks.address && 109 | (!utils.isValidAddress(req.clientAddress) || 110 | !utils.isIpEqual(await utils.getAddressIp(req.clientAddress), req.clientIp)) 111 | ) { 112 | throw new errors.AccessError(`Wrong address "${req.clientAddress}"`); 113 | } 114 | 115 | if (checks.auth && node.options.network.auth) { 116 | const auth = node.options.network.auth; 117 | const cookies = new Cookies(req, res); 118 | const cookieKey = `spreadableNetworkAuth[${node.address}]`; 119 | const cookieKeyValue = cookies.get(cookieKey); 120 | const info = basicAuth(req); 121 | const cookieInfo = cookieKeyValue ? JSON.parse(Buffer.from(cookieKeyValue, 'base64')) : null; 122 | const behaviorFail = await node.getBehavior('authentication'); 123 | 124 | if ( 125 | (!cookieInfo || cookieInfo.username != auth.username || cookieInfo.password != auth.password) && 126 | (!info || info.name != auth.username || info.pass != auth.password) 127 | ) { 128 | res.setHeader('WWW-Authenticate', `Basic realm="${node.address}"`); 129 | behaviorFail.add(req.clientAddress); 130 | throw new errors.AuthError('Authentication is required'); 131 | } 132 | 133 | behaviorFail.sub(req.clientAddress); 134 | 135 | if (!cookieKeyValue) { 136 | cookies.set(cookieKey, Buffer.from(JSON.stringify(auth)).toString('base64'), { 137 | maxAge: node.options.network.authCookieMaxAge, 138 | httpOnly: false 139 | }); 140 | } 141 | } 142 | 143 | if (checks.version) { 144 | const version = node.getVersion(); 145 | const current = req.headers['node-version'] || req.headers['client-version']; 146 | 147 | if (current !== undefined && current != version) { 148 | throw new errors.AccessError(`The version is different: "${current}" instead of "${version}"`); 149 | } 150 | } 151 | 152 | if (checks.root) { 153 | const root = node.getRoot(); 154 | if (req.headers['node-root'] != root) { 155 | throw new errors.AccessError(`Node root is different: "${req.headers['node-root']}" instead of "${root}"`); 156 | } 157 | } 158 | 159 | next(); 160 | } 161 | catch (err) { 162 | next(err); 163 | } 164 | }; 165 | }; 166 | 167 | /** 168 | * Control the client requests queue 169 | */ 170 | midds.requestQueueClient = (node, options = {}) => { 171 | options = merge({ 172 | limit: node.options.request.clientConcurrency 173 | }, options); 174 | return (req, res, next) => { 175 | const key = options.key || (req.method + req.originalUrl.split('?')[0]); 176 | delete options.key; 177 | return midds.requestQueue(node, key, options)(req, res, next); 178 | }; 179 | }; 180 | 181 | /** 182 | * Control parallel requests queue 183 | */ 184 | midds.requestQueue = (node, keys, options) => { 185 | options = merge({ 186 | limit: 1, 187 | fnHash: key => crypto.createHash('md5').update(key).digest("hex") 188 | }, options); 189 | return async (req, res, next) => { 190 | const createPromise = key => { 191 | return new Promise((resolve, reject) => { 192 | key = typeof key == 'function' ? key(req) : key; 193 | const hash = options.fnHash(key); 194 | const obj = node.__requestQueue; 195 | 196 | if (!hash) { 197 | throw new errors.WorkError('"hash" is invalid', 'ERR_SPREADABLE_INVALID_REQUEST_QUEUE_HASH'); 198 | } 199 | 200 | (!obj[hash] || !obj[hash].length) && (obj[hash] = []); 201 | const arr = obj[hash]; 202 | let finished = false; 203 | const finishFn = () => { 204 | try { 205 | if (finished) { 206 | return; 207 | } 208 | 209 | finished = true; 210 | req.removeListener('close', finishFn); 211 | res.removeListener('finish', finishFn); 212 | const index = arr.findIndex(it => it.req == req); 213 | 214 | if (index >= 0) { 215 | arr.splice(index, 1); 216 | arr[options.limit - 1] && arr[options.limit - 1].startFn(); 217 | } 218 | 219 | !arr.length && delete obj[hash]; 220 | } 221 | catch (err) { 222 | reject(err); 223 | } 224 | }; 225 | const startFn = resolve; 226 | req.on('close', finishFn); 227 | res.on('finish', finishFn); 228 | arr.push({ req, startFn }); 229 | arr.length <= options.limit && startFn(); 230 | }); 231 | }; 232 | try { 233 | !Array.isArray(keys) && (keys = [keys]); 234 | keys = [...new Set(keys)].filter(it => it); 235 | const promise = []; 236 | 237 | for (let i = 0; i < keys.length; i++) { 238 | promise.push(createPromise(keys[i])); 239 | } 240 | 241 | await Promise.all(promise); 242 | next(); 243 | } 244 | catch (err) { 245 | next(err); 246 | } 247 | }; 248 | }; 249 | 250 | export default midds; 251 | -------------------------------------------------------------------------------- /src/server/transports/express/routes.js: -------------------------------------------------------------------------------- 1 | import * as controllers from "./controllers.js"; 2 | import midds from "./midds.js"; 3 | 4 | export default [ 5 | { name: 'clientInfo', fn: controllers.clientInfo }, 6 | { name: 'timeout', fn: controllers.timeout }, 7 | { name: 'cors', fn: controllers.cors }, 8 | { name: 'compression', fn: controllers.compression }, 9 | { name: 'bodyParser', fn: controllers.bodyParser }, 10 | { name: 'api', url: '/api', fn: node => node.server.createRouter(node.server.getApiRoutes()) }, 11 | { name: 'client', url: '/client', fn: node => node.server.createRouter(node.server.getClientRoutes()) }, 12 | { name: 'ping', method: 'get', url: '/ping', fn: controllers.ping }, 13 | { name: 'status', method: 'get', url: '/status', fn: [midds.networkAccess, controllers.status] }, 14 | { name: 'indexPage', method: 'get', url: '*', fn: [midds.networkAccess, controllers.indexPage] }, 15 | { name: 'notFound', fn: controllers.notFound }, 16 | { name: 'handleErrors', fn: controllers.handleErrors } 17 | ]; 18 | -------------------------------------------------------------------------------- /src/server/transports/server/index.js: -------------------------------------------------------------------------------- 1 | import Service from "../../../service.js"; 2 | import utils from "../../../utils.js"; 3 | import https from "https"; 4 | import http from "http"; 5 | 6 | export default (Parent) => { 7 | /** 8 | * The main server transport 9 | */ 10 | return class Server extends (Parent || Service) { 11 | 12 | /** 13 | * @param {object} options 14 | * @param {object|boolean} [options.https] 15 | */ 16 | constructor(options = {}) { 17 | super(...arguments); 18 | this.options = options; 19 | } 20 | 21 | /** 22 | * Initialize the server 23 | * 24 | * @async 25 | */ 26 | async init() { 27 | this.port = this.node.port; 28 | 29 | if (await utils.isPortUsed(this.port)) { 30 | throw new Error(`Port ${this.port} is already used`); 31 | } 32 | 33 | await this.startServer(); 34 | await super.init.apply(this, arguments); 35 | } 36 | 37 | /** 38 | * Deinitialize the server 39 | * 40 | * @async 41 | */ 42 | async deinit() { 43 | await this.stopServer(); 44 | await super.deinit.apply(this, arguments); 45 | } 46 | 47 | /** 48 | * Run an http server 49 | * 50 | * @async 51 | */ 52 | async runHttpServer() { 53 | await new Promise((resolve, reject) => this.server = http.createServer(this.getServerHandler()).listen(this.port, err => { 54 | this.node.logger.info(`Node has been started on http://${this.node.address}`); 55 | err ? reject(err) : resolve(); 56 | })); 57 | } 58 | 59 | /** 60 | * Run an https server 61 | * 62 | * @async 63 | */ 64 | async runHttpsServer() { 65 | const options = this.options.https; 66 | const handler = this.getServerHandler(); 67 | await new Promise((resolve, reject) => this.server = https.createServer(options, handler).listen(this.port, err => { 68 | this.node.logger.info(`Node has been started on https://${this.node.address}`); 69 | err ? reject(err) : resolve(); 70 | })); 71 | } 72 | 73 | /** 74 | * Start the server 75 | * 76 | * @async 77 | */ 78 | async startServer() { 79 | await (typeof this.options.https == 'object' ? this.runHttpsServer() : this.runHttpServer()); 80 | } 81 | 82 | /** 83 | * Stop the server 84 | * 85 | * @async 86 | */ 87 | async stopServer() { 88 | this.server && await new Promise((resolve, reject) => this.server.close((err) => err ? reject(err) : resolve())); 89 | } 90 | 91 | getServerHandler() { 92 | return (req, res) => res.end('success'); 93 | } 94 | }; 95 | }; 96 | -------------------------------------------------------------------------------- /src/service.js: -------------------------------------------------------------------------------- 1 | export default class Service { 2 | constructor() { 3 | this.__services = []; 4 | } 5 | 6 | /** 7 | * Initialize the service 8 | * 9 | * @async 10 | */ 11 | async init() { 12 | if (!this.node && !this.__isMasterService) { 13 | throw new Error(`You have to register the service "${ this.constructor.name }" at first`); 14 | } 15 | 16 | await this.initServices(); 17 | this.__initialized = Date.now(); 18 | } 19 | 20 | /** 21 | * Deinitialize the service 22 | * 23 | * @async 24 | */ 25 | async deinit() { 26 | await this.deinitServices(); 27 | this.__initialized = false; 28 | } 29 | 30 | /** 31 | * Destroy the service 32 | * 33 | * @async 34 | */ 35 | async destroy() { 36 | await this.destroyServices(); 37 | await this.deinit(); 38 | } 39 | 40 | /** 41 | * Add the service 42 | * 43 | * @async 44 | * @param {string} name 45 | * @param {Service} service 46 | * @param {string} [type] 47 | * @returns {object} 48 | */ 49 | async addService(name, service, type) { 50 | const index = this.__services.findIndex(s => s.name === name && s.type === type); 51 | index != -1 && this.__services.splice(index, 1); 52 | this.__services.push({ service, name, type }); 53 | service.name = name; 54 | service.node = this; 55 | this.__initialized && !service.__initialized && await service.init(); 56 | return service; 57 | } 58 | 59 | /** 60 | * Get the service 61 | * 62 | * @async 63 | * @param {string} name 64 | * @param {string} [type] 65 | * @returns {object} 66 | */ 67 | async getService(name, type) { 68 | const res = this.__services.find(s => s.name === name && s.type == type); 69 | return res ? res.service : null; 70 | } 71 | 72 | /** 73 | * Remove the service 74 | * 75 | * @async 76 | * @param {string} name 77 | * @param {string} [type] 78 | */ 79 | async removeService(name, type) { 80 | const index = this.__services.findIndex(s => s.name === name && s.type == type); 81 | 82 | if (index == -1) { 83 | return; 84 | } 85 | 86 | const res = this.__services[index]; 87 | await res.service.destroy(); 88 | this.__services.splice(index, 1); 89 | } 90 | 91 | /** 92 | * Initialize the services 93 | * 94 | * @async 95 | */ 96 | async initServices() { 97 | for (let i = 0; i < this.__services.length; i++) { 98 | await this.__services[i].service.init(); 99 | } 100 | } 101 | 102 | /** 103 | * Deinitialize the services 104 | * 105 | * @async 106 | */ 107 | async deinitServices() { 108 | for (let i = this.__services.length - 1; i >= 0; i--) { 109 | await this.__services[i].service.deinit(); 110 | } 111 | } 112 | 113 | /** 114 | * Destroy the services 115 | * 116 | * @async 117 | */ 118 | async destroyServices() { 119 | for (let i = this.__services.length - 1; i >= 0; i--) { 120 | await this.__services[i].service.destroy(); 121 | } 122 | 123 | this.__services = []; 124 | } 125 | 126 | /** 127 | * Check the service is initialized 128 | * 129 | * @returns {boolean} 130 | */ 131 | isInitialized() { 132 | return !!this.__initialized; 133 | } 134 | 135 | /** 136 | * Get the service version 137 | * 138 | * @returns {string} 139 | */ 140 | getVersion() { 141 | return `${this.constructor.codename}-${this.constructor.version.split('.').slice(0, -1).join('.')}`; 142 | } 143 | } -------------------------------------------------------------------------------- /src/task/transports/cron/index.js: -------------------------------------------------------------------------------- 1 | import task from "../task/index.js"; 2 | import { CronJob } from "cron"; 3 | const Task = task(); 4 | 5 | export default (Parent) => { 6 | /** 7 | * Cron tasks transport 8 | */ 9 | return class TaskCron extends (Parent || Task) { 10 | 11 | /** 12 | * @see Task.prototype.start 13 | */ 14 | async start(task) { 15 | await super.start(task); 16 | task.cronTask = new CronJob(task.interval, () => this.run(task)); 17 | task.cronTask.start(); 18 | } 19 | 20 | /** 21 | * @see Task.prototype.stop 22 | */ 23 | async stop(task) { 24 | task.cronTask.stop(); 25 | await super.stop(task); 26 | } 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/task/transports/interval/index.js: -------------------------------------------------------------------------------- 1 | import task from "../task/index.js"; 2 | import utils from "../../../utils.js"; 3 | const Task = task(); 4 | 5 | export default (Parent) => { 6 | /** 7 | * Interval tasks transport 8 | */ 9 | return class TaskInterval extends (Parent || Task) { 10 | /** 11 | * @see Task.prototype.add 12 | */ 13 | async add(name, interval, fn, options) { 14 | return super.add(name, utils.getMs(interval), fn, options); 15 | } 16 | 17 | /** 18 | * @see Task.prototype.start 19 | */ 20 | async start(task) { 21 | await super.start(task); 22 | const obj = setInterval(() => this.run(task), task.interval); 23 | task.intervalObject = obj; 24 | } 25 | 26 | /** 27 | * @see Task.prototype.stop 28 | */ 29 | async stop(task) { 30 | clearInterval(task.intervalObject); 31 | await super.stop(task); 32 | } 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/task/transports/task/index.js: -------------------------------------------------------------------------------- 1 | import merge from "lodash-es/merge.js"; 2 | import Service from "../../../service.js"; 3 | 4 | export default (Parent) => { 5 | /** 6 | * Tasks transport 7 | */ 8 | return class Task extends (Parent || Service) { 9 | /** 10 | * @param {object} options 11 | */ 12 | constructor(options = {}) { 13 | super(...arguments); 14 | this.options = merge({ 15 | showCompletionLogs: true, 16 | showFailLogs: true 17 | }, options); 18 | this.tasks = {}; 19 | } 20 | 21 | /** 22 | * Add the task 23 | */ 24 | async add(name, interval, fn, options) { 25 | const task = merge({ 26 | interval, 27 | fn, 28 | name, 29 | }, options); 30 | task.isStopped === undefined && (task.isStopped = true); 31 | this.tasks[name] = task; 32 | 33 | if (!task.isStopped) { 34 | await this.stop(task); 35 | await this.start(task); 36 | } 37 | 38 | return task; 39 | } 40 | 41 | /** 42 | * Get the task 43 | * 44 | * @returns {object} 45 | */ 46 | async get(name) { 47 | return this.tasks[name] || null; 48 | } 49 | 50 | /** 51 | * Remove the task 52 | */ 53 | async remove(name) { 54 | const task = this.tasks[name]; 55 | 56 | if (!task) { 57 | return; 58 | } 59 | 60 | !task.isStopped && await this.stop(task); 61 | delete this.tasks[name]; 62 | } 63 | 64 | /** 65 | * Initialize the tasks 66 | * 67 | * @async 68 | */ 69 | async init() { 70 | this.startAll(); 71 | await super.init.apply(this, arguments); 72 | } 73 | 74 | /** 75 | * Deinitialize the tasks 76 | * 77 | * @async 78 | */ 79 | async deinit() { 80 | this.stopAll(); 81 | await super.deinit.apply(this, arguments); 82 | } 83 | 84 | /** 85 | * Start all tasks 86 | */ 87 | async startAll() { 88 | for (let key in this.tasks) { 89 | await this.start(this.tasks[key]); 90 | } 91 | } 92 | 93 | /** 94 | * Stop all tasks 95 | * 96 | * @async 97 | */ 98 | async stopAll() { 99 | for (let key in this.tasks) { 100 | await this.stop(this.tasks[key]); 101 | } 102 | } 103 | 104 | /** 105 | * Run the task callback 106 | * 107 | * @async 108 | * @param {object} task 109 | * @param {number} task.interval 110 | * @param {function} task.fn 111 | */ 112 | async run(task) { 113 | if (task.isStopped) { 114 | this.options.showFailLogs && this.node.logger.warn(`Task "${task.name}" should be started at first`); 115 | return; 116 | } 117 | 118 | if (task.isRun) { 119 | this.options.showFailLogs && this.node.logger.warn(`Task "${task.name}" has blocking operations`); 120 | return; 121 | } 122 | 123 | task.isRun = true; 124 | 125 | try { 126 | await task.fn(); 127 | this.options.showCompletionLogs && this.node.logger.info(`Task "${task.name}" has been completed`); 128 | } 129 | catch (err) { 130 | this.options.showFailLogs && this.node.logger.error(`Task "${task.name}", ${err.stack}`); 131 | } 132 | 133 | task.isRun = false; 134 | } 135 | 136 | /** 137 | * Start the task 138 | * 139 | * @async 140 | * @param {object} task 141 | * @param {number} task.interval 142 | * @param {function} task.fn 143 | */ 144 | async start(task) { 145 | task.isStopped = false; 146 | } 147 | 148 | /** 149 | * Stop the task 150 | * 151 | * @async 152 | * @param {object} task 153 | */ 154 | async stop(task) { 155 | task.isStopped = true; 156 | } 157 | }; 158 | }; 159 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } -------------------------------------------------------------------------------- /test/approval/approval.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import approval from "../../src/approval/transports/approval/index.js"; 3 | 4 | const Approval = approval(); 5 | 6 | export default function () { 7 | describe('Approval', () => { 8 | let approval; 9 | 10 | describe('instance creation', function () { 11 | it('should create an instance', function () { 12 | assert.doesNotThrow(() => approval = new Approval()); 13 | approval.node = this.node; 14 | }); 15 | 16 | it('should create the default properties', function () { 17 | assert.containsAllKeys(approval, ['approversCount', 'decisionLevel', 'period']); 18 | }); 19 | }); 20 | 21 | describe('.init()', function () { 22 | it('should not throw an exception', async function () { 23 | await approval.init(); 24 | }); 25 | }); 26 | 27 | describe('.deinit()', function () { 28 | it('should not throw an exception', async function () { 29 | await approval.deinit(); 30 | }); 31 | }); 32 | 33 | describe('reinitialization', () => { 34 | it('should not throw an exception', async function () { 35 | await approval.init(); 36 | }); 37 | }); 38 | 39 | describe('.destroy()', function () { 40 | it('should not throw an exception', async function () { 41 | await approval.destroy(); 42 | }); 43 | }); 44 | }); 45 | } -------------------------------------------------------------------------------- /test/approval/captcha.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import sharp from "sharp"; 3 | import isPng from "is-png"; 4 | import captcha from "../../src/approval/transports/captcha/index.js"; 5 | import utils from "../../src/utils.js"; 6 | 7 | const ApprovalCaptcha = captcha(); 8 | 9 | export default function () { 10 | describe("ApprovalCaptcha", () => { 11 | let approval; 12 | 13 | describe("instance creation", function () { 14 | it("should create an instance", function () { 15 | assert.doesNotThrow(() => (approval = new ApprovalCaptcha())); 16 | approval.node = this.node; 17 | }); 18 | 19 | it("should create the default properties", function () { 20 | assert.containsAllKeys(approval, [ 21 | "captchaLength", 22 | "captchaWidth", 23 | "captchaBackground", 24 | "captchaColor", 25 | ]); 26 | }); 27 | }); 28 | 29 | describe(".init()", function () { 30 | it("should not throw an exception", async function () { 31 | await approval.init(); 32 | }); 33 | }); 34 | 35 | describe(".createText()", function () { 36 | it("should return a random text", function () { 37 | const text = approval.createText(); 38 | assert.isOk( 39 | typeof text == "string" && text.length == approval.captchaLength 40 | ); 41 | }); 42 | }); 43 | 44 | describe(".createImage()", function () { 45 | it("should return the right image", async function () { 46 | const options = { 47 | captchaWidth: 200, 48 | captchaBackground: "transparent", 49 | captchaColor: "#000000", 50 | }; 51 | const text = approval.createText(); 52 | const img = await approval.createImage(text, options); 53 | const metadata = await img.metadata(); 54 | assert.equal(metadata.width, options.captchaWidth); 55 | }); 56 | }); 57 | 58 | describe(".createInfo()", function () { 59 | it("should return the right info", async function () { 60 | const result = await approval.createInfo({ info: {} }); 61 | const text = result.answer; 62 | assert.doesNotThrow( 63 | () => isPng(Buffer.from(result.info, "base64")), 64 | "check the info" 65 | ); 66 | assert.isOk( 67 | typeof text == "string" && text.length == approval.captchaLength, 68 | "check the answer" 69 | ); 70 | }); 71 | }); 72 | 73 | describe(".createQuestion()", function () { 74 | it("should return the right question", async function () { 75 | const data = []; 76 | 77 | for (let i = 0; i < 3; i++) { 78 | const res = await approval.createInfo({ info: {} }); 79 | data.push(res.info); 80 | } 81 | 82 | const result = await approval.createQuestion(data, {}); 83 | const buffer = Buffer.from(result.split(",")[1], "base64"); 84 | assert.isTrue(isPng(buffer)); 85 | }); 86 | }); 87 | 88 | describe(".checkAnswer()", function () { 89 | it("should return false", async function () { 90 | const approvers = ["localhost:1", "localhost:2", approval.node.address]; 91 | const result = await approval.checkAnswer( 92 | { answer: "34" }, 93 | "123456", 94 | approvers 95 | ); 96 | assert.isFalse(result); 97 | }); 98 | 99 | it("should return true", async function () { 100 | const approvers = ["localhost:1", approval.node.address, "localhost:2"]; 101 | const result = await approval.checkAnswer( 102 | { answer: "34" }, 103 | "123456", 104 | approvers 105 | ); 106 | assert.isTrue(result); 107 | }); 108 | }); 109 | 110 | describe(".getClientInfoSchema()", function () { 111 | it("should throw an error", function () { 112 | const schema = approval.getClientInfoSchema(); 113 | assert.throws( 114 | () => utils.validateSchema(schema, { captchaLength: 1 }), 115 | "", 116 | "check the unexpected value" 117 | ); 118 | assert.throws( 119 | () => utils.validateSchema(schema, { captchaWidth: 90 }), 120 | "", 121 | 'check "captchaWidth" min' 122 | ); 123 | assert.throws( 124 | () => utils.validateSchema(schema, { captchaWidth: 1000 }), 125 | "", 126 | 'check "captchaWidth" max' 127 | ); 128 | assert.throws( 129 | () => utils.validateSchema(schema, { captchaBackground: "wrong" }), 130 | "", 131 | 'check "captchaBackground"' 132 | ); 133 | assert.throws( 134 | () => utils.validateSchema(schema, { captchaColor: "wrong" }), 135 | "", 136 | 'check "captchaColor"' 137 | ); 138 | }); 139 | 140 | it("should not throw an error", function () { 141 | const schema = approval.getClientInfoSchema(); 142 | assert.doesNotThrow( 143 | () => utils.validateSchema(schema, { captchaWidth: 200 }), 144 | 'check "captchaWidth"' 145 | ); 146 | assert.doesNotThrow( 147 | () => 148 | utils.validateSchema(schema, { captchaBackground: "transparent" }), 149 | 'check "captchaBackground" transparent' 150 | ); 151 | assert.doesNotThrow( 152 | () => utils.validateSchema(schema, { captchaBackground: "#000000" }), 153 | 'check "captchaBackground" color' 154 | ); 155 | assert.doesNotThrow( 156 | () => utils.validateSchema(schema, { captchaColor: "random" }), 157 | 'check "captchaColor" random' 158 | ); 159 | assert.doesNotThrow( 160 | () => utils.validateSchema(schema, { captchaColor: "#000000" }), 161 | 'check "captchaColor" color' 162 | ); 163 | }); 164 | }); 165 | 166 | describe(".getClientAnswerSchema()", function () { 167 | it("should throw an error", function () { 168 | const schema = approval.getClientAnswerSchema(); 169 | assert.throws(() => utils.validateSchema(schema, 1)); 170 | }); 171 | 172 | it("should not throw an error", function () { 173 | const schema = approval.getClientAnswerSchema(); 174 | assert.doesNotThrow(() => utils.validateSchema(schema, "right")); 175 | }); 176 | }); 177 | 178 | describe(".getApproverInfoSchema()", function () { 179 | it("should throw an error", async function () { 180 | const schema = approval.getApproverInfoSchema(); 181 | assert.throws(() => utils.validateSchema(schema, "wrong")); 182 | }); 183 | 184 | it("should not throw an error", async function () { 185 | const schema = approval.getApproverInfoSchema(); 186 | const img = sharp({ 187 | create: { 188 | width: 100, 189 | height: 10, 190 | channels: 4, 191 | background: "transparent", 192 | }, 193 | }); 194 | img.png(); 195 | const buffer = await img.toBuffer(); 196 | assert.doesNotThrow(() => 197 | utils.validateSchema(schema, buffer.toString("base64")) 198 | ); 199 | }); 200 | }); 201 | 202 | describe(".deinit()", function () { 203 | it("should not throw an exception", async function () { 204 | await approval.deinit(); 205 | }); 206 | }); 207 | 208 | describe("reinitialization", () => { 209 | it("should not throw an exception", async function () { 210 | await approval.init(); 211 | }); 212 | }); 213 | 214 | describe(".destroy()", function () { 215 | it("should not throw an exception", async function () { 216 | await approval.destroy(); 217 | }); 218 | }); 219 | }); 220 | } 221 | -------------------------------------------------------------------------------- /test/approval/client.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import client from "../../src/approval/transports/client/index.js"; 3 | import utils from "../../src/utils.js"; 4 | 5 | const ApprovalClient = client(); 6 | 7 | export default function () { 8 | describe('ApprovalClient', () => { 9 | let approval; 10 | 11 | describe('instance creation', function () { 12 | it('should create an instance', function () { 13 | assert.doesNotThrow(() => approval = new ApprovalClient()); 14 | approval.node = this.node; 15 | }); 16 | }); 17 | 18 | describe('.init()', function () { 19 | it('should not throw an exception', async function () { 20 | await approval.init(); 21 | }); 22 | }); 23 | 24 | describe('.createInfo()', function () { 25 | it('should return the right info', async function () { 26 | const clientIp = '127.0.0.1'; 27 | const result = await approval.createInfo({ clientIp }); 28 | assert.isOk(result.info == clientIp && result.answer == clientIp); 29 | }); 30 | }); 31 | 32 | describe('.createQuestion()', function () { 33 | it('should return the right question', async function () { 34 | const clientIp = '127.0.0.1'; 35 | const result = await approval.createQuestion([], undefined, clientIp); 36 | assert.equal(result, clientIp); 37 | }); 38 | }); 39 | 40 | describe('.checkAnswer()', function () { 41 | it('should return false', async function () { 42 | const clientIp = '127.0.0.1'; 43 | const result = await approval.checkAnswer({ answer: clientIp }, '1.1.1.1'); 44 | assert.isFalse(result); 45 | }); 46 | 47 | it('should return true', async function () { 48 | const clientIp = '127.0.0.1'; 49 | const result = await approval.checkAnswer({ answer: clientIp }, clientIp); 50 | assert.isTrue(result); 51 | }); 52 | }); 53 | describe('.getClientInfoSchema()', function () { 54 | it('should throw an error', function () { 55 | const schema = approval.getClientInfoSchema(); 56 | assert.throws(() => utils.validateSchema(schema, {})); 57 | }); 58 | 59 | it('should not throw an error', function () { 60 | const schema = approval.getClientInfoSchema(); 61 | assert.doesNotThrow(() => utils.validateSchema(schema, undefined)); 62 | }); 63 | }); 64 | 65 | describe('.getClientAnswerSchema()', function () { 66 | it('should throw an error', function () { 67 | const schema = approval.getClientAnswerSchema(); 68 | assert.throws(() => utils.validateSchema(schema, 'wrong')); 69 | }); 70 | 71 | it('should not throw an error', function () { 72 | const schema = approval.getClientAnswerSchema(); 73 | assert.doesNotThrow(() => utils.validateSchema(schema, '1.1.1.1')); 74 | }); 75 | }); 76 | 77 | describe('.getApproverInfoSchema()', function () { 78 | it('should throw an error', function () { 79 | const schema = approval.getApproverInfoSchema(); 80 | assert.throws(() => utils.validateSchema(schema, 'wrong')); 81 | }); 82 | 83 | it('should not throw an error', function () { 84 | const schema = approval.getApproverInfoSchema(); 85 | assert.doesNotThrow(() => utils.validateSchema(schema, '1.1.1.1')); 86 | }); 87 | }); 88 | 89 | describe('.deinit()', function () { 90 | it('should not throw an exception', async function () { 91 | await approval.deinit(); 92 | }); 93 | }); 94 | 95 | describe('reinitialization', () => { 96 | it('should not throw an exception', async function () { 97 | await approval.init(); 98 | }); 99 | }); 100 | 101 | describe('.destroy()', function () { 102 | it('should not throw an exception', async function () { 103 | await approval.destroy(); 104 | }); 105 | }); 106 | }); 107 | } -------------------------------------------------------------------------------- /test/behavior/behavior.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import behavior from "../../src/behavior/transports/behavior/index.js"; 3 | 4 | const Behavior = behavior(); 5 | 6 | export default function () { 7 | describe('Behavior', () => { 8 | let behavior; 9 | 10 | describe('instance creation', function () { 11 | it('should create an instance', function () { 12 | assert.doesNotThrow(() => behavior = new Behavior()); 13 | behavior.node = this.node; 14 | }); 15 | }); 16 | 17 | describe('.init()', function () { 18 | it('should not throw an exception', async function () { 19 | await behavior.init(); 20 | }); 21 | }); 22 | 23 | describe('.deinit()', function () { 24 | it('should not throw an exception', async function () { 25 | await behavior.deinit(); 26 | }); 27 | }); 28 | 29 | describe('reinitialization', () => { 30 | it('should not throw an exception', async function () { 31 | await behavior.init(); 32 | }); 33 | }); 34 | 35 | describe('.destroy()', function () { 36 | it('should not throw an exception', async function () { 37 | await behavior.destroy(); 38 | }); 39 | }); 40 | }); 41 | } -------------------------------------------------------------------------------- /test/behavior/fail.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import behaviorFail from "../../src/behavior/transports/fail/index.js"; 3 | 4 | const BehaviorFail = behaviorFail(); 5 | 6 | export default function () { 7 | describe('Behavior', () => { 8 | let behavior; 9 | 10 | describe('instance creation', function () { 11 | it('should create an instance', function () { 12 | assert.doesNotThrow(() => behavior = new BehaviorFail()); 13 | behavior.node = this.node; 14 | }); 15 | 16 | it('should create the default properties', function () { 17 | assert.containsAllKeys(behavior, ['ban', 'banLifetime', 'failSuspicionLevel']); 18 | }); 19 | }); 20 | 21 | describe('.init()', function () { 22 | it('should not throw an exception', async function () { 23 | await behavior.init(); 24 | }); 25 | }); 26 | 27 | describe('.deinit()', function () { 28 | it('should not throw an exception', async function () { 29 | await behavior.deinit(); 30 | }); 31 | }); 32 | 33 | describe('reinitialization', () => { 34 | it('should not throw an exception', async function () { 35 | await behavior.init(); 36 | }); 37 | }); 38 | 39 | describe('.destroy()', function () { 40 | it('should not throw an exception', async function () { 41 | await behavior.destroy(); 42 | }); 43 | }); 44 | }); 45 | } -------------------------------------------------------------------------------- /test/cache/cache.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import cache from "../../src/cache/transports/cache/index.js"; 3 | 4 | const Cache = cache(); 5 | 6 | export default function () { 7 | describe('Cache', () => { 8 | let cache; 9 | 10 | describe('instance creation', function () { 11 | it('should create an instance', function () { 12 | assert.doesNotThrow(() => cache = new Cache()); 13 | cache.node = this.node; 14 | cache.name = 'test'; 15 | }); 16 | }); 17 | 18 | describe('.init()', function () { 19 | it('should not throw an exception', async function () { 20 | await cache.init(); 21 | }); 22 | }); 23 | 24 | describe('.deinit()', function () { 25 | it('should not throw an exception', async function () { 26 | await cache.deinit(); 27 | }); 28 | }); 29 | 30 | describe('reinitialization', () => { 31 | it('should not throw an exception', async function () { 32 | await cache.init(); 33 | }); 34 | }); 35 | 36 | describe('.destroy()', function () { 37 | it('should not throw an exception', async function () { 38 | await cache.destroy(); 39 | }); 40 | }); 41 | }); 42 | } -------------------------------------------------------------------------------- /test/cache/database.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import database from "../../src/cache/transports/database/index.js"; 3 | 4 | const CacheDatabase = database(); 5 | 6 | export default function () { 7 | describe('CacheDatabase', () => { 8 | let cache; 9 | let type; 10 | 11 | before(function () { 12 | type = 'test'; 13 | }); 14 | 15 | describe('instance creation', function () { 16 | it('should create an instance', function () { 17 | assert.doesNotThrow(() => cache = new CacheDatabase()); 18 | cache.node = this.node; 19 | cache.name = 'test'; 20 | }); 21 | }); 22 | 23 | describe('.init()', function () { 24 | it('should not throw an exception', async function () { 25 | await cache.init(); 26 | }); 27 | }); 28 | 29 | describe('.set()', function () { 30 | it('should add the cache', async function () { 31 | const key = 'key1'; 32 | const value = 1; 33 | await cache.set(key, value); 34 | const res = await this.node.db.getCache(type, key); 35 | assert.equal(res.value, value); 36 | }); 37 | }); 38 | 39 | describe('.get()', function () { 40 | it('should get the cache', async function () { 41 | const res = await cache.get('key1'); 42 | assert.equal(res.value, 1, 'check the value'); 43 | }); 44 | }); 45 | 46 | describe('.remove()', function () { 47 | it('should remove the cache', async function () { 48 | await cache.remove('key1'); 49 | assert.isNull(await cache.get('key1')); 50 | }); 51 | }); 52 | 53 | describe('.flush()', function () { 54 | it('should remove the cache', async function () { 55 | const key = 'key'; 56 | await cache.set(key, 1); 57 | await cache.flush(); 58 | assert.isNull(await cache.get(key)); 59 | }); 60 | }); 61 | 62 | describe('.deinit()', function () { 63 | it('should not throw an exception', async function () { 64 | await cache.deinit(); 65 | }); 66 | }); 67 | 68 | describe('reinitialization', () => { 69 | it('should not throw an exception', async function () { 70 | await cache.init(); 71 | }); 72 | }); 73 | 74 | describe('.destroy()', function () { 75 | it('should not throw an exception', async function () { 76 | await cache.destroy(); 77 | }); 78 | }); 79 | }); 80 | } -------------------------------------------------------------------------------- /test/client.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import node from "../src/node.js"; 3 | import client from "../src/client.js"; 4 | import approvalClient from "../src/approval/transports/client/index.js"; 5 | import tools from "./tools.js"; 6 | 7 | const Node = node(); 8 | const Client = client(); 9 | const ApprovalClient = approvalClient(); 10 | 11 | export default function () { 12 | describe('Client', () => { 13 | let client; 14 | let node; 15 | 16 | before(async function () { 17 | node = new Node(await tools.createNodeOptions()); 18 | await node.addApproval('test', new ApprovalClient()); 19 | await node.init(); 20 | }); 21 | 22 | after(async function () { 23 | await node.deinit(); 24 | }); 25 | 26 | describe('instance creation', function () { 27 | it('should create an instance', async function () { 28 | const options = await tools.createClientOptions({ address: node.address }); 29 | assert.doesNotThrow(() => client = new Client(options)); 30 | }); 31 | }); 32 | 33 | describe('.init()', function () { 34 | it('should not throw an exception', async function () { 35 | await client.init(); 36 | }); 37 | 38 | it('should set the worker address', async function () { 39 | assert.equal(client.workerAddress, node.address); 40 | }); 41 | }); 42 | 43 | describe('.getApprovalQuestion()', () => { 44 | it('should return approval info', async () => { 45 | const info = await client.getApprovalQuestion('test'); 46 | assert.isDefined(info.question); 47 | }); 48 | }); 49 | 50 | describe('.deinit()', function () { 51 | it('should not throw an exception', async function () { 52 | await client.deinit(); 53 | }); 54 | }); 55 | }); 56 | } -------------------------------------------------------------------------------- /test/db/database.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import database from "../../src/db/transports/database/index.js"; 3 | 4 | const Database = database(); 5 | 6 | export default function () { 7 | describe('Database', () => { 8 | let db; 9 | 10 | describe('instance creation', function () { 11 | it('should create an instance', function () { 12 | assert.doesNotThrow(() => db = new Database()); 13 | db.node = this.node; 14 | }); 15 | }); 16 | 17 | describe('.init()', function () { 18 | it('should not throw an exception', async function () { 19 | await db.init(); 20 | }); 21 | }); 22 | 23 | describe('.deinit()', function () { 24 | it('should not throw an exception', async function () { 25 | await db.deinit(); 26 | }); 27 | }); 28 | 29 | describe('reinitialization', () => { 30 | it('should not throw an exception', async function () { 31 | await db.init(); 32 | }); 33 | }); 34 | 35 | describe('.destroy()', function () { 36 | it('should not throw an exception', async function () { 37 | await db.destroy(); 38 | }); 39 | }); 40 | }); 41 | } -------------------------------------------------------------------------------- /test/group.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import node from "../src/node.js"; 3 | import client from "../src/client.js"; 4 | import approvalClient from "../src/approval/transports/client/index.js"; 5 | import captcha from "../src/approval/transports/captcha/index.js"; 6 | import express from "../src/server/transports/express/index.js"; 7 | import midds from "../src/server/transports/express/midds.js"; 8 | import tools from "./tools.js"; 9 | 10 | const NodeBase = node(); 11 | const Client = client(); 12 | const ApprovalClient = approvalClient(); 13 | const ApprovalCaptcha = captcha(); 14 | const ServerExpress = express(); 15 | 16 | const routes = [ 17 | { 18 | name: "approvalClientTest", 19 | method: "post", 20 | url: "/approval-client-test", 21 | fn: (node) => [ 22 | midds.approval(node), 23 | (req, res) => res.send({ success: true }), 24 | ], 25 | }, 26 | { 27 | name: "approvalCaptchaTest", 28 | method: "post", 29 | url: "/approval-captcha-test", 30 | fn: (node) => [ 31 | midds.approval(node), 32 | (req, res) => res.send({ success: true }), 33 | ], 34 | }, 35 | ]; 36 | 37 | class ServerExpressTest extends ServerExpress { 38 | getMainRoutes() { 39 | const arr = super.getMainRoutes(); 40 | arr.splice( 41 | arr.findIndex((r) => r.name == "ping"), 42 | 0, 43 | ...routes.slice() 44 | ); 45 | return arr; 46 | } 47 | } 48 | 49 | class Node extends NodeBase { 50 | static get ServerTransport() { 51 | return ServerExpressTest; 52 | } 53 | } 54 | 55 | export default function () { 56 | describe("group communication", () => { 57 | let nodes; 58 | let client; 59 | 60 | before(async () => { 61 | nodes = []; 62 | nodes.push(new Node(await tools.createNodeOptions())); 63 | nodes.push( 64 | new Node( 65 | await tools.createNodeOptions({ 66 | initialNetworkAddress: [`localhost:${nodes[0].port}`], 67 | }) 68 | ) 69 | ); 70 | }); 71 | 72 | after(async () => { 73 | for (let i = 0; i < nodes.length; i++) { 74 | await nodes[i].deinit(); 75 | } 76 | }); 77 | 78 | it("should register two nodes", async () => { 79 | await nodes[0].init(); 80 | await nodes[0].sync(); 81 | await nodes[1].init(); 82 | await nodes[1].sync(); 83 | assert.isTrue( 84 | await nodes[0].db.hasSlave(nodes[1].address), 85 | "check the second node registration as slave" 86 | ); 87 | assert.ok( 88 | await nodes[1].db.getBacklink(nodes[0].address), 89 | "check the first node registration as backlink" 90 | ); 91 | assert.ok( 92 | await nodes[1].db.getMaster(nodes[0].address), 93 | "check the first node registration as master" 94 | ); 95 | }); 96 | 97 | it("should reregister node", async () => { 98 | nodes.push(new Node(await tools.createNodeOptions())); 99 | await nodes[2].init(); 100 | nodes[1].initialNetworkAddress = nodes[2].address; 101 | await tools.wait(await nodes[1].getSyncLifetime()); 102 | await nodes[0].sync(); 103 | await nodes[2].sync(); 104 | await nodes[1].sync(); 105 | assert.equal( 106 | (await nodes[1].db.getBacklink()).address, 107 | nodes[2].address, 108 | "check the new backlink" 109 | ); 110 | await nodes[0].sync(); 111 | assert.isFalse( 112 | await nodes[0].db.hasSlave(nodes[1].address), 113 | "check the slave is removed in the master" 114 | ); 115 | }); 116 | 117 | it("should add the third node to the network", async () => { 118 | nodes[2].initialNetworkAddress = nodes[0].address; 119 | await tools.wait(await nodes[1].getSyncLifetime()); 120 | await nodes[0].sync(); 121 | await nodes[2].sync(); 122 | await nodes[1].sync(); 123 | assert.equal( 124 | (await nodes[2].db.getBacklink()).address, 125 | nodes[0].address, 126 | "check the new backlink" 127 | ); 128 | assert.isTrue( 129 | await nodes[0].db.hasSlave(nodes[2].address), 130 | "check the new slave" 131 | ); 132 | }); 133 | 134 | it("should show the right network size", async () => { 135 | for (let i = 0; i < 2; i++) { 136 | const node = new Node( 137 | await tools.createNodeOptions({ 138 | initialNetworkAddress: nodes[i].address, 139 | }) 140 | ); 141 | nodes.push(node); 142 | await node.init(); 143 | } 144 | 145 | await tools.nodesSync(nodes, nodes.length * 3); 146 | 147 | for (let i = 0; i < nodes.length; i++) { 148 | assert.equal(await nodes[i].getNetworkSize(), nodes.length); 149 | } 150 | }); 151 | 152 | it("should remove the node from the network", async () => { 153 | await nodes[0].deinit(); 154 | nodes.shift(); 155 | 156 | for (let i = 0; i < nodes.length; i++) { 157 | nodes[i].initialNetworkAddress = nodes[0].address; 158 | } 159 | 160 | await tools.wait(await nodes[0].getSyncLifetime()); 161 | await tools.nodesSync(nodes, nodes.length * 3); 162 | await tools.wait(await nodes[0].getSyncLifetime()); 163 | await tools.nodesSync(nodes, nodes.length * 3); 164 | 165 | for (let i = 0; i < nodes.length; i++) { 166 | assert.equal(await nodes[i].getNetworkSize(), nodes.length); 167 | } 168 | }); 169 | 170 | it("should prepare node and client for requests", async () => { 171 | for (let i = 0; i < nodes.length; i++) { 172 | await nodes[i].addApproval("client", new ApprovalClient()); 173 | await nodes[i].addApproval("captcha", new ApprovalCaptcha()); 174 | await nodes[i].sync(); 175 | } 176 | 177 | client = new Client( await tools.createClientOptions({ address: nodes[0].address })); 178 | await client.init(); 179 | }); 180 | 181 | it("should not approve client requests", async () => { 182 | try { 183 | await nodes[0].requestServer(nodes[0].address, "approval-client-test"); 184 | throw new Error("fail"); 185 | } catch (err) { 186 | assert.isTrue(err.code == "ERR_SPREADABLE_APPROVAL_INFO_REQUIRED"); 187 | } 188 | }); 189 | 190 | it("should approve client requests", async () => { 191 | const approvalInfo = await client.getApprovalQuestion("client"); 192 | approvalInfo.answer = approvalInfo.question; 193 | delete approvalInfo.question; 194 | const result = await nodes[0].requestServer( 195 | nodes[0].address, 196 | "approval-client-test", 197 | { body: { approvalInfo } } 198 | ); 199 | assert.isTrue(result.success); 200 | }); 201 | 202 | it("should not approve captcha requests", async () => { 203 | try { 204 | await nodes[0].requestServer(nodes[0].address, "approval-captcha-test"); 205 | throw new Error("fail"); 206 | } catch (err) { 207 | assert.isTrue(err.code == "ERR_SPREADABLE_APPROVAL_INFO_REQUIRED"); 208 | } 209 | }); 210 | 211 | it("should approve captcha requests", async () => { 212 | const approval = await nodes[0].getApproval("captcha"); 213 | const approvalInfo = await client.getApprovalQuestion("captcha"); 214 | const approvers = approvalInfo.approvers; 215 | const answers = {}; 216 | 217 | for (let i = 0; i < nodes.length; i++) { 218 | const info = await nodes[i].db.getApproval(approvalInfo.key); 219 | answers[nodes[i].address] = info.answer; 220 | } 221 | 222 | let length = approval.captchaLength; 223 | let answer = ""; 224 | 225 | for (let i = 0; i < approvers.length; i++) { 226 | const address = approvers[i]; 227 | const count = Math.floor(length / (approvers.length - i)); 228 | answer += answers[address].slice(0, count); 229 | length -= count; 230 | } 231 | 232 | approvalInfo.answer = answer; 233 | delete approvalInfo.question; 234 | const result = await nodes[0].requestServer( 235 | nodes[0].address, 236 | "approval-captcha-test", 237 | { body: { approvalInfo } } 238 | ); 239 | assert.isTrue(result.success); 240 | }); 241 | }); 242 | } 243 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import fse from "fs-extra"; 2 | import tools from "./tools.js"; 3 | import utils from "./utils.js"; 4 | import service from "./service.js"; 5 | import node from "./node.js"; 6 | import client from "./client.js"; 7 | import services from "./services.js"; 8 | import routes from "./routes.js"; 9 | import group from "./group.js"; 10 | 11 | describe('spreadable', () => { 12 | before(() => fse.ensureDir(tools.tmpPath)); 13 | after(() => fse.remove(tools.tmpPath)); 14 | 15 | describe('utils', utils.bind(this)); 16 | describe('service', service.bind(this)); 17 | describe('node', node.bind(this)); 18 | describe('client', client.bind(this)); 19 | describe('services', services.bind(this)); 20 | describe('routes', routes.bind(this)); 21 | describe('group', group.bind(this)); 22 | }); 23 | -------------------------------------------------------------------------------- /test/logger/adapter.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import adapter from "../../src/logger/transports/adapter/index.js"; 3 | import logger from "../../src/logger/transports/logger/index.js"; 4 | import transports from "../../src/logger/index.js"; 5 | 6 | const LoggerAdapter = adapter(); 7 | const Logger = logger(); 8 | 9 | class LoggerInterface extends Logger { 10 | constructor() { 11 | super(...arguments); 12 | this.infoCounter = 0; 13 | this.warnCounter = 0; 14 | this.errorCounter = 0; 15 | this.initCounter = 0; 16 | this.deinitCounter = 0; 17 | this.destroyCounter = 0; 18 | } 19 | 20 | async log(level) { 21 | this[level + 'Counter']++; 22 | } 23 | 24 | async init() { 25 | this.initCounter++; 26 | } 27 | 28 | async deinit() { 29 | this.deinitCounter++; 30 | } 31 | 32 | async destroy() { 33 | this.destroyCounter++; 34 | } 35 | } 36 | 37 | transports.LoggerInterface = LoggerInterface; 38 | 39 | export default function () { 40 | describe('LoggerConsole', function () { 41 | let logger; 42 | 43 | describe('instance creation', function () { 44 | it('should create an instance', function () { 45 | assert.doesNotThrow(() => logger = new LoggerAdapter({ 46 | transports: [ 47 | { transport: LoggerInterface, options: { x: 1 } }, 48 | { transport: 'LoggerInterface', options: { x: 2 } } 49 | ] 50 | })); 51 | logger.node = this.node; 52 | }); 53 | }); 54 | 55 | describe('.init()', function () { 56 | it('should not throw an exception', async function () { 57 | await logger.init(); 58 | }); 59 | 60 | it('should add two transports', async function () { 61 | assert.equal(logger.transports.length, 2); 62 | }); 63 | 64 | it('should add the transport options', async function () { 65 | assert.equal(logger.transports[0].options.x, 1, 'check the first'); 66 | assert.equal(logger.transports[1].options.x, 2, 'check the second'); 67 | }); 68 | 69 | it('should increment', async function () { 70 | assert.equal(logger.transports[0].initCounter, 1, 'check the first'); 71 | assert.equal(logger.transports[1].initCounter, 1, 'check the second'); 72 | }); 73 | }); 74 | 75 | describe('.info()', function () { 76 | it('should not increment', async function () { 77 | logger.level = 'warn'; 78 | await logger.info(); 79 | assert.equal(logger.transports[0].infoCounter, 0, 'check the first'); 80 | assert.equal(logger.transports[1].infoCounter, 0, 'check the second'); 81 | }); 82 | 83 | it('should increment', async function () { 84 | logger.level = 'info'; 85 | await logger.info(); 86 | assert.equal(logger.transports[0].infoCounter, 1, 'check the first'); 87 | assert.equal(logger.transports[1].infoCounter, 1, 'check the second'); 88 | }); 89 | }); 90 | 91 | describe('.warn()', function () { 92 | it('should not increment', async function () { 93 | logger.level = 'error'; 94 | await logger.info(); 95 | assert.equal(logger.transports[0].warnCounter, 0, 'check the first'); 96 | assert.equal(logger.transports[1].warnCounter, 0, 'check the second'); 97 | }); 98 | 99 | it('should increment', async function () { 100 | logger.level = 'warn'; 101 | await logger.warn(); 102 | assert.equal(logger.transports[0].warnCounter, 1, 'check the first'); 103 | assert.equal(logger.transports[1].warnCounter, 1, 'check the second'); 104 | }); 105 | }); 106 | 107 | describe('.error()', function () { 108 | it('should not increment', async function () { 109 | logger.level = false; 110 | await logger.info(); 111 | assert.equal(logger.transports[0].errorCounter, 0, 'check the first'); 112 | assert.equal(logger.transports[1].errorCounter, 0, 'check the second'); 113 | }); 114 | 115 | it('should increment', async function () { 116 | logger.level = 'error'; 117 | await logger.error(); 118 | assert.equal(logger.transports[0].errorCounter, 1, 'check the first'); 119 | assert.equal(logger.transports[1].errorCounter, 1, 'check the second'); 120 | }); 121 | }); 122 | 123 | describe('.addTransport()', function () { 124 | it('should add a new transport', async function () { 125 | logger.addTransport(new LoggerInterface({ x: 3 })); 126 | assert.equal(logger.transports[2].options.x, 3); 127 | }); 128 | }); 129 | 130 | describe('.removeTransport()', function () { 131 | it('should add a new transport', async function () { 132 | logger.removeTransport(logger.transports[2]); 133 | assert.isUndefined(logger.transports[2]); 134 | }); 135 | }); 136 | 137 | describe('.deinit()', function () { 138 | let first; 139 | let second; 140 | 141 | before(function () { 142 | first = logger.transports[0]; 143 | second = logger.transports[1]; 144 | }); 145 | 146 | it('should not throw an exception', async function () { 147 | await logger.deinit(); 148 | }); 149 | 150 | it('should remove transports', async function () { 151 | assert.lengthOf(logger.transports, 0); 152 | }); 153 | 154 | it('should increment', async function () { 155 | assert.equal(first.deinitCounter, 1, 'check the first'); 156 | assert.equal(second.deinitCounter, 1, 'check the second'); 157 | }); 158 | }); 159 | 160 | describe('reinitialization', () => { 161 | it('should not throw an exception', async function () { 162 | await logger.init(); 163 | }); 164 | it('should increment', async function () { 165 | assert.equal(logger.transports[0].initCounter, 1, 'check the first'); 166 | assert.equal(logger.transports[1].initCounter, 1, 'check the second'); 167 | }); 168 | }); 169 | 170 | describe('.destroy()', function () { 171 | let first; 172 | let second; 173 | 174 | before(function () { 175 | first = logger.transports[0]; 176 | second = logger.transports[1]; 177 | }); 178 | 179 | it('should not throw an exception', async function () { 180 | await logger.destroy(); 181 | }); 182 | 183 | it('should increment', async function () { 184 | assert.equal(first.destroyCounter, 1, 'check the first'); 185 | assert.equal(second.destroyCounter, 1, 'check the second'); 186 | }); 187 | }); 188 | }); 189 | } -------------------------------------------------------------------------------- /test/logger/console.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import loggerConsole from "../../src/logger/transports/console/index.js"; 3 | 4 | const LoggerConsole = loggerConsole(); 5 | 6 | export default function () { 7 | describe('LoggerConsole', function () { 8 | let logger; 9 | let fn; 10 | let status; 11 | let levels; 12 | 13 | before(() => { 14 | status = {}; 15 | fn = {}; 16 | levels = ['info', 'warn', 'error']; 17 | 18 | for (let i = 0; i < levels.length; i++) { 19 | const level = levels[i]; 20 | status[level] = 0; 21 | //eslint-disable-next-line no-console 22 | fn[level] = console[level], console[level] = () => status[level]++; 23 | } 24 | }); 25 | 26 | after(() => { 27 | for (let i = 0; i < levels.length; i++) { 28 | const level = levels[i]; 29 | //eslint-disable-next-line no-console 30 | console[level] = fn[level]; 31 | } 32 | }); 33 | 34 | describe('instance creation', function () { 35 | it('should create an instance', function () { 36 | assert.doesNotThrow(() => logger = new LoggerConsole()); 37 | logger.node = this.node; 38 | }); 39 | }); 40 | 41 | describe('.init()', function () { 42 | it('should not throw an exception', async function () { 43 | await logger.init(); 44 | }); 45 | }); 46 | 47 | describe('.info()', function () { 48 | it('should not increment', async function () { 49 | logger.level = 'warn'; 50 | await logger.info('test info'); 51 | assert.equal(status['info'], 0); 52 | }); 53 | 54 | it('should increment', async function () { 55 | logger.level = 'info'; 56 | await logger.info('test info'); 57 | assert.equal(status['info'], 1); 58 | }); 59 | }); 60 | 61 | describe('.warn()', function () { 62 | it('should not increment', async function () { 63 | logger.level = 'error'; 64 | await logger.info('test warn'); 65 | assert.equal(status['warn'], 0); 66 | }); 67 | 68 | it('should increment', async function () { 69 | logger.level = 'warn'; 70 | await logger.warn('test warn'); 71 | assert.equal(status['warn'], 1); 72 | }); 73 | }); 74 | 75 | describe('.error()', function () { 76 | it('should not increment', async function () { 77 | logger.level = false; 78 | await logger.info('test warn'); 79 | assert.equal(status['error'], 0); 80 | }); 81 | 82 | it('should increment', async function () { 83 | logger.level = 'error'; 84 | await logger.error('test error'); 85 | assert.equal(status['error'], 1); 86 | }); 87 | }); 88 | 89 | describe('.deinit()', function () { 90 | it('should not throw an exception', async function () { 91 | await logger.deinit(); 92 | }); 93 | }); 94 | 95 | describe('reinitialization', () => { 96 | it('should not throw an exception', async function () { 97 | await logger.init(); 98 | }); 99 | }); 100 | 101 | describe('.destroy()', function () { 102 | it('should not throw an exception', async function () { 103 | await logger.destroy(); 104 | }); 105 | }); 106 | }); 107 | } -------------------------------------------------------------------------------- /test/logger/file.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import fse from "fs-extra"; 3 | import path from "path"; 4 | import tools from "../tools.js"; 5 | import file from "../../src/logger/transports/file/index.js"; 6 | 7 | const LoggerFile = file(); 8 | 9 | export default function () { 10 | describe('LoggerConsole', function () { 11 | let logger; 12 | let folder; 13 | 14 | before(() => { 15 | folder = path.join(tools.tmpPath, 'file-logs'); 16 | }); 17 | 18 | describe('instance creation', function () { 19 | it('should create an instance', function () { 20 | assert.doesNotThrow(() => logger = new LoggerFile({ folder, level: 'info' })); 21 | logger.node = this.node; 22 | }); 23 | }); 24 | 25 | describe('.init()', function () { 26 | it('should not throw an exception', async function () { 27 | await logger.init(); 28 | }); 29 | 30 | it('should create the first file', async function () { 31 | assert.lengthOf(await fse.readdir(folder), 1); 32 | }); 33 | }); 34 | 35 | describe('.getMessageSize()', function () { 36 | it('should return the right size', function () { 37 | const prepared = logger.prepareMessage('test', 'info'); 38 | assert.equal(logger.getMessageSize(prepared), Buffer.byteLength(prepared, 'utf8')); 39 | }); 40 | }); 41 | 42 | describe('.prepareMessage()', function () { 43 | it('should prepare the right message', function () { 44 | const message = 'test'; 45 | const level = 'warn'; 46 | const prepared = logger.prepareMessage(message, level); 47 | const json = JSON.parse(prepared); 48 | assert.equal(message, json.message, 'check the message'); 49 | assert.equal(level, json.level, 'check the type'); 50 | assert.isNotNaN(new Date(json.date).getTime(), 'check the date'); 51 | }); 52 | }); 53 | 54 | describe('.info()', function () { 55 | it('should write nothing', async function () { 56 | logger.level = 'warn'; 57 | const message = 'test info'; 58 | const prepared = logger.prepareMessage(message, 'info'); 59 | await logger.info(message); 60 | const last = logger.getLastFile(); 61 | const content = await fse.readFile(last.filePath); 62 | assert.isNotOk(content.toString().match(prepared)); 63 | }); 64 | 65 | it('should write the info message', async function () { 66 | logger.level = 'info'; 67 | const message = 'test info'; 68 | const prepared = logger.prepareMessage(message, 'info'); 69 | await logger.info(message); 70 | const last = logger.getLastFile(); 71 | const content = await fse.readFile(last.filePath); 72 | assert.isOk(content.toString().match(prepared)); 73 | }); 74 | }); 75 | 76 | describe('.warn()', function () { 77 | it('should write nothing', async function () { 78 | logger.level = 'error'; 79 | const message = 'test warn'; 80 | const prepared = logger.prepareMessage(message, 'warn'); 81 | await logger.warn(message); 82 | const last = logger.getLastFile(); 83 | const content = await fse.readFile(last.filePath); 84 | assert.isNotOk(content.toString().match(prepared)); 85 | }); 86 | 87 | it('should write the warn message', async function () { 88 | logger.level = 'warn'; 89 | const message = 'test warn'; 90 | const prepared = logger.prepareMessage(message, 'warn'); 91 | await logger.warn(message); 92 | const last = logger.getLastFile(); 93 | const content = await fse.readFile(last.filePath); 94 | assert.isOk(content.toString().match(prepared)); 95 | }); 96 | }); 97 | 98 | describe('.error()', function () { 99 | it('should write nothing', async function () { 100 | logger.level = false; 101 | const message = 'test error'; 102 | const prepared = logger.prepareMessage(message, 'error'); 103 | await logger.error(message); 104 | const last = logger.getLastFile(); 105 | const content = await fse.readFile(last.filePath); 106 | assert.isNotOk(content.toString().match(prepared)); 107 | }); 108 | 109 | it('should write the error message', async function () { 110 | logger.level = 'error'; 111 | const message = 'test error'; 112 | const prepared = logger.prepareMessage(message, 'error'); 113 | await logger.error(message); 114 | const last = logger.getLastFile(); 115 | const content = await fse.readFile(last.filePath); 116 | assert.isOk(content.toString().match(prepared)); 117 | }); 118 | }); 119 | 120 | describe('.log()', function () { 121 | it('should write in a new file', async function () { 122 | logger.level = 'info'; 123 | const message = 'test'; 124 | const first = logger.getLastFile(); 125 | logger.options.fileMaxSize = first.stat.size; 126 | const prepared = logger.prepareMessage(message, 'info'); 127 | await logger.info(message); 128 | const last = logger.getLastFile(); 129 | const content = await fse.readFile(last.filePath); 130 | assert.notEqual(first.filePath, last.filePath, 'check the file'); 131 | assert.equal(content.toString(), prepared + '\n', 'check the content'); 132 | }); 133 | 134 | it('should add messages in parallel', async function () { 135 | logger.options.fileMaxSize = '10mb'; 136 | const messages = []; 137 | const p = []; 138 | for (let i = 1; i < 10; i++) { 139 | messages.push('$$' + i); 140 | p.push(logger.info(messages[messages.length - 1])); 141 | } 142 | await Promise.all(p); 143 | const last = logger.getLastFile(); 144 | const content = (await fse.readFile(last.filePath)).toString(); 145 | for (let i = 0; i < messages.length; i++) { 146 | assert.isOk(content.indexOf(messages[i]) >= 0); 147 | } 148 | }); 149 | }); 150 | 151 | describe('.addNewFile()', function () { 152 | it('should create a new file', async function () { 153 | const files = await fse.readdir(folder); 154 | const prev = logger.getLastFile(); 155 | const file = await logger.addNewFile(); 156 | const last = logger.getLastFile(); 157 | assert.equal(files.length + 1, (await fse.readdir(folder)).length, 'check the count'); 158 | assert.equal(file.index, prev.index + 1, 'check the index'); 159 | assert.isTrue(file.filePath != prev.filePath && file.filePath == last.filePath, 'check the path'); 160 | assert.containsAllKeys(file.stat, ['size'], 'check the stat'); 161 | }); 162 | }); 163 | 164 | describe('.getLastFile()', function () { 165 | it('should get the last file', async function () { 166 | const files = await fse.readdir(folder); 167 | let max = 1; 168 | for (let i = 0; i < files.length; i++) { 169 | const index = parseInt(path.basename(files[i])); 170 | index > max && (max = index); 171 | } 172 | const first = logger.getLastFile(); 173 | assert.equal(first.index, max, 'check before addition'); 174 | await logger.addNewFile(); 175 | const last = logger.getLastFile(); 176 | assert.equal(first.index + 1, last.index, 'check after addition'); 177 | }); 178 | }); 179 | 180 | describe('.normalizeFilesCount()', function () { 181 | before(async function () { 182 | await fse.emptyDir(folder); 183 | }); 184 | 185 | it('should create a new file', async function () { 186 | await logger.normalizeFilesCount(); 187 | const files = await fse.readdir(folder); 188 | assert.equal(files.length, 1); 189 | }); 190 | 191 | it('should remove excess files', async function () { 192 | const count = logger.options.filesCount + 2; 193 | for (let i = 0; i < count; i++) { 194 | await logger.addNewFile(); 195 | } 196 | await logger.normalizeFilesCount(); 197 | const files = await fse.readdir(folder); 198 | assert.equal(files.length, logger.options.filesCount); 199 | }); 200 | }); 201 | 202 | describe('.deinit()', function () { 203 | it('should not throw an exception', async function () { 204 | await logger.deinit(); 205 | }); 206 | }); 207 | 208 | describe('reinitialization', () => { 209 | it('should not throw an exception', async function () { 210 | await logger.init(); 211 | }); 212 | }); 213 | 214 | describe('.destroy()', function () { 215 | it('should not throw an exception', async function () { 216 | await logger.destroy(); 217 | }); 218 | 219 | it('should remove the folder', async function () { 220 | assert.isFalse(await fse.pathExists(folder)); 221 | }); 222 | }); 223 | }); 224 | } -------------------------------------------------------------------------------- /test/logger/logger.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import logger from "../../src/logger/transports/logger/index.js"; 3 | 4 | const Logger = logger(); 5 | 6 | export default function () { 7 | describe('Logger', () => { 8 | let logger; 9 | 10 | describe('instance creation', function () { 11 | it('should create an instance', function () { 12 | assert.doesNotThrow(() => logger = new Logger()); 13 | logger.node = this.node; 14 | }); 15 | }); 16 | 17 | describe('.init()', function () { 18 | it('should not throw an exception', async function () { 19 | await logger.init(); 20 | }); 21 | }); 22 | 23 | describe('.setLevel()', function () { 24 | it('should set the level', function () { 25 | const level = 'warn'; 26 | assert.equal(logger.level, logger.defaultLevel, 'check before'); 27 | logger.setLevel(level); 28 | assert.equal(logger.level, level, 'check before'); 29 | }); 30 | 31 | it('should throw an error with wrong level', function () { 32 | assert.throws(() => logger.setLevel('wrong')); 33 | }); 34 | 35 | it('should set the false', function () { 36 | logger.setLevel(false); 37 | assert.isFalse(logger.level); 38 | }); 39 | }); 40 | 41 | describe('.isLevelActive()', function () { 42 | it('should check the false level', function () { 43 | logger.isLevelActive('warn', 'check warn'); 44 | logger.isLevelActive('info', 'check info'); 45 | logger.isLevelActive('error', 'check error'); 46 | }); 47 | 48 | it('should check the info level', function () { 49 | logger.setLevel('info'); 50 | assert.isTrue(logger.isLevelActive('info'), 'check info'); 51 | assert.isTrue(logger.isLevelActive('warn'), 'check warn'); 52 | assert.isTrue(logger.isLevelActive('error'), 'check error'); 53 | }); 54 | 55 | it('should check the warn level', function () { 56 | logger.setLevel('warn'); 57 | assert.isFalse(logger.isLevelActive('info'), 'check info'); 58 | assert.isTrue(logger.isLevelActive('warn'), 'check warn'); 59 | assert.isTrue(logger.isLevelActive('error'), 'check error'); 60 | }); 61 | 62 | it('should check the error level', function () { 63 | logger.setLevel('error'); 64 | assert.isFalse(logger.isLevelActive('info'), 'check info'); 65 | assert.isFalse(logger.isLevelActive('warn'), 'check warn'); 66 | assert.isTrue(logger.isLevelActive('error'), 'check error'); 67 | }); 68 | }); 69 | 70 | describe('.deinit()', function () { 71 | it('should not throw an exception', async function () { 72 | await logger.deinit(); 73 | }); 74 | }); 75 | 76 | describe('reinitialization', () => { 77 | it('should not throw an exception', async function () { 78 | await logger.init(); 79 | }); 80 | }); 81 | 82 | describe('.destroy()', function () { 83 | it('should not throw an exception', async function () { 84 | await logger.destroy(); 85 | }); 86 | }); 87 | }); 88 | } -------------------------------------------------------------------------------- /test/node.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import node from "../src/node.js"; 3 | import tools from "./tools.js"; 4 | import fse from "fs-extra"; 5 | const Node = node(); 6 | 7 | export default function () { 8 | describe('Node', () => { 9 | let node; 10 | 11 | describe('instance creation', () => { 12 | it('should not create an instance because of port', async () => { 13 | const options = await tools.createNodeOptions({ port: '' }); 14 | assert.throws(() => new Node(options)); 15 | }); 16 | 17 | it('should create an instance', async () => { 18 | const options = await tools.createNodeOptions(); 19 | assert.doesNotThrow(() => node = new Node(options)); 20 | }); 21 | }); 22 | 23 | describe('.init()', () => { 24 | it('should not throw an exception', async () => { 25 | await node.init(); 26 | }); 27 | 28 | it('should create the db file', async () => { 29 | assert.isTrue(await fse.pathExists(tools.getDbFilePath(node))); 30 | }); 31 | }); 32 | 33 | describe('.getValueGivenNetworkSize()', () => { 34 | let networkSize; 35 | 36 | before(async () => { 37 | for (let i = 0; i < 9; i++) { 38 | await node.db.addMaster(`localhost:${i + 1}`, 1); 39 | } 40 | networkSize = await node.getNetworkSize(); 41 | }); 42 | 43 | after(async () => { 44 | await node.db.removeMasters(); 45 | }); 46 | 47 | it('should return the specified option value', async () => { 48 | const val = 2; 49 | assert.equal(await node.getValueGivenNetworkSize(val), val); 50 | }); 51 | 52 | it('should return the percentage', async () => { 53 | assert.equal(await node.getValueGivenNetworkSize('50%'), Math.ceil(networkSize / 2)); 54 | }); 55 | 56 | it('should return sqrt', async () => { 57 | assert.equal(await node.getValueGivenNetworkSize('auto'), Math.ceil(Math.sqrt(networkSize))); 58 | }); 59 | 60 | it('should return cbrt', async () => { 61 | assert.equal(await node.getValueGivenNetworkSize('r-3'), Math.ceil(Math.cbrt(networkSize))); 62 | }); 63 | 64 | it('should return the network size', async () => { 65 | assert.equal(await node.getValueGivenNetworkSize(networkSize + 10), networkSize); 66 | }); 67 | }); 68 | 69 | describe('.deinit()', () => { 70 | it('should not throw an exception', async () => { 71 | await node.deinit(); 72 | }); 73 | 74 | it('should not remove the db file', async () => { 75 | assert.isTrue(await fse.pathExists(tools.getDbFilePath(node))); 76 | }); 77 | }); 78 | 79 | describe('reinitialization', () => { 80 | it('should not throw an exception', async () => { 81 | await node.init(); 82 | }); 83 | 84 | it('should create the db file', async () => { 85 | assert.isTrue(await fse.pathExists(tools.getDbFilePath(node))); 86 | }); 87 | }); 88 | 89 | describe('.destroy()', () => { 90 | it('should not throw an exception', async () => { 91 | await node.destroy(); 92 | }); 93 | 94 | it('should remove the db file', async () => { 95 | assert.isFalse(await fse.pathExists(tools.getDbFilePath(node))); 96 | }); 97 | }); 98 | }); 99 | } -------------------------------------------------------------------------------- /test/routes.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import fetch from "node-fetch"; 3 | import node from "../src/node.js"; 4 | import client from "../src/client.js"; 5 | import approvalClient from "../src/approval/transports/client/index.js"; 6 | import utils from "../src/utils.js"; 7 | import schema from "../src/schema.js"; 8 | import tools from "./tools.js"; 9 | 10 | const Node = node(); 11 | const Client = client(); 12 | const ApprovalClient = approvalClient(); 13 | 14 | export default function () { 15 | describe('routes', () => { 16 | let node; 17 | let client; 18 | 19 | before(async function () { 20 | node = new Node(await tools.createNodeOptions({ 21 | network: { 22 | auth: { username: 'username', password: 'password' } 23 | } 24 | })); 25 | await node.addApproval('test', new ApprovalClient()); 26 | await node.init(); 27 | await node.sync(); 28 | client = new Client(await tools.createClientOptions({ 29 | address: node.address, 30 | auth: { username: 'username', password: 'password' } 31 | })); 32 | await client.init(); 33 | }); 34 | 35 | after(async function () { 36 | await node.deinit(); 37 | await client.deinit(); 38 | }); 39 | 40 | describe('/ping', function () { 41 | it('should return the right address', async function () { 42 | const res = await fetch(`http://${node.address}/ping`); 43 | const json = await res.json(); 44 | assert.equal(json.address, node.address); 45 | assert.equal(json.version, node.getVersion()); 46 | }); 47 | }); 48 | 49 | describe('/status', function () { 50 | it('should return an auth error', async function () { 51 | const res = await fetch(`http://${node.address}/status`); 52 | assert.equal(await res.status, 401); 53 | }); 54 | 55 | it('should return the status', async function () { 56 | const options = client.createDefaultRequestOptions({ method: 'get' }); 57 | const res = await fetch(`http://${node.address}/status`, options); 58 | const json = await res.json(); 59 | assert.doesNotThrow(() => { 60 | utils.validateSchema(schema.getStatusResponse(), json); 61 | }); 62 | }); 63 | 64 | it('should return the pretty status', async function () { 65 | const options = client.createDefaultRequestOptions({ method: 'get' }); 66 | const res = await fetch(`http://${node.address}/status?pretty`, options); 67 | const json = await res.json(); 68 | assert.doesNotThrow(() => { 69 | utils.validateSchema(schema.getStatusPrettyResponse(), json); 70 | }); 71 | }); 72 | }); 73 | 74 | describe('/client/get-available-node', function () { 75 | it('should return an auth error', async function () { 76 | const res = await fetch(`http://${node.address}/client/get-available-node`, { method: 'post' }); 77 | assert.equal(await res.status, 401); 78 | }); 79 | 80 | it('should return the node address', async function () { 81 | const options = client.createDefaultRequestOptions(); 82 | const res = await fetch(`http://${node.address}/client/get-available-node`, options); 83 | const json = await res.json(); 84 | assert.doesNotThrow(() => { 85 | utils.validateSchema(schema.getAvailableNodeResponse(), json); 86 | }); 87 | }); 88 | }); 89 | 90 | describe('/client/request-approval-key', function () { 91 | it('should return an auth error', async function () { 92 | const res = await fetch(`http://${node.address}/client/request-approval-key`, { method: 'post' }); 93 | assert.equal(await res.status, 401); 94 | }); 95 | 96 | it('should return a data error', async function () { 97 | const options = client.createDefaultRequestOptions(); 98 | const res = await fetch(`http://${node.address}/client/request-approval-key`, options); 99 | assert.equal(res.status, 422); 100 | }); 101 | 102 | it('should return the right schema', async function () { 103 | const body = { action: 'test' }; 104 | const options = client.createDefaultRequestOptions(tools.createJsonRequestOptions({ body })); 105 | const res = await fetch(`http://${node.address}/client/request-approval-key`, options); 106 | const json = await res.json(); 107 | assert.doesNotThrow(() => { 108 | utils.validateSchema(schema.getRequestApprovalKeyResponse(), json); 109 | }); 110 | }); 111 | }); 112 | 113 | describe('/client/add-approval-info', function () { 114 | it('should return an auth error', async function () { 115 | const res = await fetch(`http://${node.address}/client/add-approval-info`, { method: 'post' }); 116 | assert.equal(await res.status, 401); 117 | }); 118 | 119 | it('should return a data error', async function () { 120 | const options = client.createDefaultRequestOptions(); 121 | const res = await fetch(`http://${node.address}/client/add-approval-info`, options); 122 | assert.equal(res.status, 422); 123 | }); 124 | 125 | it('should return the success message', async function () { 126 | const approval = await node.getApproval('test'); 127 | const body = { 128 | action: 'test', 129 | key: 'key', 130 | startedAt: utils.getClosestPeriodTime(Date.now(), approval.period) 131 | }; 132 | const options = client.createDefaultRequestOptions(tools.createJsonRequestOptions({ body })); 133 | const res = await fetch(`http://${node.address}/client/add-approval-info`, options); 134 | const json = await res.json(); 135 | assert.isTrue(json.success); 136 | }); 137 | }); 138 | 139 | describe('/client/request-approval-question', function () { 140 | it('should return an auth error', async function () { 141 | const res = await fetch(`http://${node.address}/client/request-approval-question`, { method: 'post' }); 142 | assert.equal(await res.status, 401); 143 | }); 144 | 145 | it('should return a data error', async function () { 146 | const options = client.createDefaultRequestOptions(); 147 | const res = await fetch(`http://${node.address}/client/request-approval-question`, options); 148 | assert.equal(res.status, 422); 149 | }); 150 | 151 | it('should return the question', async function () { 152 | const body = { 153 | action: 'test', 154 | key: 'key', 155 | confirmedAddresses: [node.address] 156 | }; 157 | const options = client.createDefaultRequestOptions(tools.createJsonRequestOptions({ body })); 158 | const res = await fetch(`http://${node.address}/client/request-approval-question`, options); 159 | const json = await res.json(); 160 | assert.isDefined(json.question); 161 | }); 162 | }); 163 | 164 | describe('/api/node/get-approval-info', function () { 165 | it('should return an auth error', async function () { 166 | const options = tools.createJsonRequestOptions(); 167 | const res = await fetch(`http://${node.address}/api/node/get-approval-info`, options); 168 | assert.equal(await res.status, 401); 169 | }); 170 | 171 | it('should return a data error', async function () { 172 | const options = node.createDefaultRequestOptions(); 173 | const res = await fetch(`http://${node.address}/api/node/get-approval-info`, options); 174 | assert.equal(res.status, 422); 175 | }); 176 | 177 | it('should return the info', async function () { 178 | const body = { 179 | action: 'test', 180 | key: 'key' 181 | }; 182 | const options = node.createDefaultRequestOptions(tools.createJsonRequestOptions({ body })); 183 | const res = await fetch(`http://${node.address}/api/node/get-approval-info`, options); 184 | const json = tools.createServerResponse(node.address, await res.json()); 185 | assert.isDefined(json.info); 186 | }); 187 | }); 188 | 189 | describe('/api/node/check-approval-answer', function () { 190 | it('should return an auth error', async function () { 191 | const options = tools.createJsonRequestOptions(); 192 | const res = await fetch(`http://${node.address}/api/node/check-approval-answer`, options); 193 | assert.equal(await res.status, 401); 194 | }); 195 | 196 | it('should return a data error', async function () { 197 | const options = node.createDefaultRequestOptions(); 198 | const res = await fetch(`http://${node.address}/api/node/check-approval-answer`, options); 199 | assert.equal(res.status, 422); 200 | }); 201 | 202 | it('should return the success message', async function () { 203 | const approver = await node.db.getApproval('key'); 204 | const body = { 205 | action: 'test', 206 | key: 'key', 207 | clientIp: approver.clientIp, 208 | approvers: [node.address], 209 | answer: approver.answer 210 | }; 211 | const options = node.createDefaultRequestOptions(tools.createJsonRequestOptions({ body })); 212 | const res = await fetch(`http://${node.address}/api/node/check-approval-answer`, options); 213 | const json = tools.createServerResponse(node.address, await res.json()); 214 | assert.isTrue(json.success); 215 | }); 216 | }); 217 | 218 | describe('/api/node/register', function () { 219 | let targetNode; 220 | before(async () => { 221 | targetNode = new Node(await tools.createNodeOptions({ 222 | initialNetworkAddress: node.address, 223 | network: node.options.network 224 | })); 225 | await targetNode.init(); 226 | await targetNode.sync(); 227 | }); 228 | 229 | after(async () => { 230 | await targetNode.deinit(); 231 | }); 232 | 233 | it('should return an auth error', async function () { 234 | const res = await fetch(`http://${node.address}/api/node/register`, { method: 'post' }); 235 | assert.equal(await res.status, 401); 236 | }); 237 | 238 | it('should return an interview error', async function () { 239 | const body = { target: 'localhost:1' }; 240 | const options = node.createDefaultRequestOptions(tools.createJsonRequestOptions({ body })); 241 | const res = await fetch(`http://${node.address}/api/node/register`, options); 242 | assert.equal(await res.status, 422); 243 | }); 244 | 245 | it('should return the right schema', async function () { 246 | const body = { target: targetNode.address }; 247 | const options = node.createDefaultRequestOptions(tools.createJsonRequestOptions({ body })); 248 | const res = await fetch(`http://${node.address}/api/node/register`, options); 249 | const json = tools.createServerResponse(node.address, await res.json()); 250 | assert.doesNotThrow(() => { 251 | utils.validateSchema(schema.getRegisterResponse(), json); 252 | }); 253 | }); 254 | }); 255 | 256 | describe('/api/node/structure', function () { 257 | it('should return an auth error', async function () { 258 | const options = tools.createJsonRequestOptions(); 259 | const res = await fetch(`http://${node.address}/api/node/structure`, options); 260 | assert.equal(await res.status, 401); 261 | }); 262 | 263 | it('should return the right schema', async function () { 264 | const options = node.createDefaultRequestOptions(); 265 | const res = await fetch(`http://${node.address}/api/node/structure`, options); 266 | const json = tools.createServerResponse(node.address, await res.json()); 267 | assert.doesNotThrow(() => { 268 | utils.validateSchema(schema.getStructureResponse(), json); 269 | }); 270 | }); 271 | }); 272 | 273 | describe('/api/node/get-interview-summary', function () { 274 | it('should return an auth error', async function () { 275 | const options = tools.createJsonRequestOptions(); 276 | const res = await fetch(`http://${node.address}/api/node/get-interview-summary`, options); 277 | assert.equal(await res.status, 401); 278 | }); 279 | 280 | it('should return the right schema', async function () { 281 | const options = node.createDefaultRequestOptions(); 282 | const res = await fetch(`http://${node.address}/api/node/get-interview-summary`, options); 283 | const json = tools.createServerResponse(node.address, await res.json()); 284 | assert.doesNotThrow(() => { 285 | utils.validateSchema(schema.getInterviewSummaryResponse(), json); 286 | }); 287 | }); 288 | }); 289 | 290 | describe('/api/node/provide-registration', function () { 291 | it('should return an auth error', async function () { 292 | const res = await fetch(`http://${node.address}/api/node/provide-registration`, { method: 'post' }); 293 | assert.equal(await res.status, 401); 294 | }); 295 | 296 | it('should return the right schema', async function () { 297 | const body = { target: node.address }; 298 | const options = node.createDefaultRequestOptions(tools.createJsonRequestOptions({ body })); 299 | const res = await fetch(`http://${node.address}/api/node/provide-registration`, options); 300 | const json = tools.createServerResponse(node.address, await res.json()); 301 | assert.doesNotThrow(() => { 302 | utils.validateSchema(schema.getProvideRegistrationResponse(), json); 303 | }); 304 | }); 305 | }); 306 | }); 307 | } -------------------------------------------------------------------------------- /test/server/express.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import server from "../../src/server/transports/express/index.js"; 3 | 4 | const ServerExpress = server(); 5 | 6 | export default function () { 7 | describe('ServerExpress', () => { 8 | let server; 9 | let nodeServer; 10 | 11 | describe('instance creation', function () { 12 | it('should create an instance', function () { 13 | assert.doesNotThrow(() => server = new ServerExpress()); 14 | server.node = this.node; 15 | nodeServer = this.node.server; 16 | this.node.server = server; 17 | }); 18 | }); 19 | 20 | describe('.init()', function () { 21 | it('should not throw an exception', async function () { 22 | await server.init(); 23 | }); 24 | }); 25 | 26 | describe('.deinit()', function () { 27 | it('should not throw an exception', async function () { 28 | await server.deinit(); 29 | }); 30 | }); 31 | 32 | describe('reinitialization', () => { 33 | it('should not throw an exception', async function () { 34 | await server.init(); 35 | }); 36 | }); 37 | 38 | describe('.destroy()', function () { 39 | it('should not throw an exception', async function () { 40 | await server.destroy(); 41 | this.node.server = nodeServer; 42 | }); 43 | }); 44 | }); 45 | } -------------------------------------------------------------------------------- /test/server/server.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import fetch from "node-fetch"; 3 | import https from "https"; 4 | import server from "../../src/server/transports/server/index.js"; 5 | import utils from "../../src/utils.js"; 6 | import * as selfsigned from "selfsigned"; 7 | 8 | const Server = server(); 9 | 10 | export default function () { 11 | describe('Server', () => { 12 | let server; 13 | let nodeServer; 14 | 15 | describe('instance creation', function () { 16 | it('should create an instance', function () { 17 | assert.doesNotThrow(() => server = new Server()); 18 | server.node = this.node; 19 | nodeServer = this.node.server; 20 | this.node.server = server; 21 | }); 22 | }); 23 | 24 | describe('.init()', function () { 25 | it('should not throw an exception', async function () { 26 | await server.init(); 27 | }); 28 | 29 | it('should ping to the server', async function () { 30 | const res = await fetch(`http://${this.node.address}`); 31 | assert.equal(res.status, 200); 32 | }); 33 | }); 34 | 35 | describe('.deinit()', function () { 36 | it('should not throw an exception', async function () { 37 | await server.deinit(); 38 | }); 39 | 40 | it('should not ping to the server', async function () { 41 | assert.isFalse(await utils.isPortUsed(this.node.port)); 42 | }); 43 | }); 44 | 45 | describe('reinitialization', () => { 46 | it('should not throw an exception', async function () { 47 | const pems = selfsigned.generate(); 48 | server.options.https = { key: pems.private, cert: pems.cert, ca: pems.clientcert }; 49 | await server.init(); 50 | }); 51 | 52 | it('should ping to the server', async function () { 53 | const agent = new https.Agent({ rejectUnauthorized: false }); 54 | const res = await fetch(`https://${this.node.address}`, { agent }); 55 | assert.equal(res.status, 200); 56 | }); 57 | }); 58 | 59 | describe('.destroy()', function () { 60 | it('should not throw an exception', async function () { 61 | await server.destroy(); 62 | this.node.server = nodeServer; 63 | }); 64 | 65 | it('should not ping to the server', async function () { 66 | assert.isFalse(await utils.isPortUsed(this.node.port)); 67 | }); 68 | }); 69 | }); 70 | } -------------------------------------------------------------------------------- /test/service.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import Service from "../src/service.js"; 3 | 4 | export default function () { 5 | describe('Service', () => { 6 | let service; 7 | 8 | describe('instance creation', () => { 9 | it('should create an instance', async () => { 10 | assert.doesNotThrow(() => service = new Service()); 11 | }); 12 | }); 13 | 14 | describe('.init()', () => { 15 | it('should not initialize the slave service without registration', async () => { 16 | try { 17 | await service.init(); 18 | throw new Error('Fail'); 19 | } 20 | catch (err) { 21 | assert.isOk(err.message.match('You have to register')); 22 | } 23 | }); 24 | 25 | it('should initialize the service', async () => { 26 | service.__isMasterService = true; 27 | await service.init(); 28 | assert.typeOf(service.__initialized, 'number'); 29 | }); 30 | }); 31 | 32 | describe('.isInitialized()', () => { 33 | it('should be true', () => { 34 | assert.isTrue(service.isInitialized()); 35 | }); 36 | }); 37 | 38 | describe('.deinit()', () => { 39 | it('should deinitialize the server', async () => { 40 | await service.deinit(); 41 | assert.isFalse(service.__initialized); 42 | }); 43 | }); 44 | 45 | describe('.isInitialized()', () => { 46 | it('should be false', () => { 47 | assert.isFalse(service.isInitialized()); 48 | }); 49 | }); 50 | 51 | describe('reinitialization', () => { 52 | it('should not throw an exception', async () => { 53 | await service.init(); 54 | assert.isOk(service.isInitialized()); 55 | }); 56 | }); 57 | 58 | describe('.destroy()', () => { 59 | it('should destroy the service', async () => { 60 | await service.destroy(); 61 | assert.isFalse(service.isInitialized()); 62 | }); 63 | }); 64 | }); 65 | } -------------------------------------------------------------------------------- /test/services.js: -------------------------------------------------------------------------------- 1 | import node from "../src/node.js"; 2 | import tools from "./tools.js"; 3 | import database from "./db/database.js"; 4 | import loki from "./db/loki.js"; 5 | import behavior from "./behavior/behavior.js"; 6 | import behaviorFail from "./behavior/fail.js"; 7 | import approval from "./approval/approval.js" 8 | import approvalCaptcha from "./approval/captcha.js" 9 | import approvalClient from "./approval/client.js" 10 | import cache from "./cache/cache.js"; 11 | import cacheDatabase from "./cache/database.js"; 12 | import logger from "./logger/logger.js"; 13 | import loggerConsole from "./logger/console.js"; 14 | import loggerFile from "./logger/file.js"; 15 | import loggerAdapter from "./logger/adapter.js"; 16 | import task from "./task/task.js"; 17 | import taskInterval from "./task/interval.js"; 18 | import taskCron from "./task/cron.js"; 19 | import server from "./server/server.js"; 20 | import express from "./server/express.js"; 21 | 22 | const Node = node(); 23 | 24 | export default function () { 25 | describe('services', () => { 26 | before(async function () { 27 | this.node = new Node(await tools.createNodeOptions({ server: false })); 28 | await this.node.init(); 29 | }); 30 | 31 | after(async function () { 32 | await this.node.destroy(); 33 | }); 34 | 35 | describe('db', () => { 36 | describe('database', database.bind(this)); 37 | describe('loki', loki.bind(this)); 38 | }); 39 | 40 | describe('behavior', () => { 41 | describe('behavior', behavior.bind(this)); 42 | describe('fail', behaviorFail.bind(this)); 43 | }); 44 | 45 | describe('approval', () => { 46 | describe('approval', approval.bind(this)); 47 | describe('client', approvalClient.bind(this)); 48 | describe('captcha', approvalCaptcha.bind(this)); 49 | }); 50 | 51 | describe('cache', () => { 52 | describe('cache', cache.bind(this)); 53 | describe('Cache Database', cacheDatabase.bind(this)); 54 | }); 55 | 56 | describe('logger', () => { 57 | describe('logger', logger.bind(this)); 58 | describe('console', loggerConsole.bind(this)); 59 | describe('file', loggerFile.bind(this)); 60 | describe('adapter', loggerAdapter.bind(this)); 61 | }); 62 | 63 | describe('task', () => { 64 | describe('task', task.bind(this)); 65 | describe('interval', taskInterval.bind(this)); 66 | describe('cron', taskCron.bind(this)); 67 | }); 68 | 69 | describe('server', () => { 70 | describe('server', server.bind(this)); 71 | describe('express', express.bind(this)); 72 | }); 73 | }); 74 | } -------------------------------------------------------------------------------- /test/task/cron.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import cron from "../../src/task/transports/cron/index.js"; 3 | import tools from "../tools.js"; 4 | 5 | const TaskCron = cron(); 6 | 7 | export default function () { 8 | describe('TaskCron', () => { 9 | let task; 10 | 11 | describe('instance creation', function () { 12 | it('should create an instance', function () { 13 | assert.doesNotThrow(() => task = new TaskCron()); 14 | task.node = this.node; 15 | }); 16 | }); 17 | 18 | describe('.init()', function () { 19 | it('should not throw an exception', async function () { 20 | await task.init(); 21 | }); 22 | }); 23 | 24 | describe('.start()', function () { 25 | it('should start the task every 1s', async function () { 26 | let counter = 0; 27 | const interval = 1000; 28 | const res = await task.add('test', '* * * * * *', () => counter++); 29 | await task.start(res); 30 | assert.equal(counter, 0, 'check before'); 31 | await tools.wait(interval * 2); 32 | assert.isOk(counter > 0, 'check after'); 33 | }); 34 | }); 35 | 36 | describe('.deinit()', function () { 37 | it('should not throw an exception', async function () { 38 | await task.deinit(); 39 | }); 40 | }); 41 | 42 | describe('reinitialization', () => { 43 | it('should not throw an exception', async function () { 44 | await task.init(); 45 | }); 46 | }); 47 | 48 | describe('.destroy()', function () { 49 | it('should not throw an exception', async function () { 50 | await task.destroy(); 51 | }); 52 | }); 53 | }); 54 | } -------------------------------------------------------------------------------- /test/task/interval.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import interval from "../../src/task/transports/interval/index.js"; 3 | import tools from "../tools.js"; 4 | 5 | const TaskInterval = interval(); 6 | 7 | export default function () { 8 | describe('TaskInterval', () => { 9 | let task; 10 | 11 | describe('instance creation', function () { 12 | it('should create an instance', function () { 13 | assert.doesNotThrow(() => task = new TaskInterval()); 14 | task.node = this.node; 15 | }); 16 | }); 17 | 18 | describe('.init()', function () { 19 | it('should not throw an exception', async function () { 20 | await task.init(); 21 | }); 22 | }); 23 | 24 | describe('.start()', function () { 25 | it('should start the task every 100ms', async function () { 26 | let counter = 0; 27 | const interval = 100; 28 | const res = await task.add('test', interval, () => counter++); 29 | await task.start(res); 30 | assert.equal(counter, 0, 'check before all'); 31 | await tools.wait(interval / 2); 32 | assert.equal(counter, 0, 'check after the half iteration'); 33 | await tools.wait(interval / 2); 34 | assert.equal(counter, 1, 'check after the first iteration'); 35 | await tools.wait(interval); 36 | assert.equal(counter, 2, 'check after the second iteration'); 37 | }); 38 | }); 39 | 40 | describe('.deinit()', function () { 41 | it('should not throw an exception', async function () { 42 | await task.deinit(); 43 | }); 44 | }); 45 | 46 | describe('reinitialization', () => { 47 | it('should not throw an exception', async function () { 48 | await task.init(); 49 | }); 50 | }); 51 | 52 | describe('.destroy()', function () { 53 | it('should not throw an exception', async function () { 54 | await task.destroy(); 55 | }); 56 | }); 57 | }); 58 | } -------------------------------------------------------------------------------- /test/task/task.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import task from "../../src/task/transports/task/index.js"; 3 | import tools from "../tools.js"; 4 | 5 | const Task = task(); 6 | 7 | export default function () { 8 | describe('Task', () => { 9 | let task; 10 | 11 | describe('instance creation', function () { 12 | it('should create an instance', function () { 13 | assert.doesNotThrow(() => task = new Task()); 14 | task.node = this.node; 15 | }); 16 | }); 17 | 18 | describe('.init()', function () { 19 | it('should not throw an exception', async function () { 20 | await task.init(); 21 | }); 22 | }); 23 | 24 | describe('.add()', function () { 25 | it('should create the task', async function () { 26 | const name = 'test'; 27 | const interval = 1; 28 | const option = 1; 29 | await task.add(name, interval, () => { }, { test: option }); 30 | const res = task.tasks[name]; 31 | assert.isObject(res, 'chek the object'); 32 | assert.equal(res.name, name, 'check the name'); 33 | assert.equal(res.interval, interval, 'check the interval'); 34 | assert.equal(res.test, option, 'check the option'); 35 | }); 36 | 37 | it('should update the task', async function () { 38 | const name = 'test'; 39 | const interval = 2; 40 | const option = 2; 41 | await task.add(name, interval, () => { }, { test: option }); 42 | const res = task.tasks[name]; 43 | assert.isObject(res, 'chek the object'); 44 | assert.equal(res.name, name, 'check the name'); 45 | assert.equal(res.interval, interval, 'check the interval'); 46 | assert.equal(res.test, option, 'check the option'); 47 | }); 48 | }); 49 | 50 | describe('.get()', function () { 51 | it('should get the task', async function () { 52 | const name = 'test'; 53 | const interval = 2; 54 | const option = 2; 55 | const res = await task.get(name); 56 | assert.isObject(res, 'chek the object'); 57 | assert.equal(res.name, name, 'check the name'); 58 | assert.equal(res.interval, interval, 'check the interval'); 59 | assert.equal(res.test, option, 'check the option'); 60 | }); 61 | 62 | it('should not get the wrong task', async function () { 63 | assert.isNull(await task.get('wrong')); 64 | }); 65 | }); 66 | 67 | describe('.remove()', function () { 68 | it('should remove the task', async function () { 69 | const name = 'test'; 70 | await task.remove(name); 71 | assert.isNull(await task.get(name)); 72 | }); 73 | }); 74 | 75 | describe('.start()', function () { 76 | it('should start the task', async function () { 77 | const res = await task.add('test', 1, () => { }); 78 | assert.isTrue(res.isStopped, 'check the status before'); 79 | await task.start(res); 80 | assert.isFalse(res.isStopped, 'check the status after'); 81 | }); 82 | }); 83 | 84 | describe('.stop()', function () { 85 | it('should stop the task', async function () { 86 | const res = await task.get('test'); 87 | assert.isFalse(res.isStopped, 'check the status before'); 88 | await task.stop(res); 89 | assert.isTrue(res.isStopped, 'check the status after'); 90 | }); 91 | }); 92 | 93 | describe('.run()', function () { 94 | it('should run the task callback', async function () { 95 | let counter = 0; 96 | const res = await task.add('test', 1, () => counter++); 97 | await task.start(res); 98 | await task.run(res); 99 | assert.equal(counter, 1, 'check the function calling'); 100 | }); 101 | }); 102 | 103 | describe('blocking', function () { 104 | it('should block the task', async function () { 105 | let counter = 0; 106 | let interval = 100; 107 | const res = await task.add('test', interval, async () => (await tools.wait(interval), counter++)); 108 | await task.start(res); 109 | await Promise.all([task.run(res), task.run(res)]); 110 | assert.equal(counter, 1, 'check after the first iteration'); 111 | await task.run(res); 112 | assert.equal(counter, 2, 'check after the second iteration'); 113 | }); 114 | }); 115 | 116 | describe('.deinit()', function () { 117 | it('should not throw an exception', async function () { 118 | await task.deinit(); 119 | }); 120 | }); 121 | 122 | describe('reinitialization', () => { 123 | it('should not throw an exception', async function () { 124 | await task.init(); 125 | }); 126 | }); 127 | 128 | describe('.destroy()', function () { 129 | it('should not throw an exception', async function () { 130 | await task.destroy(); 131 | }); 132 | }); 133 | }); 134 | } -------------------------------------------------------------------------------- /test/tools.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import merge from "lodash-es/merge.js"; 3 | import shuffle from "lodash-es/shuffle.js"; 4 | import getPort from "get-port"; 5 | import FormData from "form-data"; 6 | import fse from "fs-extra"; 7 | const tools = {}; 8 | tools.tmpPath = path.join(process.cwd(), 'test/tmp'); 9 | 10 | /** 11 | * Get the database path 12 | * 13 | * @param {number} port 14 | * @returnss {string} 15 | */ 16 | tools.getDbFilePath = function (node) { 17 | return path.join(node.storagePath, 'loki.db'); 18 | }; 19 | 20 | /** 21 | * Create an actual request options 22 | * 23 | * @param {object} [options] 24 | * @returnss {object} 25 | */ 26 | tools.createJsonRequestOptions = function (options = {}) { 27 | let body = options.body; 28 | typeof body == 'object' && (body = JSON.stringify(body)); 29 | return merge({ 30 | method: 'post', 31 | headers: { 32 | 'Content-Type': 'application/json' 33 | } 34 | }, options, { body }); 35 | }; 36 | 37 | /** 38 | * Create a request form data 39 | * 40 | * @param {object} body 41 | * @returns {FormData} 42 | */ 43 | tools.createRequestFormData = function (body) { 44 | const form = new FormData(); 45 | 46 | for (let key in body) { 47 | let val = body[key]; 48 | 49 | if (typeof val == 'object') { 50 | form.append(key, val.value, val.options); 51 | } 52 | else { 53 | form.append(key, val); 54 | } 55 | } 56 | 57 | return form; 58 | }; 59 | 60 | /** 61 | * Create an actual server response 62 | * 63 | * @param {string} address 64 | * @param {object} res 65 | * @returnss {object} 66 | */ 67 | tools.createServerResponse = function (address, res) { 68 | res.address = address; 69 | return res; 70 | }; 71 | 72 | /** 73 | * Save the response to a file 74 | * 75 | * @async 76 | * @param {http.ServerResponse} 77 | */ 78 | tools.saveResponseToFile = async function (response, filePath) { 79 | await new Promise((resolve, reject) => { 80 | try { 81 | const ws = fse.createWriteStream(filePath); 82 | response.body 83 | .on('error', reject) 84 | .pipe(ws) 85 | .on('error', reject) 86 | .on('finish', resolve); 87 | } 88 | catch (err) { 89 | reject(err); 90 | } 91 | }); 92 | }; 93 | 94 | /** 95 | * Get free port 96 | * 97 | * @async 98 | * @returns {number} 99 | */ 100 | tools.getFreePort = async function () { 101 | return await getPort(); 102 | }; 103 | 104 | /** 105 | * Create the node options 106 | * 107 | * @async 108 | * @param {object} [options] 109 | * @returns {object} 110 | */ 111 | tools.createNodeOptions = async function (options = {}) { 112 | const port = options.port || await this.getFreePort(); 113 | return merge({ 114 | port, 115 | task: false, 116 | request: { 117 | pingTimeout: 500, 118 | serverTimeout: 600 119 | }, 120 | network: { 121 | syncInterval: 1000, 122 | autoSync: false, 123 | serverMaxFails: 1 124 | }, 125 | logger: false, 126 | initialNetworkAddress: `localhost:${port}`, 127 | hostname: 'localhost', 128 | storage: { 129 | path: path.join(this.tmpPath, 'node-' + port) 130 | } 131 | }, options); 132 | }; 133 | 134 | /** 135 | * Create the client options 136 | * 137 | * @async 138 | * @param {object} [options] 139 | * @returns {object} 140 | */ 141 | tools.createClientOptions = async function (options = {}) { 142 | return merge({ 143 | logger: false, 144 | task: false 145 | }, options); 146 | }; 147 | 148 | /** 149 | * Wait for the timeout 150 | * 151 | * @async 152 | * @param {number} timeout 153 | */ 154 | tools.wait = async function (timeout) { 155 | return await new Promise(resolve => setTimeout(resolve, timeout)); 156 | }; 157 | 158 | /** 159 | * Sync each node in the list 160 | * 161 | * @async 162 | * @param {object[]} nodes 163 | * @param {number} [count] 164 | */ 165 | tools.nodesSync = async function (nodes, count = 1) { 166 | nodes = shuffle(nodes); 167 | 168 | for (let i = 0; i < count; i++) { 169 | for (let k = 0; k < nodes.length; k++) { 170 | try { 171 | await nodes[k].sync(); 172 | } 173 | catch (err) { 174 | if (['ERR_SPREADABLE_REQUEST_TIMEDOUT'].includes(err.code)) { 175 | throw err; 176 | } 177 | } 178 | } 179 | } 180 | }; 181 | 182 | export default tools; 183 | -------------------------------------------------------------------------------- /webpack.client.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { fileURLToPath } from "url"; 3 | import merge from "lodash-es/merge.js"; 4 | import config from "./webpack.common.js"; 5 | 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 7 | 8 | export default (options = {}, wp) => { 9 | options = merge( 10 | { 11 | name: "client", 12 | include: [], 13 | mock: { 14 | "fs-extra": true, 15 | chalk: true, 16 | ip6addr: true, 17 | "tcp-port-used": true, 18 | "validate-ip-node": true, 19 | crypto: true, 20 | path: true, 21 | stream: true 22 | }, 23 | }, 24 | options 25 | ); 26 | options.include.push([path.resolve(__dirname, "src/browser/client")]); 27 | return wp? config(options, wp) : options; 28 | }; 29 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | import CssMinimizerPlugin from "css-minimizer-webpack-plugin"; 2 | import ESLintPlugin from "eslint-webpack-plugin"; 3 | import fse from "fs-extra"; 4 | import { merge, capitalize } from "lodash-es"; 5 | import MiniCssExtractPlugin from "mini-css-extract-plugin"; 6 | import NodePolyfillPlugin from "node-polyfill-webpack-plugin"; 7 | import path from "path"; 8 | import TerserPlugin from "terser-webpack-plugin"; 9 | import { fileURLToPath } from "url"; 10 | import webpack from "webpack"; 11 | 12 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 13 | 14 | export default (options = {}) => { 15 | const cwd = process.cwd(); 16 | const name = options.name || "build"; 17 | const pack = JSON.parse( 18 | fse.readFileSync( 19 | new URL( 20 | options.packagePath || path.join(cwd, "package.json"), 21 | import.meta.url 22 | ) 23 | ) 24 | ); 25 | pack.name = pack.name.split("-")[0] || pack.name; 26 | const banner = options.banner || `${pack.name} ${name}\n@version ${pack.version}\n{@link ${pack.homepage}}`; 27 | let plugins = []; 28 | plugins.push(new webpack.BannerPlugin({ banner })); 29 | plugins.push(new MiniCssExtractPlugin({ filename: "style.css" })); 30 | plugins.push(new NodePolyfillPlugin()); 31 | plugins.push(new ESLintPlugin({ exclude: ["node_modules", "dist"] })); 32 | plugins = plugins.concat(options.plugins || []); 33 | const mock = merge( 34 | { 35 | https: true, 36 | http: true, 37 | net: true, 38 | tls: true, 39 | os: true, 40 | "fs-extra": true, 41 | fs: true, 42 | dns: true, 43 | }, 44 | options.mock 45 | ); 46 | const include = options.include || []; 47 | const mockIndexPath = options.mockIndexPath || path.resolve(__dirname, "src/browser/mock"); 48 | const isProd = options.isProd === undefined? process.env.NODE_ENV == "production": options.isProd; 49 | const alias = options.alias || {}; 50 | const entry = {}; 51 | const mainEntry = options.entry || path.resolve(cwd, `src/browser/${name}`); 52 | entry[`${pack.name}.${name}`] = mainEntry; 53 | 54 | for (let key in mock) { 55 | const val = mock[key]; 56 | 57 | if (val === false) { 58 | continue; 59 | } 60 | 61 | alias[key] = val === true? mockIndexPath : val; 62 | } 63 | 64 | return merge( 65 | { 66 | mode: isProd? "production" : "development", 67 | performance: { hints: false }, 68 | watch: !isProd, 69 | devtool: isProd? false : "inline-source-map", 70 | entry, 71 | output: { 72 | path: options.distPath || path.join(cwd, `/dist/${name}`), 73 | filename: "[name].js", 74 | library: options.library || capitalize(name) + capitalize(pack.name), 75 | libraryTarget: "umd", 76 | libraryExport: "default", 77 | clean: true, 78 | }, 79 | optimization: { 80 | minimizer: [ 81 | new TerserPlugin({ 82 | extractComments: false, 83 | }), 84 | new CssMinimizerPlugin({ 85 | minimizerOptions: { 86 | preset: [ 87 | "default", 88 | { 89 | mergeRules: false, 90 | }, 91 | ], 92 | }, 93 | }), 94 | ], 95 | }, 96 | plugins, 97 | module: { 98 | rules: [ 99 | { 100 | test: /\.js$/, 101 | loader: "babel-loader", 102 | exclude: /node_modules/, 103 | include, 104 | options: { 105 | configFile: path.join(mainEntry, ".babelrc"), 106 | }, 107 | }, 108 | { 109 | test: /\.html$/, 110 | loader: "html-loader", 111 | options: { 112 | esModule: false, 113 | minimize: { 114 | removeScriptTypeAttributes: false, 115 | }, 116 | }, 117 | }, 118 | { 119 | test: /\.s?css$/, 120 | use: [ 121 | MiniCssExtractPlugin.loader, 122 | "css-loader", 123 | "resolve-url-loader", 124 | { 125 | loader: "sass-loader", 126 | options: { 127 | sourceMap: true, 128 | }, 129 | }, 130 | ], 131 | }, 132 | { 133 | test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/, 134 | type: 'asset/resource', 135 | dependency: { not: ['url'] } 136 | }, 137 | ], 138 | }, 139 | resolve: { 140 | alias 141 | } 142 | }, 143 | options.config 144 | ); 145 | }; 146 | --------------------------------------------------------------------------------