Merge pull request #4 from ranchimall/master
This commit is contained in:
commit
b5d123dfed
6
.gitattributes
vendored
6
.gitattributes
vendored
@ -1,6 +0,0 @@
|
||||
*.gitattributes linguist-vendored
|
||||
util/mongoose.c linguist-vendored
|
||||
util/mongoose.h linguist-vendored
|
||||
|
||||
.gitattributes export-ignore
|
||||
util/* export-ignore
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
/node_modules/
|
||||
/package-lock.json
|
||||
/args/config.json
|
||||
/args/param.json
|
||||
/args/keys.json
|
||||
/log.txt
|
||||
/bash_start*
|
||||
*test*
|
||||
*.tmp*
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 RanchiMall
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
67
README.md
67
README.md
@ -1,23 +1,62 @@
|
||||
|
||||
# SuperNodeStorage
|
||||
FLO Supernode Storage is a Cloud Storage progam for FLO Dapps
|
||||
# SuperNode Storage
|
||||
|
||||
## Installation
|
||||
1. Download or clone the [repo](https://github.com/ranchimall/SuperNodeStorage):
|
||||
|
||||
git clone https://github.com/ranchimall/SuperNodeStorage
|
||||
2. Add a strong <server_password> in `.config` file
|
||||
3. Change other configurations (if needed)
|
||||
4. Host and publish the domain name or IP with port
|
||||
### Pre-requisite
|
||||
- [X] Nodejs `version >= 12.9` (`--lts` recommended)
|
||||
- [X] MySQL Server `version > 8.0`
|
||||
|
||||
## Usage
|
||||
1. Start the app using the following command in terminal. The server WSS will be started and the supernode html-js will be opened in the browser.
|
||||
### Download
|
||||
Download the repository using git:
|
||||
```
|
||||
git clone https://github.com/ranchimall/SuperNodeStorage.git
|
||||
```
|
||||
|
||||
./start_supernode.sh
|
||||
2. (Only for first time login) Enter the <server_password> and <private_key> when prompted
|
||||
### Install
|
||||
Install using npm:
|
||||
```
|
||||
cd SuperNodeStorage
|
||||
npm install
|
||||
```
|
||||
Finish the configuration when prompted
|
||||
|
||||
The Supernode storage will automatically start
|
||||
### Configuration
|
||||
|
||||
NOTE: The <server_password> and <private_key> will be stored securedly in IndexedDB of the browser
|
||||
#### General Configuration
|
||||
If not finished during installation, or to re-configure use:
|
||||
```
|
||||
npm run configure
|
||||
```
|
||||
- **port**: Port of the server to run on
|
||||
- **MySQL host**: Host of the MySQL server (default: ***localhost***)
|
||||
- **Database name**: Database in which the data should be stored (`<database-name>`) (default: ***supernode***)
|
||||
- **MySQL username**: Username for MySQL (`<sql-username>`)
|
||||
- **MySQL password**: Password for MySQL (`<sql-password>`)
|
||||
|
||||
NOTE: Users may add `start_supernode` to bootup process to automatically start the supernode during boot up
|
||||
***Recommended*** *(optional)* Create and use a MySQL user instead of root. Remember to give access to the database to the user.
|
||||
|
||||
#### Set/Reset Node key password
|
||||
If not set during installation, or to reset password, use:
|
||||
```
|
||||
npm run reset-password
|
||||
```
|
||||
- **private key**: Private key of the node
|
||||
- **password**: Password to set for the node (`<password>`)
|
||||
|
||||
**Note**: Private key of the node is encrypted using the `<password>`. Thus use a ***strong*** password.
|
||||
|
||||
### More
|
||||
For help or list of all commands, use
|
||||
```
|
||||
npm run help
|
||||
```
|
||||
|
||||
## Starting the Server
|
||||
After successful installation and configuration using the above steps, SuperNodeStorage can be started using:
|
||||
```
|
||||
npm start -- -PASSWORD=<password>
|
||||
```
|
||||
|
||||
##
|
||||
For more information and detailed installation, check the wiki [here](https://github.com/ranchimall/SuperNodeStorage/wiki).
|
||||
|
||||
9560
app/index.html
9560
app/index.html
File diff suppressed because it is too large
Load Diff
Binary file not shown.
144
args/gen-param.html
Normal file
144
args/gen-param.html
Normal file
@ -0,0 +1,144 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RanchiMall Blockchain Data Cloud</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
body {
|
||||
background-color: #FAF3DD;
|
||||
}
|
||||
#main_header{
|
||||
display: flex;
|
||||
padding: 1rem 1.5rem;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
}
|
||||
#main_header__logo{
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
section{
|
||||
display: grid;
|
||||
place-content: center;
|
||||
padding: 3rem 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
h1{
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #065C6F;
|
||||
}
|
||||
p{
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
.download-icon{
|
||||
height: 2.6rem;
|
||||
width: 2.6rem;
|
||||
padding: 1rem;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 0.5rem;
|
||||
fill: teal;
|
||||
background-color: rgba(0,0,0, 0.1);
|
||||
}
|
||||
h3{
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
strong{
|
||||
display: flex;
|
||||
padding: 0.6rem 0.8rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #FFD369;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
a{
|
||||
padding: 0.3rem 0.5rem;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
background-color: rgba(0,0,0, 0.1);
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header id="main_header">
|
||||
<svg id="main_header__logo" class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20.46,21.32C20,19.78,18.6,18.59,15.3,17a12.67,12.67,0,0,1-2.64-1.56,4.27,4.27,0,0,1-.79-1,2.6,2.6,0,0,1,0-1.41c.24-.68.49-1,2.43-2.85a7.18,7.18,0,0,0,2.09-2.92,4.25,4.25,0,0,0,0-1.77,6.52,6.52,0,0,0-2.85-3.11c-.56-.36-.81-.4-.81-.15a2.33,2.33,0,0,1-.18.45L12.4,3l-.53-.36c-.28-.21-.64-.41-.77-.49s-.46-.11-.46,0a6.21,6.21,0,0,1-.37.83s-.08,0-.17-.08c-1.15-.83-1.64-1-1.64-.73A7.33,7.33,0,0,1,7.7,3.65C6.48,5.68,5.24,6.7,4,6.7c-.56,0-.54,0-.37.64s.2.58.68.43a3.37,3.37,0,0,0,1.09-.54.86.86,0,0,1,.3-.17,1.34,1.34,0,0,1,.13.39.79.79,0,0,0,.17.4A3.5,3.5,0,0,0,7.37,7.3L7.8,7l.09.34c.12.45.19.51.62.39a4.25,4.25,0,0,0,2.17-1.54l.38-.45,0,.39A5.92,5.92,0,0,1,8.89,9.54L7.67,10.71c-2,1.93-1.89,3.51.37,5a27.41,27.41,0,0,0,2.89,1.51c.17.07.62.32,1,.54C14,19,15,20.23,15,21.48a2,2,0,0,0,0,.49h0c0,.05,0,.05.56-.1a1.89,1.89,0,0,0,.53-.21,2.41,2.41,0,0,0-.34-1.15,7.05,7.05,0,0,0-1.68-1.77,21.91,21.91,0,0,0-3.2-1.83A9.53,9.53,0,0,1,8.16,15.2a2.18,2.18,0,0,1-.74-1.55C7.42,12.79,7.86,12,9,11c1.77-1.64,2.45-2.45,2.92-3.55a2.28,2.28,0,0,0,.26-1.26A2,2,0,0,0,12,5.06l-.2-.45L12,4.3l.28-.49.09-.18L12.6,4a3.69,3.69,0,0,1,.61,1.76A3.47,3.47,0,0,1,12.94,7l-.09.25s-.21.37-.41.69A17.78,17.78,0,0,1,9.91,10.6c-1.07,1-1.43,1.62-1.47,2.47a2.05,2.05,0,0,0,.7,1.73,10.47,10.47,0,0,0,3.28,2.08c2.28,1.13,3.26,1.81,4,2.73a2.94,2.94,0,0,1,.74,1.75,1.26,1.26,0,0,0,.09.57.48.48,0,0,0,.26,0l.51-.13.29-.08,0-.28c-.13-1-1-2-2.47-3a25.52,25.52,0,0,0-3.26-1.77,8.59,8.59,0,0,1-2.23-1.43,2.09,2.09,0,0,1-.5-2.62c.26-.53.5-.83,2.35-2.6,1.51-1.45,2.15-2.58,2.15-3.79A3.67,3.67,0,0,0,13,3.48a3,3,0,0,1-.4-.42A1.85,1.85,0,0,1,13,2.33a6.74,6.74,0,0,1,1.83,1.73,2.62,2.62,0,0,1,.47,1.68,3,3,0,0,1-.55,1.84c-.45.78-.79,1.14-2.67,2.93a5.56,5.56,0,0,0-1.3,1.64,1.77,1.77,0,0,0-.21,1,1.76,1.76,0,0,0,.19.92,6.28,6.28,0,0,0,2.9,2.34,21.6,21.6,0,0,1,3.66,2c1.35,1,2,2,2,3a1.06,1.06,0,0,0,.05.47,2.83,2.83,0,0,0,1-.24C20.56,21.68,20.56,21.66,20.46,21.32ZM7.29,6.4h0a2.23,2.23,0,0,1-.9.28L6,6.72l.43-.53a15.22,15.22,0,0,0,1.89-3,3.52,3.52,0,0,1,.38-.67c.07-.08.49.2,1,.64l.39.35L9.66,4A6.7,6.7,0,0,1,7.29,6.4Zm3.58-1.11A5.8,5.8,0,0,1,9.25,6.51h0a3.3,3.3,0,0,1-.74.17l-.35,0,.39-.49a15.64,15.64,0,0,0,1.32-2,4.63,4.63,0,0,1,.28-.49c.06-.08.33.26.57.77l.28.57Zm1-1.4a1.63,1.63,0,0,1-.28.4A6.63,6.63,0,0,1,11,3.72l-.53-.56.12-.29c.2-.49.24-.51.64-.19a5.57,5.57,0,0,1,.85.78A2.78,2.78,0,0,1,11.87,3.89Z"/></svg>
|
||||
<h4>RanchiMall Blockchain Data Cloud</h4>
|
||||
</header>
|
||||
<section>
|
||||
<h1>Generate Parameters</h1>
|
||||
<p>This page will automatically generate param.json file</p>
|
||||
<div>
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
(function (fileName) {
|
||||
const status = document.getElementById("status");
|
||||
//Generate param
|
||||
var param = {
|
||||
screen: {
|
||||
height: screen.height,
|
||||
width: screen.width,
|
||||
colorDepth: screen.colorDepth,
|
||||
availHeight: screen.availHeight,
|
||||
availWidth: screen.availWidth,
|
||||
pixelDepth: screen.pixelDepth
|
||||
},
|
||||
navigator: {
|
||||
userAgent: navigator.userAgent,
|
||||
plugins: [],
|
||||
mimeTypes: [],
|
||||
cookieEnabled: navigator.cookieEnabled,
|
||||
language: navigator.language
|
||||
},
|
||||
history: {
|
||||
length: history.length
|
||||
},
|
||||
location: location.toString()
|
||||
};
|
||||
for (var i = 0; i < navigator.plugins.length; i++)
|
||||
param.navigator.plugins[i] = {
|
||||
name: navigator.plugins[i].name,
|
||||
filename: navigator.plugins[i].filename,
|
||||
description: navigator.plugins[i].description,
|
||||
version: navigator.plugins[i].version
|
||||
};
|
||||
for (var i = 0; i < navigator.mimeTypes.length; i++)
|
||||
param.navigator.mimeTypes[i] = {
|
||||
description: navigator.mimeTypes[i].description,
|
||||
type: navigator.mimeTypes[i].type,
|
||||
suffixes: navigator.mimeTypes[i].suffixes
|
||||
};
|
||||
|
||||
console.log(param);
|
||||
status.innerHTML += `
|
||||
<svg class="icon download-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M13 12h3l-4 4-4-4h3V8h2v4zm2-8H5v16h14V8h-4V4zM3 2.992C3 2.444 3.447 2 3.999 2H16l5 5v13.993A1 1 0 0 1 20.007 22H3.993A1 1 0 0 1 3 21.008V2.992z"/></svg>
|
||||
<h3>Downloading file...</h3>
|
||||
<br/>If the download does't start automatically,
|
||||
`
|
||||
//Download the file
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = window.URL.createObjectURL(new Blob([JSON.stringify(param)], {
|
||||
type: "octet/stream"
|
||||
}));
|
||||
anchor.textContent = "click here";
|
||||
anchor.download = fileName;
|
||||
status.appendChild(anchor);
|
||||
// anchor.click();
|
||||
status.innerHTML += `<strong>Note: Save "param.json" file in SuperNodeStorage directory</strong>`;
|
||||
})("param.json");
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
37
args/param-default.json
Normal file
37
args/param-default.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"screen": {
|
||||
"height": 1160,
|
||||
"width": 2000,
|
||||
"colorDepth": 24,
|
||||
"availHeight": 1080,
|
||||
"availWidth": 1920,
|
||||
"pixelDepth": 24
|
||||
},
|
||||
"navigator": {
|
||||
"userAgent": "Node/14.17.3 (Linux; aarch64; arm)",
|
||||
"plugins": [{
|
||||
"name": "MySQL",
|
||||
"filename": "mysql",
|
||||
"description": "A node.js driver for mysql. It is written in JavaScript, does not require compiling, and is 100% MIT licensed."
|
||||
}, {
|
||||
"name": "WebSocket",
|
||||
"filename": "ws",
|
||||
"description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js"
|
||||
}, {
|
||||
"name": "Node fetch",
|
||||
"filename": "node-fetch",
|
||||
"description": "A light-weight module that brings window.fetch to node.js"
|
||||
}],
|
||||
"mimeTypes": [{
|
||||
"description": "",
|
||||
"type": "application/pdf",
|
||||
"suffixes": "pdf"
|
||||
}],
|
||||
"cookieEnabled": true,
|
||||
"language": "en-US"
|
||||
},
|
||||
"history": {
|
||||
"length": 512
|
||||
},
|
||||
"location": "protocol://subdomain.example.domain/path"
|
||||
}
|
||||
29
debug/checksum-db.js
Normal file
29
debug/checksum-db.js
Normal file
@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
const Database = require('../src/database');
|
||||
|
||||
function CheckDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const config = require(`../args/config.json`);
|
||||
Database.init(config["sql_user"], config["sql_pwd"], config["sql_db"], config["sql_host"]).then(result => {
|
||||
let ds = require("../src/data_structure.json");
|
||||
let cols = [];
|
||||
for (let s in ds)
|
||||
for (let c in ds[s])
|
||||
cols.push(ds[s][c]);
|
||||
let crc_ = "SUM(" + cols.map(c => `IFNULL(CRC32(${c}), 0)`).join('+') + ")";
|
||||
Database.DB.listTable().then(disks => {
|
||||
let promises = disks.map(d => Database.query(`SELECT ? AS disk, COUNT(*) AS total_rec, ${crc_} AS crc_checksum FROM _${d}`, d));
|
||||
Promise.allSettled(promises).then(results => {
|
||||
let records = results.filter(r => r.status === "fulfilled").map(r => r.value[0]);
|
||||
console.table(records);
|
||||
let errors = results.filter(r => r.status === "rejected");
|
||||
if (errors.length)
|
||||
console.error(errors.map(r => r.reason));
|
||||
resolve(true);
|
||||
})
|
||||
}).catch(error => reject(error));
|
||||
}).catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
|
||||
CheckDB().then(_ => process.exit(0)).catch(error => { console.error(error); process.exit(1); })
|
||||
@ -1,2 +0,0 @@
|
||||
All logs are stored in this directory
|
||||
NOTE: DO NOT delete this directory
|
||||
37
package.json
Normal file
37
package.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "supernodestorage",
|
||||
"version": "1.0.0",
|
||||
"description": "Supernode Storage is a Cloud Storage program for FLO Dapps",
|
||||
"main": "src/main.js",
|
||||
"dependencies": {
|
||||
"mysql": "^2.18.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
"ws": "^7.5.0"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"postinstall": "node setup/post-install.js",
|
||||
"help": "node setup/help.js",
|
||||
"setup": "node setup/post-install.js",
|
||||
"configure": "node setup/configure-settings.js",
|
||||
"reset-password": "node setup/reset-password.js",
|
||||
"checksum-db": "node debug/checksum-db.js",
|
||||
"start": "node start.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ranchimall/SuperNodeStorage.git"
|
||||
},
|
||||
"keywords": [
|
||||
"FLO",
|
||||
"supernode",
|
||||
"cloud",
|
||||
"storage"
|
||||
],
|
||||
"author": "Sai Raj (https://github.com/sairajzero)",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ranchimall/SuperNodeStorage/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ranchimall/SuperNodeStorage#readme"
|
||||
}
|
||||
85
setup/configure-settings.js
Normal file
85
setup/configure-settings.js
Normal file
@ -0,0 +1,85 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const getInput = require('./getInput');
|
||||
|
||||
var config, flag_new;
|
||||
try {
|
||||
config = require(`../args/config.json`);
|
||||
flag_new = false;
|
||||
} catch (error) {
|
||||
config = {
|
||||
"port": "8080",
|
||||
|
||||
"sql_user": "mySQL_user",
|
||||
"sql_pwd": "mySQL_password",
|
||||
"sql_db": "supernode",
|
||||
"sql_host": "localhost"
|
||||
};
|
||||
flag_new = true;
|
||||
}
|
||||
|
||||
function flaggedYesOrNo(text) {
|
||||
return new Promise((resolve) => {
|
||||
if (flag_new)
|
||||
resolve(true);
|
||||
else
|
||||
getInput.YesOrNo(text)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function configurePort() {
|
||||
return new Promise(resolve => {
|
||||
getInput.Text('Enter port', config["port"]).then(port => {
|
||||
config["port"] = port;
|
||||
resolve(true);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function configureSQL() {
|
||||
return new Promise(resolve => {
|
||||
flaggedYesOrNo('Do you want to re-configure mySQL connection').then(value => {
|
||||
if (value) {
|
||||
console.log('Enter mySQL connection values: ')
|
||||
getInput.Text('MySQL host', config['sql_host']).then(host => {
|
||||
config['sql_host'] = host;
|
||||
getInput.Text('Database name', config['sql_db']).then(dbname => {
|
||||
config['sql_db'] = dbname;
|
||||
getInput.Text('MySQL username', config['sql_user']).then(sql_user => {
|
||||
config['sql_user'] = sql_user;
|
||||
getInput.Text('MySQL password', config['sql_pwd']).then(sql_pwd => {
|
||||
config['sql_pwd'] = sql_pwd;
|
||||
resolve(true);
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
} else
|
||||
resolve(false);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function configure() {
|
||||
return new Promise((resolve, reject) => {
|
||||
configurePort().then(port_result => {
|
||||
configureSQL().then(sql_result => {
|
||||
fs.writeFile(path.resolve(__dirname, '..', 'args', `config.json`), JSON.stringify(config), 'utf8', (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return reject(false);
|
||||
}
|
||||
console.log('Configuration successful!');
|
||||
resolve(true);
|
||||
})
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
if (!module.parent)
|
||||
configure().then(_ => null).catch(error => console.error(error)).finally(_ => process.exit(0));
|
||||
else
|
||||
module.exports = configure;
|
||||
42
setup/getInput.js
Normal file
42
setup/getInput.js
Normal file
@ -0,0 +1,42 @@
|
||||
const readline = require('readline');
|
||||
|
||||
const getInput = {
|
||||
Text: function(text, current = null) {
|
||||
return new Promise((resolve) => {
|
||||
let r = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
r.question(`${text} :` + (current ? `(${current})` : ''), value => {
|
||||
r.close();
|
||||
value = value || current;
|
||||
if (value === null) {
|
||||
console.log("Please enter a value!");
|
||||
this.Text(text, current).then(result => resolve(result));
|
||||
} else
|
||||
resolve(value);
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
YesOrNo: function(text, def_value = "YES") {
|
||||
return new Promise((resolve) => {
|
||||
let r = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
r.question(`${text}? [YES/NO] : (${def_value})`, value => {
|
||||
r.close();
|
||||
value = (value || def_value).toLowerCase();
|
||||
value = ['yes', 'y'].includes(value) ? true : ['no', 'n'].includes(value) ? false : null;
|
||||
if (value === null) {
|
||||
console.log("Please enter a valid value!");
|
||||
this.YesOrNo(text, def_value).then(result => resolve(result));
|
||||
} else
|
||||
resolve(value);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = getInput;
|
||||
30
setup/help.js
Normal file
30
setup/help.js
Normal file
@ -0,0 +1,30 @@
|
||||
let message = `
|
||||
SuperNode Storage
|
||||
=================
|
||||
|
||||
Setup
|
||||
-----
|
||||
npm install - Install the app and node modules.
|
||||
npm run help - List all commands.
|
||||
npm run setup - Finish the setup (configure and reset password).
|
||||
npm run configure - Configure the app.
|
||||
npm run reset-password - Reset the password (for private-key).
|
||||
|
||||
(Optional) Open args/gen-param.html and Download param.json to args/ directory
|
||||
|
||||
Start Node
|
||||
----------
|
||||
npm start - Start the application (main).
|
||||
|
||||
NOTE: argument 'PASSWORD' required for 'npm start'
|
||||
npm start -- -PASSWORD=<password>
|
||||
|
||||
(Optional) 'console.debug' is now turned off by default. pass argument '--debug' to turn it on
|
||||
npm start -- -PASSWORD=<password> --debug
|
||||
|
||||
Debugging
|
||||
---------
|
||||
npm run checksum-db - List the stored node tables and checksum
|
||||
`;
|
||||
|
||||
console.log(message);
|
||||
38
setup/post-install.js
Normal file
38
setup/post-install.js
Normal file
@ -0,0 +1,38 @@
|
||||
const getInput = require("./getInput");
|
||||
|
||||
let message = `
|
||||
SuperNode Storage is installed
|
||||
To list all commands, use:
|
||||
npm run help
|
||||
`;
|
||||
console.log(message);
|
||||
|
||||
getInput.YesOrNo('Do you want to finish the setup now').then(value => {
|
||||
if (value) {
|
||||
let configureSettings = require('./configure-settings');
|
||||
configureSettings()
|
||||
.then(_ => console.log('To Re-configure, use:'))
|
||||
.catch(_ => console.log('Finish the configuration later using: '))
|
||||
.finally(_ => {
|
||||
console.log('npm run configure');
|
||||
getInput.YesOrNo('Do you want to Reset password for private key now').then(value => {
|
||||
if (value) {
|
||||
let resetPassword = require('./reset-password');
|
||||
resetPassword()
|
||||
.then(_ => console.log('To reset the password again, use: '))
|
||||
.catch(_ => console.log('Reset the password later using: '))
|
||||
.finally(_ => {
|
||||
console.log('npm run reset-password');
|
||||
process.exit(0);
|
||||
})
|
||||
} else {
|
||||
console.log('Reset the password later using:\n' + 'npm run reset-password');
|
||||
process.exit(0);
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
console.log('Finish the setup later using:\n' + 'npm run setup');
|
||||
process.exit(0);
|
||||
}
|
||||
})
|
||||
87
setup/reset-password.js
Normal file
87
setup/reset-password.js
Normal file
@ -0,0 +1,87 @@
|
||||
const fs = require('fs');
|
||||
const getInput = require('./getInput');
|
||||
|
||||
global.floGlobals = require('../src/floGlobals');
|
||||
require('../src/set_globals');
|
||||
require('../src/lib');
|
||||
const floCrypto = require('../src/floCrypto');
|
||||
const path = require('path');
|
||||
|
||||
function validateKey(privKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if (!privKey || privKey === "")
|
||||
throw 'Private Key cannot be empty!';
|
||||
let floID = floCrypto.getFloID(privKey);
|
||||
if (!floID || !floCrypto.verifyPrivKey(privKey, floID))
|
||||
throw 'Invalid Private Key!';
|
||||
return resolve(privKey);
|
||||
} catch (error) {
|
||||
getInput.Text(error + ' Re-Enter: (Cancel)', 'Cancel').then(value => {
|
||||
if (value === 'Cancel')
|
||||
return reject(true);
|
||||
validateKey(value)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getPassword() {
|
||||
return new Promise((resolve, reject) => {
|
||||
getInput.Text(`Enter a password [Minimum 8 characters]`, 'Cancel').then(value1 => {
|
||||
if (value1 === 'Cancel')
|
||||
return reject(true);
|
||||
else if (value1.length < 8) {
|
||||
console.log('Password length must be minimum of 8 characters');
|
||||
getPassword()
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
} else {
|
||||
getInput.Text(`Re-enter password`).then(value2 => {
|
||||
if (value1 !== value2) {
|
||||
console.log('Passwords doesnot match! Try again.');
|
||||
getPassword()
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
} else
|
||||
resolve(value1);
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function resetPassword() {
|
||||
return new Promise((resolve, reject) => {
|
||||
getInput.Text(`Enter private key`).then(value => {
|
||||
validateKey(value).then(privKey => {
|
||||
getPassword().then(password => {
|
||||
let encrypted = Crypto.AES.encrypt(privKey, password);
|
||||
let randNum = floCrypto.randInt(10, 15);
|
||||
let splitShares = floCrypto.createShamirsSecretShares(encrypted, randNum, randNum);
|
||||
fs.writeFile(path.resolve(__dirname, '..', 'args', `keys.json`), JSON.stringify(splitShares), 'utf8', (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return reject(false);
|
||||
}
|
||||
console.log('Password reset successful!');
|
||||
resolve(true);
|
||||
})
|
||||
}).catch(error => {
|
||||
console.log('Password reset cancelled!');
|
||||
reject(true);
|
||||
})
|
||||
}).catch(error => {
|
||||
console.log('Password reset cancelled!');
|
||||
reject(true);
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (!module.parent)
|
||||
resetPassword().then(_ => null).catch(_ => null).finally(_ => process.exit(0));
|
||||
else
|
||||
module.exports = resetPassword;
|
||||
19
src/_constants.js
Normal file
19
src/_constants.js
Normal file
@ -0,0 +1,19 @@
|
||||
const INVALID = function (message) {
|
||||
if (!(this instanceof INVALID))
|
||||
return new INVALID(message);
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
INVALID,
|
||||
INVALID_E_CODE: 400,
|
||||
INTERNAL_E_CODE: 500,
|
||||
|
||||
INTERVAL_REFRESH_TIME: 1 * 60 * 60 * 1000, //1 hr
|
||||
|
||||
backup: {
|
||||
RETRY_TIMEOUT: 5 * 60 * 1000, //5 mins
|
||||
MIGRATE_WAIT_DELAY: 5 * 60 * 1000, //5 mins
|
||||
SYNC_WAIT_TIME: 5 * 60 * 1000 //5 mins
|
||||
}
|
||||
}
|
||||
550
src/backup/intra.js
Normal file
550
src/backup/intra.js
Normal file
@ -0,0 +1,550 @@
|
||||
'use strict';
|
||||
const WebSocket = require('ws');
|
||||
const { DB } = require('../database');
|
||||
const keys = require('../keys');
|
||||
const TYPE_ = require('./message_types.json');
|
||||
const { _list, packet_, _nextNode, _prevNode, SUPERNODE_INDICATOR } = require("./values");
|
||||
const sync = require('./sync');
|
||||
const { RETRY_TIMEOUT, MIGRATE_WAIT_DELAY } = require('../_constants')['backup'];
|
||||
|
||||
var refresher; //container for and refresher
|
||||
|
||||
//-----NODE CONNECTORS (WEBSOCKET)-----
|
||||
|
||||
//Connect to Node websocket
|
||||
function connectToNode(snID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!(snID in floGlobals.supernodes))
|
||||
return reject(`${snID} is not a supernode`);
|
||||
const ws = new WebSocket("wss://" + floGlobals.supernodes[snID].uri + "/");
|
||||
ws.on("error", () => reject(`${snID} is offline`));
|
||||
ws.on('open', () => resolve(ws));
|
||||
});
|
||||
};
|
||||
|
||||
//Connect to Node websocket thats online
|
||||
function connectToActiveNode(snID, reverse = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!(snID in floGlobals.supernodes))
|
||||
return reject(`${snID} is not a supernode`);
|
||||
if (snID === keys.node_id)
|
||||
return reject(`Reached end of circle. Next node avaiable is self`);
|
||||
connectToNode(snID)
|
||||
.then(ws => resolve(ws))
|
||||
.catch(error => {
|
||||
var next = (reverse ? cloud.prevNode(snID) : cloud.nextNode(snID));
|
||||
connectToActiveNode(next, reverse)
|
||||
.then(ws => resolve(ws))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
//Connect to next available node
|
||||
function connectToNextNode(curNode = keys.node_id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (curNode === keys.node_id && !(keys.node_id in floGlobals.supernodes))
|
||||
return reject(`This (${keys.node_id}) is not a supernode`);
|
||||
let nextNodeID = cloud.nextNode(curNode);
|
||||
if (nextNodeID === keys.node_id)
|
||||
return reject("No other node online");
|
||||
connectToNode(nextNodeID).then(ws => {
|
||||
_nextNode.set(nextNodeID, ws);
|
||||
_nextNode.send(packet_.construct({
|
||||
type_: TYPE_.BACKUP_HANDSHAKE_INIT
|
||||
}));
|
||||
resolve("BACKUP_HANDSHAKE_INIT: " + nextNodeID);
|
||||
}).catch(error => {
|
||||
connectToNextNode(nextNodeID)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function connectToAliveNodes(nodes = null) {
|
||||
if (!Array.isArray(nodes)) nodes = Object.keys(floGlobals.supernodes);
|
||||
nodes = nodes.filter(n => n !== keys.node_id);
|
||||
return new Promise((resolve, reject) => {
|
||||
Promise.allSettled(nodes.map(n => connectToNode(n))).then(results => {
|
||||
let ws_connections = {};
|
||||
nodes.forEach((n, i) =>
|
||||
ws_connections[n] = (results[i].status === "fulfilled") ? results[i].value : null);
|
||||
resolve(ws_connections);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
//Connect to all given nodes [Array] (Default: All super-nodes)
|
||||
function connectToAllActiveNodes(nodes = null) {
|
||||
if (!Array.isArray(nodes)) nodes = Object.keys(floGlobals.supernodes);
|
||||
return new Promise((resolve, reject) => {
|
||||
Promise.allSettled(nodes.map(n => connectToActiveNode(n))).then(results => {
|
||||
let ws_connections = {};
|
||||
nodes.forEach((n, i) =>
|
||||
ws_connections[n] = (results[i].status === "fulfilled") ? results[i].value : null);
|
||||
resolve(ws_connections);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
//-----PROCESS TASKS-----
|
||||
|
||||
_nextNode.onmessage = evt => processTaskFromNextNode(evt.data);
|
||||
_nextNode.onclose = evt => reconnectNextNode();
|
||||
|
||||
//Tasks from next-node
|
||||
function processTaskFromNextNode(packet) {
|
||||
console.debug("_nextNode: ", packet);
|
||||
var {
|
||||
from,
|
||||
message
|
||||
} = packet_.parse(packet);
|
||||
if (message) {
|
||||
message.forEach(task => {
|
||||
switch (task.type_) {
|
||||
case TYPE_.RECONNECT_NEXT_NODE: //Triggered when a node inbetween is available
|
||||
reconnectNextNode(); break;
|
||||
case TYPE_.BACKUP_HANDSHAKE_END:
|
||||
handshakeEnd(); break;
|
||||
case TYPE_.REQ_HASH:
|
||||
sync.sendBlockHashes(task.node_i, _nextNode); break;
|
||||
case TYPE_.RES_HASH:
|
||||
sync.checkBlockHash(task.node_i, task.block_n, task.hash); break;
|
||||
case TYPE_.VAL_LAST_BLK:
|
||||
sync.setLastBlock(task.node_i, task.block_n); break;
|
||||
case TYPE_.REQ_BLOCK:
|
||||
sync.sendBlockData(task.node_i, task.block_n, _nextNode); break;
|
||||
case TYPE_.INDICATE_BLK:
|
||||
sync.syncIndicator(task.node_i, task.block_n, task.status, from); break;
|
||||
case TYPE_.STORE_BACKUP_DATA:
|
||||
storeBackupData(task.data, from, packet); break;
|
||||
default:
|
||||
console.warn("Invalid task type_:" + task.type_ + "from next-node");
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
_prevNode.onmessage = evt => processTaskFromPrevNode(evt.data);
|
||||
_prevNode.onclose = evt => _prevNode.close();
|
||||
|
||||
//Tasks from prev-node
|
||||
function processTaskFromPrevNode(packet) {
|
||||
console.debug("_prevNode: ", packet);
|
||||
var {
|
||||
from,
|
||||
message
|
||||
} = packet_.parse(packet);
|
||||
if (message) {
|
||||
message.forEach(task => {
|
||||
switch (task.type_) {
|
||||
case TYPE_.ORDER_BACKUP:
|
||||
orderBackup(task.order); break;
|
||||
case TYPE_.STORE_BACKUP_DATA:
|
||||
storeBackupData(task.data, from, packet); break;
|
||||
case TYPE_.TAG_BACKUP_DATA:
|
||||
tagBackupData(task.data, from, packet); break;
|
||||
case TYPE_.NOTE_BACKUP_DATA:
|
||||
noteBackupData(task.data, from, packet); break;
|
||||
case TYPE_.REQ_HASH:
|
||||
sync.sendBlockHashes(task.node_i, _nextNode); break;
|
||||
case TYPE_.RES_HASH:
|
||||
sync.checkBlockHash(task.node_i, task.block_n, task.hash); break;
|
||||
case TYPE_.VAL_LAST_BLK:
|
||||
sync.setLastBlock(task.node_i, task.block_n); break;
|
||||
case TYPE_.REQ_BLOCK:
|
||||
sync.sendBlockData(task.node_i, task.block_n, _nextNode); break;
|
||||
case TYPE_.INDICATE_BLK:
|
||||
sync.syncIndicator(task.node_i, task.block_n, task.status, from); break;
|
||||
case TYPE_.DELETE_MIGRATED_DATA:
|
||||
deleteMigratedData(task.data, from, packet); break;
|
||||
default:
|
||||
console.warn("Invalid task type_:" + task.type_ + "from prev-node");
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
//Tasks from any supernode
|
||||
function processTaskFromSupernode(packet, ws) {
|
||||
console.debug("superNode: ", packet);
|
||||
var {
|
||||
from,
|
||||
message
|
||||
} = packet_.parse(packet);
|
||||
if (message) {
|
||||
message.forEach(task => {
|
||||
switch (task.type_) {
|
||||
case TYPE_.BACKUP_HANDSHAKE_INIT:
|
||||
handshakeMid(from, ws); break;
|
||||
case TYPE_.STORE_MIGRATED_DATA:
|
||||
storeMigratedData(task.data); break;
|
||||
case TYPE_.INITIATE_REFRESH:
|
||||
initiateRefresh(); break;
|
||||
default:
|
||||
console.warn("Invalid task type_:" + task.type_ + "from super-node");
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
//-----HANDSHAKE PROCESS-----
|
||||
|
||||
//Acknowledge handshake
|
||||
function handshakeMid(id, ws) {
|
||||
if (_prevNode.id && _prevNode.id in floGlobals.supernodes) {
|
||||
if (cloud.innerNodes(_prevNode.id, keys.node_id).includes(id)) {
|
||||
//close existing prev-node connection
|
||||
_prevNode.send(packet_.construct({
|
||||
type_: TYPE_.RECONNECT_NEXT_NODE
|
||||
}));
|
||||
_prevNode.close();
|
||||
//set the new prev-node connection
|
||||
_prevNode.set(id, ws);
|
||||
_prevNode.send(packet_.construct({
|
||||
type_: TYPE_.BACKUP_HANDSHAKE_END
|
||||
}));
|
||||
} else {
|
||||
//Incorrect order, existing prev-node is already after the incoming node
|
||||
ws.send(packet_.construct({
|
||||
type_: TYPE_.RECONNECT_NEXT_NODE
|
||||
}));
|
||||
return;
|
||||
};
|
||||
} else {
|
||||
//set the new prev-node connection
|
||||
_prevNode.set(id, ws);
|
||||
_prevNode.send(packet_.construct({
|
||||
type_: TYPE_.BACKUP_HANDSHAKE_END
|
||||
}));
|
||||
};
|
||||
if (!_nextNode.id)
|
||||
reconnectNextNode();
|
||||
//Reorder storelist
|
||||
let nodes_inbtw = cloud.innerNodes(_prevNode.id, keys.node_id).concat(keys.node_id),
|
||||
req_sync = [],
|
||||
new_order = [];
|
||||
nodes_inbtw.forEach(n => {
|
||||
switch (_list[n]) {
|
||||
case 0: //Do nothing
|
||||
break;
|
||||
case undefined:
|
||||
req_sync.push(n);
|
||||
default:
|
||||
_list[n] = 0;
|
||||
new_order.push(n);
|
||||
};
|
||||
});
|
||||
if (req_sync.length)
|
||||
req_sync.forEach(node_i => sync.requestDataSync(node_i, _nextNode));
|
||||
if (new_order.length) {
|
||||
let synced_list = new_order.filter(n => !req_sync.includes(n)); //send order only for synced disks
|
||||
_nextNode.send(packet_.construct({
|
||||
type_: TYPE_.ORDER_BACKUP,
|
||||
order: _list.get(synced_list)
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
//Complete handshake
|
||||
function handshakeEnd() {
|
||||
console.log("Backup connected: " + _nextNode.id);
|
||||
_nextNode.send(packet_.construct({
|
||||
type_: TYPE_.ORDER_BACKUP,
|
||||
order: _list.get()
|
||||
}));
|
||||
};
|
||||
|
||||
//Reconnect to next available node
|
||||
function reconnectNextNode() {
|
||||
if (_nextNode.id)
|
||||
_nextNode.close();
|
||||
connectToNextNode()
|
||||
.then(result => console.debug(result))
|
||||
.catch(error => {
|
||||
//Case: No other node is online
|
||||
console.info(error);
|
||||
//Serve all nodes
|
||||
for (let sn in floGlobals.supernodes)
|
||||
DB.createTable(sn)
|
||||
.then(result => _list[sn] = 0)
|
||||
.catch(error => console.error(error));
|
||||
});
|
||||
};
|
||||
|
||||
//-----BACKUP TASKS-----
|
||||
|
||||
//Order the stored backup
|
||||
function orderBackup(order) {
|
||||
let new_order = [],
|
||||
req_sync = [];
|
||||
let cur_serve = cloud.innerNodes(_prevNode.id, keys.node_id).concat(keys.node_id);
|
||||
for (let n in order) {
|
||||
if (!cur_serve.includes(n) && order[n] + 1 !== _list[n] && n in floGlobals.supernodes) {
|
||||
if (order[n] >= floGlobals.sn_config.backupDepth)
|
||||
DB.dropTable(n).then(_ => null)
|
||||
.catch(error => console.error(error))
|
||||
.finally(_ => _list.delete(n));
|
||||
else if (order[n] >= 0) {
|
||||
if (_list[n] === undefined)
|
||||
req_sync.push(n);
|
||||
_list[n] = order[n] + 1;
|
||||
new_order.push(n);
|
||||
};
|
||||
};
|
||||
};
|
||||
if (req_sync.length)
|
||||
req_sync.forEach(node_i => sync.requestDataSync(node_i, _prevNode));
|
||||
if (new_order.length) {
|
||||
let synced_list = new_order.filter(n => !req_sync.includes(n)); //send order only for synced disks
|
||||
_nextNode.send(packet_.construct({
|
||||
type_: TYPE_.ORDER_BACKUP,
|
||||
order: _list.get(synced_list)
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
//Store (backup) data
|
||||
function storeBackupData(data, from, packet) {
|
||||
let closestNode = cloud.closestNode(data.receiverID);
|
||||
if (_list.stored.includes(closestNode)) {
|
||||
DB.storeData(closestNode, data).then(_ => null).catch(e => console.error(e));
|
||||
if (_list[closestNode] < floGlobals.sn_config.backupDepth && _nextNode.id !== from)
|
||||
_nextNode.send(packet);
|
||||
};
|
||||
};
|
||||
|
||||
//Tag (backup) data
|
||||
function tagBackupData(data, from, packet) {
|
||||
let closestNode = cloud.closestNode(data.receiverID);
|
||||
if (_list.stored.includes(closestNode)) {
|
||||
DB.storeTag(closestNode, data).then(_ => null).catch(e => console.error(e));
|
||||
if (_list[closestNode] < floGlobals.sn_config.backupDepth && _nextNode.id !== from)
|
||||
_nextNode.send(packet);
|
||||
};
|
||||
};
|
||||
|
||||
//Note (backup) data
|
||||
function noteBackupData(data, from, packet) {
|
||||
let closestNode = cloud.closestNode(data.receiverID);
|
||||
if (_list.stored.includes(closestNode)) {
|
||||
DB.storeNote(closestNode, data).then(_ => null).catch(e => console.error(e));
|
||||
if (_list[closestNode] < floGlobals.sn_config.backupDepth && _nextNode.id !== from)
|
||||
_nextNode.send(packet);
|
||||
};
|
||||
};
|
||||
|
||||
//Store (migrated) data
|
||||
function storeMigratedData(data) {
|
||||
let closestNode = cloud.closestNode(data.receiverID);
|
||||
if (_list.serving.includes(closestNode)) {
|
||||
DB.storeData(closestNode, data, true).then(_ => null).catch(e => console.error(e));
|
||||
_nextNode.send(packet_.construct({
|
||||
type_: TYPE_.STORE_BACKUP_DATA,
|
||||
data: data
|
||||
}));
|
||||
};
|
||||
};
|
||||
|
||||
//Delete (migrated) data
|
||||
function deleteMigratedData(data, from, packet) {
|
||||
let closestNode = cloud.closestNode(data.receiverID);
|
||||
if (data.snID !== closestNode && _list.stored.includes(data.snID)) {
|
||||
DB.deleteData(data.snID, data.vectorClock).then(_ => null).catch(e => console.error(e));
|
||||
if (_list[data.snID] < floGlobals.sn_config.backupDepth && _nextNode.id !== from)
|
||||
_nextNode.send(packet);
|
||||
};
|
||||
};
|
||||
|
||||
function initiateRefresh() {
|
||||
refresher.invoke(false).then(_ => null).catch(_ => null);
|
||||
};
|
||||
|
||||
//Forward incoming to next node
|
||||
function forwardToNextNode(mode, data) {
|
||||
var modeMap = {
|
||||
'TAG': TYPE_.TAG_BACKUP_DATA,
|
||||
'NOTE': TYPE_.NOTE_BACKUP_DATA,
|
||||
'DATA': TYPE_.STORE_BACKUP_DATA
|
||||
};
|
||||
if (mode in modeMap && _nextNode.id)
|
||||
_nextNode.send(packet_.construct({
|
||||
type_: modeMap[mode],
|
||||
data: data
|
||||
}));
|
||||
};
|
||||
|
||||
//Data migration processor
|
||||
function dataMigration(node_change, flag) {
|
||||
if (flag) dataMigration.intimateAllNodes(); //Initmate All nodes to call refresher
|
||||
if (!Object.keys(node_change).length)
|
||||
return;
|
||||
console.log("Node list changed! Data migration required", node_change);
|
||||
let new_nodes = [],
|
||||
del_nodes = [];
|
||||
for (let n in node_change)
|
||||
(node_change[n] ? new_nodes : del_nodes).push(n);
|
||||
if (_prevNode.id && del_nodes.includes(_prevNode.id))
|
||||
_prevNode.close();
|
||||
const old_kb = cloud.kb;
|
||||
setTimeout(() => {
|
||||
//reconnect next node if current next node is deleted
|
||||
if (_nextNode.id) {
|
||||
if (del_nodes.includes(_nextNode.id))
|
||||
reconnectNextNode();
|
||||
else { //reconnect next node if there are newly added nodes in between self and current next node
|
||||
let innerNodes = cloud.innerNodes(keys.node_id, _nextNode.id);
|
||||
if (new_nodes.filter(n => innerNodes.includes(n)).length)
|
||||
reconnectNextNode();
|
||||
};
|
||||
} else if (!_prevNode.id) {
|
||||
//no other nodes online: server all new nodes too
|
||||
new_nodes.forEach(n => {
|
||||
DB.createTable(n)
|
||||
.then(result => _list[n] = 0)
|
||||
.catch(error => console.error(error));
|
||||
});
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
dataMigration.process_new(new_nodes);
|
||||
dataMigration.process_del(del_nodes, old_kb);
|
||||
}, MIGRATE_WAIT_DELAY);
|
||||
}, MIGRATE_WAIT_DELAY);
|
||||
};
|
||||
|
||||
//data migration sub-process: Deleted nodes
|
||||
dataMigration.process_del = async function (del_nodes, old_kb) {
|
||||
if (!del_nodes.length)
|
||||
return;
|
||||
let serve = _prevNode.id ? old_kb.innerNodes(_prevNode.id, keys.node_id) : _list.serving;
|
||||
let process_nodes = del_nodes.filter(n => serve.includes(n));
|
||||
if (process_nodes.length) {
|
||||
connectToAllActiveNodes().then(ws_connections => {
|
||||
let remaining = process_nodes.length;
|
||||
process_nodes.forEach(n => {
|
||||
console.info(`START: Data migration (del) for ${n}`);
|
||||
DB.readAllDataStream(n, 0, d => {
|
||||
let closest = cloud.closestNode(d.receiverID);
|
||||
if (_list.serving.includes(closest)) {
|
||||
DB.storeData(closest, d, true).then(_ => null).catch(e => console.error(e));
|
||||
if (_nextNode.id)
|
||||
_nextNode.send(packet_.construct({
|
||||
type_: TYPE_.STORE_BACKUP_DATA,
|
||||
data: d
|
||||
}));
|
||||
} else
|
||||
ws_connections[closest].send(packet_.construct({
|
||||
type_: TYPE_.STORE_MIGRATED_DATA,
|
||||
data: d
|
||||
}));
|
||||
}).then(result => {
|
||||
console.info(`END: Data migration (del) for ${n}`);
|
||||
_list.delete(n);
|
||||
DB.dropTable(n).then(_ => null).catch(e => console.error(e));
|
||||
}).catch(error => {
|
||||
console.info(`ERROR: Data migration (del) for ${n}`);
|
||||
console.error(error);
|
||||
}).finally(_ => remaining--);
|
||||
});
|
||||
const interval = setInterval(() => {
|
||||
if (remaining <= 0) {
|
||||
for (let c in ws_connections)
|
||||
if (ws_connections[c])
|
||||
ws_connections[c].close();
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, RETRY_TIMEOUT);
|
||||
});
|
||||
};
|
||||
del_nodes.forEach(n => {
|
||||
if (!process_nodes.includes(n) && _list.stored.includes(n)) {
|
||||
_list.delete(n);
|
||||
DB.dropTable(n).then(_ => null).catch(e => console.error(e));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
//data migration sub-process: Added nodes
|
||||
dataMigration.process_new = async function (new_nodes) {
|
||||
if (!new_nodes.length)
|
||||
return;
|
||||
connectToAllActiveNodes(new_nodes).then(ws_connections => {
|
||||
let process_nodes = _list.serving,
|
||||
remaining = process_nodes.length;
|
||||
process_nodes.forEach(n => {
|
||||
console.info(`START: Data migration (re-new) for ${n}`);
|
||||
DB.readAllDataStream(n, 0, d => {
|
||||
let closest = cloud.closestNode(d.receiverID);
|
||||
if (new_nodes.includes(closest)) {
|
||||
if (_list.serving.includes(closest)) {
|
||||
DB.storeData(closest, d, true).then(_ => null).catch(e => console.error(e));
|
||||
if (_nextNode.id)
|
||||
_nextNode.send(packet_.construct({
|
||||
type_: TYPE_.STORE_BACKUP_DATA,
|
||||
data: d
|
||||
}));
|
||||
} else
|
||||
ws_connections[closest].send(packet_.construct({
|
||||
type_: TYPE_.STORE_MIGRATED_DATA,
|
||||
data: d
|
||||
}));
|
||||
DB.deleteData(n, d.vectorClock).then(_ => null).catch(e => console.error(e));
|
||||
if (_nextNode.id)
|
||||
_nextNode.send(packet_.construct({
|
||||
type_: TYPE_.DELETE_MIGRATED_DATA,
|
||||
data: {
|
||||
vectorClock: d.vectorClock,
|
||||
receiverID: d.receiverID,
|
||||
snID: n
|
||||
}
|
||||
}));
|
||||
};
|
||||
}).then(result => console.info(`END: Data migration (re-new) for ${n}`))
|
||||
.catch(error => {
|
||||
console.info(`ERROR: Data migration (re-new) for ${n}`);
|
||||
console.error(error);
|
||||
}).finally(_ => remaining--);
|
||||
});
|
||||
const interval = setInterval(() => {
|
||||
if (remaining <= 0) {
|
||||
for (let c in ws_connections)
|
||||
if (ws_connections[c])
|
||||
ws_connections[c].close();
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, RETRY_TIMEOUT);
|
||||
});
|
||||
};
|
||||
|
||||
dataMigration.intimateAllNodes = function () {
|
||||
connectToAliveNodes().then(ws_connections => {
|
||||
let packet = packet_.construct({
|
||||
type_: TYPE_.INITIATE_REFRESH
|
||||
});
|
||||
for (let n in ws_connections)
|
||||
if (ws_connections[n]) {
|
||||
ws_connections[n].send(packet);
|
||||
ws_connections[n].close();
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const logInterval = setInterval(() => {
|
||||
console.debug(_prevNode.id, _nextNode.id, _list.get());
|
||||
}, RETRY_TIMEOUT);
|
||||
|
||||
//-----EXPORTS-----
|
||||
module.exports = {
|
||||
reconnectNextNode,
|
||||
processTaskFromSupernode,
|
||||
forwardToNextNode,
|
||||
dataMigration,
|
||||
logInterval,
|
||||
SUPERNODE_INDICATOR,
|
||||
_list,
|
||||
set refresher(r) {
|
||||
refresher = r;
|
||||
}
|
||||
};
|
||||
27
src/backup/message_types.json
Normal file
27
src/backup/message_types.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"REQ_HASH": "req_hash",
|
||||
"RES_HASH": "block_hash",
|
||||
"REQ_BLOCK": "req_block",
|
||||
"VAL_LAST_BLK": "set_last_block",
|
||||
"INDICATE_BLK": "indicate_block",
|
||||
"STORE_BACKUP_DATA": "store_data",
|
||||
|
||||
"ORDER_BACKUP": "orderBackup",
|
||||
|
||||
"STORE_MIGRATED_DATA": "migratedData",
|
||||
"DELETE_MIGRATED_DATA": "migratedDelete",
|
||||
|
||||
"DELETE_BACKUP_DATA": "backupDelete___NOT_USED",
|
||||
"TAG_BACKUP_DATA": "backupTag",
|
||||
"NOTE_BACKUP_DATA": "backupNote",
|
||||
"EDIT_BACKUP_DATA": "backupEdit___NOT_USED",
|
||||
|
||||
"INITIATE_REFRESH": "initiateRefresh",
|
||||
|
||||
"DATA_REQUEST": "dataRequest",
|
||||
"DATA_SYNC": "dataSync",
|
||||
|
||||
"BACKUP_HANDSHAKE_INIT": "handshakeInitate",
|
||||
"BACKUP_HANDSHAKE_END": "handshakeEnd",
|
||||
"RECONNECT_NEXT_NODE": "reconnectNextNode"
|
||||
}
|
||||
234
src/backup/sync.js
Normal file
234
src/backup/sync.js
Normal file
@ -0,0 +1,234 @@
|
||||
'use strict';
|
||||
const Database = require("../database");
|
||||
const DB = Database.DB;
|
||||
const floGlobals = require("../floGlobals");
|
||||
const { H_struct } = require("../data_structure.json");
|
||||
const TYPE_ = require('./message_types.json');
|
||||
const { _list, packet_, _nextNode } = require("./values");
|
||||
const { SYNC_WAIT_TIME } = require('../_constants')['backup'];
|
||||
|
||||
//const SESSION_GROUP_CONCAT_MAX_LENGTH = 100000 //MySQL default max value is 1024, which is too low for block grouping
|
||||
|
||||
const col_aggregate = (function () {
|
||||
let ds = require("../data_structure.json");
|
||||
let cols = [];
|
||||
for (let s in ds)
|
||||
for (let c in ds[s])
|
||||
cols.push(ds[s][c]);
|
||||
return cols.map(c => `IFNULL(CRC32(${c}), 0)`).join('+');
|
||||
})();
|
||||
|
||||
const _x = {
|
||||
get block_calc_sql() {
|
||||
return `CEIL(CAST(${H_struct.VECTOR_CLOCK} AS UNSIGNED) / (${floGlobals.sn_config.blockInterval}))`;
|
||||
},
|
||||
block_range_sql(block_n) {
|
||||
let upper_vc = `${block_n * floGlobals.sn_config.blockInterval + 1}`,
|
||||
lower_vc = `${(block_n - 1) * floGlobals.sn_config.blockInterval + 1}`;
|
||||
return `${H_struct.VECTOR_CLOCK} > '${lower_vc}' AND ${H_struct.VECTOR_CLOCK} < '${upper_vc}'`;
|
||||
},
|
||||
get hash_algo_sql() {
|
||||
return `SUM(CRC32(MD5(${col_aggregate})))`;
|
||||
},
|
||||
t_name(node_i) {
|
||||
return '_' + node_i;
|
||||
}
|
||||
}
|
||||
|
||||
const queueSync = {
|
||||
list: {},
|
||||
|
||||
init(node_i, ws) {
|
||||
if (node_i in this.list)
|
||||
return console.debug("Another sync is in process for ", node_i);
|
||||
console.info(`START: Data sync for ${node_i}`)
|
||||
this.list[node_i] = { ws, ts: Date.now(), q: [], cur: new Set(), ver: new Set(), hashes: {} };
|
||||
DB.createTable(node_i)
|
||||
.then(result => ws.send(packet_.construct({ type_: TYPE_.REQ_HASH, node_i })))
|
||||
.catch(error => console.error(error));
|
||||
},
|
||||
|
||||
add_block(node_i, block_n, hash) {
|
||||
if (!(node_i in this.list))
|
||||
return console.error(`Queue-Sync: Not active for ${node_i}. Cannot request block ${block_n}`);
|
||||
let r = this.list[node_i];
|
||||
r.hashes[block_n] = hash;
|
||||
if (r.cur.size) //atleast a block is already in syncing process
|
||||
r.q.push(block_n);
|
||||
else {
|
||||
console.debug(`Queue-Sync: Started block sync for ${node_i}#${block_n}`);
|
||||
r.cur.add(block_n);
|
||||
r.ws.send(packet_.construct({ type_: TYPE_.REQ_BLOCK, node_i, block_n }))
|
||||
}
|
||||
},
|
||||
|
||||
end_block(node_i, block_n) {
|
||||
if (!(node_i in this.list))
|
||||
return console.error(`Queue-Sync: Not active for ${node_i}. Cannot request block ${block_n}`);
|
||||
|
||||
let r = this.list[node_i];
|
||||
if (!r.cur.has(block_n))
|
||||
return console.warn(`Queue-Sync: Block ${block_n} not currently syncing`);
|
||||
r.cur.delete(block_n);
|
||||
r.ver.add(block_n);
|
||||
console.debug(`Queue-Sync: Finished block sync for ${node_i}#${block_n}`);
|
||||
|
||||
if (!r.cur.size && r.q.length) {
|
||||
//request next block
|
||||
let n = r.q.shift();
|
||||
console.debug(`Queue-Sync: Started block sync for ${node_i}#${n}`);
|
||||
r.cur.add(n);
|
||||
r.ws.send(packet_.construct({ type_: TYPE_.REQ_BLOCK, node_i, block_n: n }))
|
||||
}
|
||||
//verify hash of block after timeout
|
||||
setTimeout(() => verifyBlockHash(node_i, block_n, r.hashes[block_n]).then(verified => {
|
||||
if (!verified) //hash mistmatch, resync
|
||||
this.add_block(node_i, block_n, r.hashes[block_n]);
|
||||
else //hash matched
|
||||
delete r.hashes[block_n];
|
||||
r.ver.delete(block_n);
|
||||
}).catch(error => console.error(error)), SYNC_WAIT_TIME);
|
||||
},
|
||||
|
||||
last_block(node_i, block_n) {
|
||||
if (!(node_i in this.list))
|
||||
return console.error(`Queue-Sync: Not active for ${node_i}. Cannot request block ${block_n}`);
|
||||
let r = this.list[node_i];
|
||||
r.last_block = block_n;
|
||||
r.check_interval = setInterval(() => {
|
||||
if (!r.cur.size && !r.q.length && !r.ver.size) {
|
||||
if (Object.keys(r.hashes).length)
|
||||
console.warn(`Queue-Sync: queue list is empty, but hash list is not empty for ${node_i}`);
|
||||
//all blocks synced, remove the sync instance
|
||||
console.info(`END: Data sync for ${node_i}`);
|
||||
clearInterval(r.check_interval);
|
||||
delete this.list[node_i];
|
||||
//indicate next node for ordering
|
||||
_nextNode.send(packet_.construct({
|
||||
type_: TYPE_.ORDER_BACKUP,
|
||||
order: _list.get([node_i])
|
||||
}));
|
||||
}
|
||||
}, SYNC_WAIT_TIME);
|
||||
}
|
||||
};
|
||||
|
||||
/*function setSessionVar() {
|
||||
return new Promise((resolve, reject) => {
|
||||
Database.query(`SET SESSION group_concat_max_len = ${SESSION_GROUP_CONCAT_MAX_LENGTH}`)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}*/
|
||||
|
||||
//R: request hashes for node_i
|
||||
function requestDataSync(node_i, ws) {
|
||||
queueSync.init(node_i, ws);
|
||||
}
|
||||
|
||||
//S: send hashes for node_i
|
||||
function sendBlockHashes(node_i, ws) {
|
||||
if (!_list.stored.includes(node_i))
|
||||
return console.debug(`Block hash is requested for ${node_i}, but its not in stored list`);
|
||||
let t_name = _x.t_name(node_i);
|
||||
//setSessionVar().then(_ => {
|
||||
let statement = `SELECT ${_x.block_calc_sql} AS block_n, ${_x.hash_algo_sql} as hash`
|
||||
+ ` FROM ${t_name} GROUP BY block_n ORDER BY block_n`;
|
||||
let last_block;
|
||||
Database.query_stream(statement, r => {
|
||||
ws.send(packet_.construct({
|
||||
type_: TYPE_.RES_HASH,
|
||||
node_i: node_i,
|
||||
block_n: r.block_n,
|
||||
hash: r.hash
|
||||
}));
|
||||
last_block = r.block_n;
|
||||
}).then(result => {
|
||||
console.debug(`Sent ${result} block hashes`);
|
||||
ws.send(packet_.construct({
|
||||
type_: TYPE_.VAL_LAST_BLK,
|
||||
node_i: node_i,
|
||||
block_n: last_block,
|
||||
}));
|
||||
}).catch(error => console.error(error))
|
||||
//}).catch(error => console.error(error));
|
||||
}
|
||||
|
||||
//R: check hash and request data if hash mismatch
|
||||
function checkBlockHash(node_i, block_n, hash) {
|
||||
verifyBlockHash(node_i, block_n, hash).then(result => {
|
||||
if (!result) //Hash mismatch: ReSync block
|
||||
queueSync.add_block(node_i, block_n, hash)
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function verifyBlockHash(node_i, block_n, hash) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let t_name = _x.t_name(node_i);
|
||||
//setSessionVar().then(_ => {
|
||||
let statement = `SELECT ${_x.hash_algo_sql} as hash`
|
||||
+ ` FROM ${t_name} WHERE ${_x.block_calc_sql} = ?`;
|
||||
Database.query(statement, [block_n]).then(result => {
|
||||
if (!result.length || result[0].hash != hash) { //Hash mismatch
|
||||
//ReSync Block
|
||||
let clear_statement = `DELETE FROM ${t_name} WHERE ${_x.block_range_sql(block_n)}`;
|
||||
Database.query(clear_statement)
|
||||
.then(_ => resolve(false))
|
||||
.catch(error => reject(error))
|
||||
} else //Hash is verified
|
||||
resolve(true);
|
||||
}).catch(error => reject(error))
|
||||
//}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
//R: set last block number
|
||||
function setLastBlock(node_i, block_n) {
|
||||
queueSync.last_block(node_i, block_n);
|
||||
}
|
||||
|
||||
//S: send data for block
|
||||
function sendBlockData(node_i, block_n, ws) {
|
||||
if (!_list.stored.includes(node_i))
|
||||
return console.debug(`Block hash is requested for ${node_i}, but its not in stored list`);
|
||||
let t_name = _x.t_name(node_i);
|
||||
ws.send(packet_.construct({
|
||||
type_: TYPE_.INDICATE_BLK,
|
||||
node_i, block_n,
|
||||
status: true
|
||||
}))
|
||||
console.debug(`START: Sync-send ${node_i}#${block_n} to ${ws.id}`);
|
||||
let statement = `SELECT * FROM ${t_name} WHERE ${_x.block_calc_sql} = ?`;
|
||||
Database.query_stream(statement, [block_n], d => ws.send(packet_.construct({
|
||||
type_: TYPE_.STORE_BACKUP_DATA,
|
||||
data: d
|
||||
}))).then(result => console.debug(`END: Sync-send ${node_i}#${block_n} to ${ws.id} (${result} records)`))
|
||||
.catch(error => {
|
||||
console.debug(`ERROR: Sync-send ${node_i}#${block_n} to ${ws.id}`)
|
||||
console.error(error);
|
||||
}).finally(_ => ws.send(packet_.construct({
|
||||
type_: TYPE_.INDICATE_BLK,
|
||||
node_i, block_n,
|
||||
status: false
|
||||
})));
|
||||
}
|
||||
|
||||
//R: end block
|
||||
function endBlockSync(node_i, block_n) {
|
||||
queueSync.end_block(node_i, block_n);
|
||||
}
|
||||
|
||||
function syncIndicator(node_i, block_n, status, from) {
|
||||
console.debug(`${status ? 'Start' : 'End'}: Sync-receive ${node_i}#${block_n} from ${from}`);
|
||||
if (!status)
|
||||
endBlockSync(node_i, block_n);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
requestDataSync,
|
||||
sendBlockHashes,
|
||||
checkBlockHash,
|
||||
setLastBlock,
|
||||
sendBlockData,
|
||||
syncIndicator
|
||||
}
|
||||
140
src/backup/values.js
Normal file
140
src/backup/values.js
Normal file
@ -0,0 +1,140 @@
|
||||
'use strict';
|
||||
const keys = require('../keys');
|
||||
|
||||
const SUPERNODE_INDICATOR = '$';
|
||||
|
||||
//List of node backups stored
|
||||
const _list = {};
|
||||
Object.defineProperty(_list, 'delete', {
|
||||
value: function (id) {
|
||||
delete this[id];
|
||||
}
|
||||
});
|
||||
Object.defineProperty(_list, 'get', {
|
||||
value: function (keys = null) {
|
||||
if (keys === null) keys = Object.keys(this);
|
||||
if (Array.isArray(keys))
|
||||
return Object.fromEntries(keys.map(k => [k, this[k]]));
|
||||
else
|
||||
return this[keys];
|
||||
}
|
||||
});
|
||||
Object.defineProperty(_list, 'stored', {
|
||||
get: function () {
|
||||
return Object.keys(this);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(_list, 'serving', {
|
||||
get: function () {
|
||||
let serveList = [];
|
||||
for (let id in this)
|
||||
if (this[id] === 0)
|
||||
serveList.push(id);
|
||||
return serveList;
|
||||
}
|
||||
});
|
||||
|
||||
//Node container
|
||||
function NodeContainer() {
|
||||
var _ws, _id, _onmessage, _onclose;
|
||||
Object.defineProperty(this, 'set', {
|
||||
value: function (id, ws) {
|
||||
if (_ws !== undefined)
|
||||
this.close();
|
||||
_id = id;
|
||||
_ws = ws;
|
||||
if (_onmessage)
|
||||
_ws.onmessage = _onmessage;
|
||||
if (_onclose)
|
||||
_ws.onclose = _onclose;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, 'id', {
|
||||
get: function () {
|
||||
return _id;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, 'readyState', {
|
||||
get: function () {
|
||||
if (_ws instanceof WebSocket)
|
||||
return _ws.readyState;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, 'send', {
|
||||
value: function (packet) {
|
||||
_ws.send(packet);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, 'onmessage', {
|
||||
set: function (fn) {
|
||||
if (fn instanceof Function)
|
||||
_onmessage = fn;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, 'onclose', {
|
||||
set: function (fn) {
|
||||
if (fn instanceof Function)
|
||||
_onclose = fn;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, 'is', {
|
||||
value: function (ws) {
|
||||
return ws === _ws;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, 'close', {
|
||||
value: function () {
|
||||
if (_ws.readyState === 1) {
|
||||
_ws.onclose = () => console.warn('Closing: ' + _id);
|
||||
_ws.close();
|
||||
};
|
||||
_ws = _id = undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//Container for next-node
|
||||
const _nextNode = new NodeContainer();
|
||||
//Container for prev-node
|
||||
const _prevNode = new NodeContainer();
|
||||
|
||||
//Packet processing
|
||||
const packet_ = {};
|
||||
packet_.construct = function (message) {
|
||||
const packet = {
|
||||
from: keys.node_id,
|
||||
message: message,
|
||||
time: Date.now()
|
||||
};
|
||||
packet.sign = floCrypto.signData(this.s(packet), keys.node_priv);
|
||||
return SUPERNODE_INDICATOR + JSON.stringify(packet);
|
||||
};
|
||||
packet_.s = d => [JSON.stringify(d.message), d.time].join("|");
|
||||
packet_.parse = function (str) {
|
||||
try {
|
||||
let packet = JSON.parse(str.substring(SUPERNODE_INDICATOR.length));
|
||||
let curTime = Date.now();
|
||||
if (packet.time > curTime - floGlobals.sn_config.delayDelta &&
|
||||
packet.from in floGlobals.supernodes &&
|
||||
floCrypto.verifySign(this.s(packet), packet.sign, floGlobals.supernodes[packet.from].pubKey)) {
|
||||
if (!Array.isArray(packet.message))
|
||||
packet.message = [packet.message];
|
||||
return packet;
|
||||
} else
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error(str, error);
|
||||
return false;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
_list,
|
||||
_nextNode,
|
||||
_prevNode,
|
||||
packet_,
|
||||
SUPERNODE_INDICATOR
|
||||
}
|
||||
25
src/base_tables.json
Normal file
25
src/base_tables.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"LastTxs": {
|
||||
"ID": "CHAR(34) NOT NULL",
|
||||
"N": "INT NOT NULL",
|
||||
"PRIMARY": "KEY (ID)"
|
||||
},
|
||||
"Configs": {
|
||||
"NAME": "VARCHAR(64) NOT NULL",
|
||||
"VAL": "VARCHAR(512) NOT NULL",
|
||||
"PRIMARY": "KEY (NAME)"
|
||||
},
|
||||
"SuperNodes": {
|
||||
"FLO_ID": "CHAR(34) NOT NULL",
|
||||
"PUB_KEY": "CHAR(66) NOT NULL",
|
||||
"URI": "VARCHAR(256) NOT NULL",
|
||||
"PRIMARY": "KEY (FLO_ID)"
|
||||
},
|
||||
"Applications": {
|
||||
"APP_NAME": "VARCHAR(64) NOT NULL",
|
||||
"ADMIN_ID": "CHAR(34) NOT NULL",
|
||||
"SUB_ADMINS": "TEXT",
|
||||
"TRUSTED_IDS": "TEXT",
|
||||
"PRIMARY": "KEY (APP_NAME)"
|
||||
}
|
||||
}
|
||||
273
src/client.js
Normal file
273
src/client.js
Normal file
@ -0,0 +1,273 @@
|
||||
const { DB } = require("./database");
|
||||
const { _list } = require("./backup/values");
|
||||
const { INVALID } = require("./_constants");
|
||||
|
||||
function processIncomingData(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (error) {
|
||||
return reject(INVALID("Data not in JSON-Format"));
|
||||
};
|
||||
let curTime = Date.now();
|
||||
if (!data.time || data.time > curTime + floGlobals.sn_config.delayDelta ||
|
||||
data.time < curTime - floGlobals.sn_config.delayDelta)
|
||||
return reject(INVALID("Invalid Time"));
|
||||
else {
|
||||
let process;
|
||||
if ('request' in data) //Request
|
||||
process = processRequestFromUser(data.request);
|
||||
else if ('message' in data) //Store data
|
||||
process = processDataFromUser(data);
|
||||
else if ('tag' in data) //Tag data
|
||||
process = processTagFromUser(data);
|
||||
else if ('note' in data)
|
||||
process = processNoteFromUser(data);
|
||||
/*
|
||||
else if (data.edit)
|
||||
return processEditFromUser(gid, uid, data);
|
||||
else if (data.delete)
|
||||
return processDeleteFromUser(gid, uid, data);
|
||||
*/
|
||||
else
|
||||
return reject(INVALID("Invalid Data-format"));
|
||||
process.then(result => {
|
||||
//console.debug(result);
|
||||
resolve(result);
|
||||
}).catch(error => {
|
||||
(error instanceof INVALID ? console.debug : console.error)(error);
|
||||
reject(error);
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function processDataFromUser(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateAddr(data.receiverID))
|
||||
return reject(INVALID("Invalid receiverID"));
|
||||
let closeNode = cloud.closestNode(data.receiverID);
|
||||
if (!_list.serving.includes(closeNode))
|
||||
return reject(INVALID("Incorrect Supernode"));
|
||||
if (!floCrypto.validateAddr(data.senderID))
|
||||
return reject(INVALID("Invalid senderID"));
|
||||
if (!floCrypto.verifyPubKey(data.pubKey, data.senderID))
|
||||
return reject(INVALID("Invalid pubKey"));
|
||||
let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"]
|
||||
.map(d => data[d]).join("|");
|
||||
if (!floCrypto.verifySign(hashcontent, data.sign, data.pubKey))
|
||||
return reject(INVALID("Invalid signature"));
|
||||
|
||||
DB.addData(closeNode, {
|
||||
vectorClock: `${Date.now()}_${data.senderID}`,
|
||||
senderID: data.senderID,
|
||||
receiverID: data.receiverID,
|
||||
time: data.time,
|
||||
application: data.application,
|
||||
type: data.type,
|
||||
message: data.message,
|
||||
comment: data.comment,
|
||||
sign: data.sign,
|
||||
pubKey: data.pubKey
|
||||
}).then(rb => {
|
||||
DB.getData(closeNode, rb.vectorClock)
|
||||
.then(result => resolve([result[0], 'DATA', rb]))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
function processRequestFromUser(request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateAddr(request.receiverID))
|
||||
return reject(INVALID("Invalid receiverID"));
|
||||
let closeNode = cloud.closestNode(request.receiverID);
|
||||
if (!_list.serving.includes(closeNode))
|
||||
return reject(INVALID("Incorrect Supernode"));
|
||||
DB.searchData(closeNode, request)
|
||||
.then(result => resolve([result]))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
function processTagFromUser(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateAddr(data.receiverID))
|
||||
return reject(INVALID("Invalid receiverID"));
|
||||
let closeNode = cloud.closestNode(data.receiverID);
|
||||
if (!_list.serving.includes(closeNode))
|
||||
return reject(INVALID("Incorrect Supernode"));
|
||||
DB.getData(closeNode, data.vectorClock).then(result => {
|
||||
if (!result.length)
|
||||
return reject(INVALID("Invalid vectorClock"));
|
||||
result = result[0];
|
||||
if (!(result.application in floGlobals.appList))
|
||||
return reject(INVALID("Application not authorised"));
|
||||
if (!floCrypto.validateAddr(data.requestorID) ||
|
||||
(!floGlobals.appSubAdmins[result.application].includes(data.requestorID)
|
||||
&& !floGlobals.appTrustedIDs[result.application].includes(data.requestorID)))
|
||||
return reject(INVALID("Invalid requestorID"));
|
||||
if (!floCrypto.verifyPubKey(data.pubKey, data.requestorID))
|
||||
return reject(INVALID("Invalid pubKey"));
|
||||
let hashcontent = ["time", "vectorClock", "tag"].map(d => data[d]).join("|");
|
||||
if (!floCrypto.verifySign(hashcontent, data.sign, data.pubKey))
|
||||
return reject(INVALID("Invalid signature"));
|
||||
let tag = ([null, undefined, ""].includes(data.tag) ? null : data.tag.toString());
|
||||
DB.tagData(closeNode, data.vectorClock, tag, data.time, data.pubKey, data.sign).then(rb => {
|
||||
DB.getData(closeNode, data.vectorClock)
|
||||
.then(result => resolve([result[0], 'TAG', rb]))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
};
|
||||
|
||||
function processNoteFromUser(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateAddr(data.receiverID))
|
||||
return reject(INVALID("Invalid receiverID"));
|
||||
let closeNode = cloud.closestNode(data.receiverID);
|
||||
if (!_list.serving.includes(closeNode))
|
||||
return reject(INVALID("Incorrect Supernode"));
|
||||
DB.getData(closeNode, data.vectorClock).then(result => {
|
||||
if (!result.length)
|
||||
return reject(INVALID("Invalid vectorClock"));
|
||||
result = result[0];
|
||||
if (result.application in floGlobals.appList && floGlobals.appList[result.application] === result.receiverID) {
|
||||
if (!floGlobals.appSubAdmins[result.application].includes(data.requestorID) && result.receiverID !== data.requestorID)
|
||||
return reject(INVALID("Invalid requestorID"));
|
||||
} else if (result.receiverID !== data.requestorID)
|
||||
return reject(INVALID("Invalid requestorID"));
|
||||
if (!floCrypto.verifyPubKey(data.pubKey, data.requestorID))
|
||||
return reject(INVALID("Invalid pubKey"));
|
||||
let hashcontent = ["time", "vectorClock", "note"].map(d => data[d]).join("|");
|
||||
if (!floCrypto.verifySign(hashcontent, data.sign, data.pubKey))
|
||||
return reject(INVALID("Invalid signature"));
|
||||
let note = ([null, undefined, ""].includes(data.note) ? null : data.note.toString());
|
||||
DB.noteData(closeNode, data.vectorClock, note, data.time, data.pubKey, data.sign).then(rb => {
|
||||
DB.getData(closeNode, data.vectorClock)
|
||||
.then(result => resolve([result[0], 'NOTE', rb]))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function checkIfRequestSatisfy(request, data) {
|
||||
if (!request || request.mostRecent || request.receiverID !== (data.proxyID || data.receiverID))
|
||||
return false;
|
||||
if (request.atVectorClock && request.atVectorClock !== data.vectorClock)
|
||||
return false;
|
||||
if (request.lowerVectorClock && request.lowerVectorClock > data.vectorClock)
|
||||
return false;
|
||||
if (request.upperVectorClock && request.upperVectorClock < data.vectorClock)
|
||||
return false;
|
||||
if (request.afterTime && request.afterTime > data.log_time)
|
||||
return false;
|
||||
if (request.application !== data.application)
|
||||
return false;
|
||||
if (request.comment && request.comment !== data.comment)
|
||||
return false;
|
||||
if (request.type && request.type !== data.type)
|
||||
return false;
|
||||
if (request.senderID) {
|
||||
if (Array.isArray(request.senderID)) {
|
||||
if (!request.senderID.includes(data.senderID))
|
||||
return false;
|
||||
} else if (request.senderID !== data.senderID)
|
||||
return false;
|
||||
};
|
||||
return true;
|
||||
};
|
||||
|
||||
function processStatusFromUser(request, ws) {
|
||||
if (request.status) {
|
||||
//Set user-online status
|
||||
if (!request.floID || !request.application || !request.sign || !request.pubKey || !request.time)
|
||||
return ws.send("Invalid request parameters");
|
||||
if (!floCrypto.verifyPubKey(request.pubKey, request.floID))
|
||||
return ws.send("Invalid pubKey");
|
||||
let hashcontent = ["time", "application", "floID"].map(d => request[d]).join("|");
|
||||
if (!floCrypto.verifySign(hashcontent, request.sign, request.pubKey))
|
||||
return ws.send("Invalid signature");
|
||||
clientOnline(ws, request.application, request.floID);
|
||||
} else {
|
||||
//Track online status
|
||||
if (!request.application || !Array.isArray(request.trackList))
|
||||
return ws.send("Invalid request parameters");
|
||||
addRequestClient(ws, request.application, request.trackList);
|
||||
}
|
||||
}
|
||||
|
||||
let onlineClients = {},
|
||||
requestClients = {};
|
||||
|
||||
const ONLINE = 1,
|
||||
OFFLINE = 0;
|
||||
|
||||
function clientOnline(ws, application, floID) {
|
||||
if (!(application in onlineClients))
|
||||
onlineClients[application] = {};
|
||||
if (floID in onlineClients[application])
|
||||
onlineClients[application][floID] += 1;
|
||||
else {
|
||||
onlineClients[application][floID] = 1;
|
||||
informRequestClients(application, floID, ONLINE);
|
||||
}
|
||||
ws.on('close', () => clientOffline(application, floID));
|
||||
}
|
||||
|
||||
function clientOffline(application, floID) {
|
||||
if (application in onlineClients)
|
||||
if (floID in onlineClients[application]) {
|
||||
if (onlineClients[application][floID] > 1)
|
||||
onlineClients[application][floID] -= 1;
|
||||
else {
|
||||
delete onlineClients[application][floID];
|
||||
informRequestClients(application, floID, OFFLINE);
|
||||
if (!Object.keys(onlineClients[application]).length)
|
||||
delete onlineClients[application];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addRequestClient(ws, application, trackList) {
|
||||
let id = Date.now() + floCrypto.randString(8);
|
||||
if (!(application in requestClients))
|
||||
requestClients[application] = {};
|
||||
requestClients[application][id] = {
|
||||
ws: ws,
|
||||
trackList
|
||||
};
|
||||
let status = {};
|
||||
if (application in onlineClients)
|
||||
trackList.forEach(floID => status[floID] = (onlineClients[application][floID] ? ONLINE : OFFLINE));
|
||||
else
|
||||
trackList.forEach(floID => status[floID] = OFFLINE);
|
||||
ws.send(JSON.stringify(status));
|
||||
ws.on('close', () => rmRequestClient(application, id));
|
||||
}
|
||||
|
||||
function rmRequestClient(application, id) {
|
||||
if ((application in requestClients) && (id in requestClients[application])) {
|
||||
delete requestClients[application][id];
|
||||
if (!Object.keys(requestClients[application]).length)
|
||||
delete requestClients[application];
|
||||
}
|
||||
}
|
||||
|
||||
function informRequestClients(application, floID, status) {
|
||||
if (application in requestClients)
|
||||
for (let r in requestClients[application])
|
||||
if (requestClients[application][r].trackList.includes(floID))
|
||||
requestClients[application][r].ws.send(JSON.stringify({
|
||||
[floID]: status
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkIfRequestSatisfy,
|
||||
processRequestFromUser,
|
||||
processIncomingData,
|
||||
processStatusFromUser
|
||||
};
|
||||
160
src/cloud.js
Normal file
160
src/cloud.js
Normal file
@ -0,0 +1,160 @@
|
||||
'use strict';
|
||||
|
||||
function K_Bucket(masterID, nodeList) {
|
||||
|
||||
const decodeID = function(floID) {
|
||||
let k = bitjs.Base58.decode(floID);
|
||||
k.shift();
|
||||
k.splice(-4, 4);
|
||||
const decodedId = Crypto.util.bytesToHex(k);
|
||||
const nodeIdBigInt = new BigInteger(decodedId, 16);
|
||||
const nodeIdBytes = nodeIdBigInt.toByteArrayUnsigned();
|
||||
const nodeIdNewInt8Array = new Uint8Array(nodeIdBytes);
|
||||
return nodeIdNewInt8Array;
|
||||
};
|
||||
|
||||
const _KB = new BuildKBucket({
|
||||
localNodeId: decodeID(masterID)
|
||||
});
|
||||
nodeList.forEach(id => _KB.add({
|
||||
id: decodeID(id),
|
||||
floID: id
|
||||
}));
|
||||
const _CO = nodeList.map(sn => [_KB.distance(decodeID(masterID), decodeID(sn)), sn])
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(a => a[1]);
|
||||
|
||||
const self = this;
|
||||
|
||||
Object.defineProperty(self, 'order', {
|
||||
get: () => Array.from(_CO)
|
||||
});
|
||||
|
||||
self.innerNodes = function(id1, id2) {
|
||||
if (!_CO.includes(id1) || !_CO.includes(id2))
|
||||
throw Error('Given nodes are not in KB');
|
||||
let iNodes = [];
|
||||
for (let i = _CO.indexOf(id1) + 1; _CO[i] != id2; i++) {
|
||||
if (i < _CO.length)
|
||||
iNodes.push(_CO[i]);
|
||||
else i = -1;
|
||||
};
|
||||
return iNodes;
|
||||
};
|
||||
|
||||
self.outterNodes = function(id1, id2) {
|
||||
if (!_CO.includes(id1) || !_CO.includes(id2))
|
||||
throw Error('Given nodes are not in KB');
|
||||
let oNodes = [];
|
||||
for (let i = _CO.indexOf(id2) + 1; _CO[i] != id1; i++) {
|
||||
if (i < _CO.length)
|
||||
oNodes.push(_CO[i]);
|
||||
else i = -1;
|
||||
};
|
||||
return oNodes;
|
||||
};
|
||||
|
||||
self.prevNode = function(id, N = 1) {
|
||||
let n = N || _CO.length;
|
||||
if (!_CO.includes(id))
|
||||
throw Error('Given node is not KB');
|
||||
let pNodes = [];
|
||||
for (let i = 0, j = _CO.indexOf(id) - 1; i < n; j--) {
|
||||
if (j == _CO.indexOf(id))
|
||||
break;
|
||||
else if (j > -1)
|
||||
pNodes[i++] = _CO[j];
|
||||
else j = _CO.length;
|
||||
};
|
||||
return (N == 1 ? pNodes[0] : pNodes);
|
||||
};
|
||||
|
||||
self.nextNode = function(id, N = 1) {
|
||||
let n = N || _CO.length;
|
||||
if (!_CO.includes(id))
|
||||
throw Error('Given node is not KB');
|
||||
let nNodes = [];
|
||||
for (let i = 0, j = _CO.indexOf(id) + 1; i < n; j++) {
|
||||
if (j == _CO.indexOf(id))
|
||||
break;
|
||||
else if (j < _CO.length)
|
||||
nNodes[i++] = _CO[j];
|
||||
else j = -1;
|
||||
};
|
||||
return (N == 1 ? nNodes[0] : nNodes);
|
||||
};
|
||||
|
||||
self.closestNode = function(id, N = 1) {
|
||||
let decodedId = decodeID(id);
|
||||
let n = N || _CO.length;
|
||||
let cNodes = _KB.closest(decodedId, n)
|
||||
.map(k => k.floID);
|
||||
return (N == 1 ? cNodes[0] : cNodes);
|
||||
};
|
||||
}
|
||||
|
||||
const blockchainPrefix = 0x23;
|
||||
|
||||
function proxyID(address) {
|
||||
if (!address)
|
||||
return;
|
||||
var bytes;
|
||||
if (address.length == 34) { //legacy encoding
|
||||
let decode = bitjs.Base58.decode(address);
|
||||
bytes = decode.slice(0, decode.length - 4);
|
||||
let checksum = decode.slice(decode.length - 4),
|
||||
hash = Crypto.SHA256(Crypto.SHA256(bytes, {
|
||||
asBytes: true
|
||||
}), {
|
||||
asBytes: true
|
||||
});
|
||||
hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3] ?
|
||||
bytes = undefined : bytes.shift();
|
||||
} else if (address.length == 42 || address.length == 62) { //bech encoding
|
||||
if (typeof coinjs !== 'function')
|
||||
throw "library missing (lib_btc.js)";
|
||||
let decode = coinjs.bech32_decode(address);
|
||||
if (decode) {
|
||||
bytes = decode.data;
|
||||
bytes.shift();
|
||||
bytes = coinjs.bech32_convert(bytes, 5, 8, false);
|
||||
if (address.length == 62) //for long bech, aggregate once more to get 160 bit
|
||||
bytes = coinjs.bech32_convert(bytes, 5, 8, false);
|
||||
}
|
||||
} else if (address.length == 66) { //public key hex
|
||||
bytes = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(address), {
|
||||
asBytes: true
|
||||
}));
|
||||
}
|
||||
if (!bytes)
|
||||
throw "Invalid address: " + address;
|
||||
else {
|
||||
bytes.unshift(blockchainPrefix);
|
||||
let hash = Crypto.SHA256(Crypto.SHA256(bytes, {
|
||||
asBytes: true
|
||||
}), {
|
||||
asBytes: true
|
||||
});
|
||||
return bitjs.Base58.encode(bytes.concat(hash.slice(0, 4)));
|
||||
}
|
||||
}
|
||||
|
||||
var kBucket;
|
||||
const cloud = module.exports = function Cloud(masterID, nodeList) {
|
||||
kBucket = new K_Bucket(masterID, nodeList);
|
||||
}
|
||||
|
||||
cloud.proxyID = (a) => proxyID(a);
|
||||
cloud.closestNode = (id, N = 1) => kBucket.closestNode(proxyID(id), N);
|
||||
cloud.prevNode = (id, N = 1) => kBucket.prevNode(id, N);
|
||||
cloud.nextNode = (id, N = 1) => kBucket.nextNode(id, N);
|
||||
cloud.innerNodes = (id1, id2) => kBucket.innerNodes(id1, id2);
|
||||
cloud.outterNodes = (id1, id2) => kBucket.outterNodes(id1, id2);
|
||||
Object.defineProperties(cloud, {
|
||||
kb: {
|
||||
get: () => kBucket
|
||||
},
|
||||
order: {
|
||||
get: () => kBucket.order
|
||||
}
|
||||
});
|
||||
33
src/data_structure.json
Normal file
33
src/data_structure.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"H_struct": {
|
||||
"VECTOR_CLOCK": "vectorClock",
|
||||
"SENDER_ID": "senderID",
|
||||
"RECEIVER_ID": "receiverID",
|
||||
"TYPE": "type",
|
||||
"APPLICATION": "application",
|
||||
"TIME": "time",
|
||||
"PUB_KEY": "pubKey"
|
||||
},
|
||||
"B_struct": {
|
||||
"MESSAGE": "message",
|
||||
"SIGNATURE": "sign",
|
||||
"COMMENT": "comment"
|
||||
},
|
||||
"L_struct": {
|
||||
"PROXY_ID": "proxyID",
|
||||
"STATUS": "status_n",
|
||||
"LOG_TIME": "log_time"
|
||||
},
|
||||
"T_struct": {
|
||||
"TAG": "tag",
|
||||
"TAG_TIME": "tag_time",
|
||||
"TAG_KEY": "tag_key",
|
||||
"TAG_SIGN": "tag_sign"
|
||||
},
|
||||
"F_struct": {
|
||||
"NOTE": "note",
|
||||
"NOTE_TIME": "note_time",
|
||||
"NOTE_KEY": "note_key",
|
||||
"NOTE_SIGN": "note_sign"
|
||||
}
|
||||
}
|
||||
496
src/database.js
Normal file
496
src/database.js
Normal file
@ -0,0 +1,496 @@
|
||||
'use strict';
|
||||
var mysql = require('mysql');
|
||||
|
||||
const Base_Tables = require("./base_tables.json");
|
||||
const { H_struct, B_struct, L_struct, T_struct, F_struct } = require("./data_structure.json");
|
||||
|
||||
var pool; //container for pool
|
||||
|
||||
function initConnection(user, password, dbname, host = 'localhost') {
|
||||
return new Promise((resolve, reject) => {
|
||||
pool = mysql.createPool({
|
||||
host: host,
|
||||
user: user,
|
||||
password: password,
|
||||
database: dbname
|
||||
});
|
||||
getConnection().then(conn => {
|
||||
conn.release();
|
||||
resolve(DB);
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
const getConnection = () => new Promise((resolve, reject) => {
|
||||
pool.getConnection((error, conn) => {
|
||||
if (error)
|
||||
reject(error);
|
||||
else
|
||||
resolve(conn);
|
||||
});
|
||||
});
|
||||
|
||||
function queryResolve(sql, values) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getConnection().then(conn => {
|
||||
const fn = (err, res) => {
|
||||
conn.release();
|
||||
(err ? reject(err) : resolve(res));
|
||||
};
|
||||
if (values)
|
||||
conn.query(sql, values, fn);
|
||||
else
|
||||
conn.query(sql, fn);
|
||||
}).catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
|
||||
function queryStream(sql, values, callback) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getConnection().then(conn => {
|
||||
if (!callback && values instanceof Function) {
|
||||
callback = values;
|
||||
values = undefined;
|
||||
}
|
||||
var err_flag, row_count = 0;
|
||||
(values ? conn.query(sql, values) : conn.query(sql))
|
||||
.on('error', err => err_flag = err)
|
||||
.on('result', row => {
|
||||
row_count++;
|
||||
conn.pause();
|
||||
callback(row);
|
||||
conn.resume();
|
||||
}).on('end', _ => {
|
||||
conn.release();
|
||||
err_flag ? reject(err_flag) : resolve(row_count);
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const DB = {};
|
||||
|
||||
/* Base Tables */
|
||||
|
||||
DB.createBase = function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statements = [];
|
||||
for (let t in Base_Tables)
|
||||
statements.push("CREATE TABLE IF NOT EXISTS " + t + "( " +
|
||||
Object.keys(Base_Tables[t]).map(a => a + " " + Base_Tables[t][a]).join(", ") + " )");
|
||||
Promise.all(statements.map(s => queryResolve(s)))
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.setLastTx = function (id, n) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "INSERT INTO LastTxs (ID, N) VALUES (?, ?)" +
|
||||
" ON DUPLICATE KEY UPDATE N=?";
|
||||
queryResolve(statement, [id, n, n])
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.setConfig = function (name, value) {
|
||||
return new Promise((resolve, reject) => {
|
||||
value = JSON.stringify(value);
|
||||
let statement = "INSERT INTO Configs (NAME, VAL) VALUES (?, ?)" +
|
||||
" ON DUPLICATE KEY UPDATE VAL=?";
|
||||
queryResolve(statement, [name, value, value])
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.addSuperNode = function (id, pubKey, uri) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "INSERT INTO SuperNodes (FLO_ID, PUB_KEY, URI) VALUES (?, ?, ?)" +
|
||||
" ON DUPLICATE KEY UPDATE URI=?";
|
||||
queryResolve(statement, [id, pubKey, uri, uri])
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.rmSuperNode = function (id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "DELETE FROM SuperNodes" +
|
||||
" WHERE FLO_ID=?";
|
||||
queryResolve(statement, id)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.updateSuperNode = function (id, uri) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "UPDATE SuperNodes SET URI=? WHERE FLO_ID=?";
|
||||
queryResolve(statement, [uri, id])
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
|
||||
DB.setSubAdmin = function (appName, subAdmins) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "UPDATE Applications" +
|
||||
" SET SUB_ADMINS=?" +
|
||||
" WHERE APP_NAME=?";
|
||||
queryResolve(statement, [subAdmins.join(","), appName])
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.setTrustedIDs = function (appName, trustedIDs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "UPDATE Applications" +
|
||||
" SET TRUSTED_IDS=?" +
|
||||
" WHERE APP_NAME=?";
|
||||
queryResolve(statement, [trustedIDs.join(","), appName])
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
DB.addApp = function (appName, adminID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "INSERT INTO Applications (APP_NAME, ADMIN_ID) VALUES (?, ?)" +
|
||||
" ON DUPLICATE KEY UPDATE ADMIN_ID=?";
|
||||
queryResolve(statement, [appName, adminID, adminID])
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.rmApp = function (appName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "DELETE FROM Applications" +
|
||||
" WHERE APP_NAME=" + appName;
|
||||
queryResolve(statement)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.getBase = function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
let tables = Object.keys(Base_Tables);
|
||||
Promise.all(tables.map(t => queryResolve("SELECT * FROM " + t))).then(result => {
|
||||
let tmp = Object.fromEntries(tables.map((t, i) => [t, result[i]]));
|
||||
result = {};
|
||||
result.lastTx = Object.fromEntries(tmp.LastTxs.map(a => [a.ID, a.N]));
|
||||
result.sn_config = Object.fromEntries(tmp.Configs.map(a => [a.NAME, JSON.parse(a.VAL)]));
|
||||
result.appList = Object.fromEntries(tmp.Applications.map(a => [a.APP_NAME, a.ADMIN_ID]));
|
||||
result.appSubAdmins = Object.fromEntries(tmp.Applications.map(a => [a.APP_NAME, a.SUB_ADMINS ? a.SUB_ADMINS.split(",") : []]));
|
||||
result.appTrustedIDs = Object.fromEntries(tmp.Applications.map(a => [a.APP_NAME, a.TRUSTED_IDS ? a.TRUSTED_IDS.split(",") : []]));
|
||||
result.supernodes = Object.fromEntries(tmp.SuperNodes.map(a => [a.FLO_ID, {
|
||||
pubKey: a.PUB_KEY,
|
||||
uri: a.URI
|
||||
}]));
|
||||
resolve(result);
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/* Supernode Tables */
|
||||
|
||||
DB.createTable = function (snID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "CREATE TABLE IF NOT EXISTS _" + snID + " ( " +
|
||||
H_struct.VECTOR_CLOCK + " VARCHAR(88) NOT NULL, " +
|
||||
H_struct.SENDER_ID + " VARCHAR(72) NOT NULL, " +
|
||||
H_struct.RECEIVER_ID + " VARCHAR(72) NOT NULL, " +
|
||||
H_struct.APPLICATION + " TINYTEXT NOT NULL, " +
|
||||
H_struct.TYPE + " TINYTEXT, " +
|
||||
B_struct.MESSAGE + " LONGTEXT NOT NULL, " +
|
||||
H_struct.TIME + " BIGINT NOT NULL, " +
|
||||
B_struct.SIGNATURE + " VARCHAR(160) NOT NULL, " +
|
||||
H_struct.PUB_KEY + " CHAR(66) NOT NULL, " +
|
||||
B_struct.COMMENT + " TINYTEXT, " +
|
||||
L_struct.PROXY_ID + " CHAR(34), " +
|
||||
L_struct.STATUS + " INT NOT NULL, " +
|
||||
L_struct.LOG_TIME + " BIGINT NOT NULL, " +
|
||||
T_struct.TAG + " TINYTEXT, " +
|
||||
T_struct.TAG_TIME + " BIGINT, " +
|
||||
T_struct.TAG_KEY + " CHAR(66), " +
|
||||
T_struct.TAG_SIGN + " VARCHAR(160), " +
|
||||
F_struct.NOTE + " TINYTEXT, " +
|
||||
F_struct.NOTE_TIME + " BIGINT, " +
|
||||
F_struct.NOTE_KEY + " CHAR(66), " +
|
||||
F_struct.NOTE_SIGN + " VARCHAR(160), " +
|
||||
"PRIMARY KEY (" + H_struct.VECTOR_CLOCK + ")" +
|
||||
" )";
|
||||
queryResolve(statement)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.dropTable = function (snID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "DROP TABLE _" + snID;
|
||||
queryResolve(statement)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.listTable = function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
queryResolve("SHOW TABLES").then(result => {
|
||||
const disks = [];
|
||||
for (let i in result)
|
||||
for (let j in result[i])
|
||||
if (result[i][j].startsWith("_"))
|
||||
disks.push(result[i][j].split("_")[1]);
|
||||
resolve(disks);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
/* Data Service (by client)*/
|
||||
|
||||
DB.addData = function (snID, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
data[L_struct.STATUS] = 1;
|
||||
data[L_struct.LOG_TIME] = Date.now();
|
||||
let proxyID = cloud.proxyID(data[H_struct.RECEIVER_ID]);
|
||||
data[L_struct.PROXY_ID] = proxyID !== data[H_struct.RECEIVER_ID] ? proxyID : null;
|
||||
let attr = Object.keys(H_struct).map(a => H_struct[a])
|
||||
.concat(Object.keys(B_struct).map(a => B_struct[a]))
|
||||
.concat(Object.keys(L_struct).map(a => L_struct[a]));
|
||||
let values = attr.map(a => data[a]);
|
||||
let statement = "INSERT INTO _" + snID +
|
||||
" (" + attr.join(", ") + ") " +
|
||||
"VALUES (" + attr.map(a => '?').join(", ") + ")";
|
||||
data = Object.fromEntries(attr.map((a, i) => [a, values[i]]));
|
||||
queryResolve(statement, values)
|
||||
.then(result => resolve(data))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.getData = function (snID, vectorClock) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "SELECT * FROM _" + snID +
|
||||
" WHERE " + H_struct.VECTOR_CLOCK + "=?";
|
||||
queryResolve(statement, [vectorClock])
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
};
|
||||
|
||||
DB.tagData = function (snID, vectorClock, tag, tagTime, tagKey, tagSign) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = {
|
||||
[T_struct.TAG]: tag,
|
||||
[T_struct.TAG_TIME]: tagTime,
|
||||
[T_struct.TAG_KEY]: tagKey,
|
||||
[T_struct.TAG_SIGN]: tagSign,
|
||||
[L_struct.LOG_TIME]: Date.now()
|
||||
};
|
||||
let attr = Object.keys(data);
|
||||
let values = attr.map(a => data[a]).concat(vectorClock);
|
||||
data[H_struct.VECTOR_CLOCK] = vectorClock; //also add vectorClock to resolve data
|
||||
let statement = "UPDATE _" + snID +
|
||||
" SET " + attr.map(a => a + "=?").join(", ") +
|
||||
" WHERE " + H_struct.VECTOR_CLOCK + "=?";
|
||||
queryResolve(statement, values)
|
||||
.then(result => resolve(data))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.noteData = function (snID, vectorClock, note, noteTime, noteKey, noteSign) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = {
|
||||
[F_struct.NOTE]: note,
|
||||
[F_struct.NOTE_TIME]: noteTime,
|
||||
[F_struct.NOTE_KEY]: noteKey,
|
||||
[F_struct.NOTE_SIGN]: noteSign,
|
||||
[L_struct.LOG_TIME]: Date.now()
|
||||
};
|
||||
let attr = Object.keys(data);
|
||||
let values = attr.map(a => data[a]).concat(vectorClock);
|
||||
data[H_struct.VECTOR_CLOCK] = vectorClock; //also add vectorClock to resolve data
|
||||
let statement = "UPDATE _" + snID +
|
||||
" SET " + attr.map(a => a + "=?").join(", ") +
|
||||
" WHERE " + H_struct.VECTOR_CLOCK + "=?";
|
||||
queryResolve(statement, values)
|
||||
.then(result => resolve(data))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
DB.searchData = function (snID, request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let conditionArr = [];
|
||||
if (request.lowerVectorClock || request.upperVectorClock || request.atVectorClock) {
|
||||
if (request.atVectorClock)
|
||||
conditionArr.push(`${H_struct.VECTOR_CLOCK} = '${request.atVectorClock}'`);
|
||||
else if (request.lowerVectorClock && request.upperVectorClock)
|
||||
conditionArr.push(`${H_struct.VECTOR_CLOCK} BETWEEN '${request.lowerVectorClock}' AND '${request.upperVectorClock}'`);
|
||||
else if (request.lowerVectorClock)
|
||||
conditionArr.push(`${H_struct.VECTOR_CLOCK} >= '${request.lowerVectorClock}'`);
|
||||
else if (request.upperVectorClock)
|
||||
conditionArr.push(`${H_struct.VECTOR_CLOCK} <= '${request.upperVectorClock}'`);
|
||||
}
|
||||
if (request.afterTime)
|
||||
conditionArr.push(`${L_struct.LOG_TIME} > ${request.afterTime}`);
|
||||
conditionArr.push(`${H_struct.APPLICATION} = '${request.application}'`);
|
||||
conditionArr.push(`IFNULL(${L_struct.PROXY_ID}, ${H_struct.RECEIVER_ID}) = '${cloud.proxyID(request.receiverID)}'`);
|
||||
if (request.comment)
|
||||
conditionArr.push(`${B_struct.COMMENT} = '${request.comment}'`);
|
||||
if (request.type)
|
||||
conditionArr.push(`${H_struct.TYPE} = '${request.type}'`);
|
||||
if (request.senderID) {
|
||||
if (typeof request.senderID === "string" && request.senderID.includes(','))
|
||||
request.senderID = request.senderID.split(',');
|
||||
if (Array.isArray(request.senderID))
|
||||
conditionArr.push(`${H_struct.SENDER_ID} IN ('${request.senderID.join("', '")}')`);
|
||||
else
|
||||
conditionArr.push(`${H_struct.SENDER_ID} = '${request.senderID}'`);
|
||||
};
|
||||
//console.log(conditionArr);
|
||||
//let attr = Object.keys(H_struct).map(a => H_struct[a]).concat(Object.keys(B_struct).map(a => B_struct[a]));
|
||||
//let statement = "SELECT " + attr.join(", ") + " FROM _" + snID +
|
||||
let statement = "SELECT * FROM _" + snID +
|
||||
" WHERE " + conditionArr.join(" AND ") +
|
||||
" ORDER BY " + (request.afterTime ? L_struct.LOG_TIME : H_struct.VECTOR_CLOCK) +
|
||||
(request.mostRecent ? " DESC LIMIT 1" : "");
|
||||
queryResolve(statement)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
/* Backup service */
|
||||
|
||||
DB.lastLogTime = function (snID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let attr = "MAX(" + L_struct.LOG_TIME + ")";
|
||||
let statement = "SELECT " + attr + " FROM _" + snID;
|
||||
queryResolve(statement)
|
||||
.then(result => resolve(result[0][attr] || 0))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.createGetLastLog = function (snID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.createTable(snID).then(result => {
|
||||
DB.lastLogTime(snID)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.readAllDataStream = function (snID, logtime, callback) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "SELECT * FROM _" + snID +
|
||||
" WHERE " + L_struct.LOG_TIME + ">" + logtime +
|
||||
" ORDER BY " + L_struct.LOG_TIME;
|
||||
queryStream(statement, callback)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.storeData = function (snID, data, updateLogTime = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (updateLogTime)
|
||||
data[L_struct.LOG_TIME] = Date.now();
|
||||
let u_attr = Object.keys(B_struct).map(a => B_struct[a])
|
||||
.concat(Object.keys(L_struct).map(a => L_struct[a]))
|
||||
.concat(Object.keys(T_struct).map(a => T_struct[a]))
|
||||
.concat(Object.keys(F_struct).map(a => F_struct[a]));
|
||||
let attr = Object.keys(H_struct).map(a => H_struct[a]).concat(u_attr);
|
||||
let values = attr.map(a => data[a]);
|
||||
let u_values = u_attr.map(a => data[a]);
|
||||
let statement = "INSERT INTO _" + snID +
|
||||
" (" + attr.join(", ") + ") " +
|
||||
"VALUES (" + attr.map(a => '?').join(", ") + ") " +
|
||||
"ON DUPLICATE KEY UPDATE " + u_attr.map(a => a + "=?").join(", ");
|
||||
queryResolve(statement, values.concat(u_values))
|
||||
.then(result => resolve(data))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.storeTag = function (snID, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let attr = Object.keys(T_struct).map(a => T_struct[a]).concat(L_struct.LOG_TIME);
|
||||
let values = attr.map(a => data[a]).concat(data[H_struct.VECTOR_CLOCK]);
|
||||
let statement = "UPDATE _" + snID +
|
||||
" SET " + attr.map(a => a + "=?").join(", ") +
|
||||
" WHERE " + H_struct.VECTOR_CLOCK + "=?";
|
||||
queryResolve(statement, values)
|
||||
.then(result => resolve(data))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.storeNote = function (snID, data) {
|
||||
let attr = Object.keys(F_struct).map(a => F_struct[a]).concat(L_struct.LOG_TIME);
|
||||
let values = attr.map(a => data[a]).concat(data[H_struct.VECTOR_CLOCK]);
|
||||
let statement = "UPDATE _" + snID +
|
||||
" SET " + attr.map(a => a + "=?").join(", ") +
|
||||
" WHERE " + H_struct.VECTOR_CLOCK + "=?";
|
||||
queryResolve(statement, values)
|
||||
.then(result => resolve(data))
|
||||
.catch(error => reject(error));
|
||||
}
|
||||
|
||||
DB.deleteData = function (snID, vectorClock) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "DELETE FROM _" + snID +
|
||||
" WHERE " + H_struct.VECTOR_CLOCK + "=?";
|
||||
queryResolve(statement, [vectorClock])
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
/* Data clearing */
|
||||
|
||||
DB.clearAuthorisedAppData = function (snID, app, adminID, subAdmins, timestamp) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "DELETE FROM _" + snID +
|
||||
" WHERE ( " + H_struct.TIME + "<? AND " +
|
||||
H_struct.APPLICATION + "=? AND " +
|
||||
T_struct.TAG + " IS NULL )" +
|
||||
(subAdmins.length ? " AND ( " +
|
||||
H_struct.RECEIVER_ID + " != ? OR " +
|
||||
H_struct.SENDER_ID + " NOT IN (" + subAdmins.map(a => "?").join(", ") + ") )" :
|
||||
"");
|
||||
queryResolve(statement, [timestamp, app, adminID].concat(subAdmins))
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
DB.clearUnauthorisedAppData = function (snID, appList, timestamp) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let statement = "DELETE FROM _" + snID +
|
||||
" WHERE " + H_struct.TIME + "<?" +
|
||||
(appList.length ? " AND " +
|
||||
H_struct.APPLICATION + " NOT IN (" + appList.map(a => "?").join(", ") + ")" :
|
||||
"");
|
||||
queryResolve(statement, [timestamp].concat(appList))
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
init: initConnection,
|
||||
query: queryResolve,
|
||||
query_stream: queryStream,
|
||||
DB
|
||||
};
|
||||
580
src/floBlockchainAPI.js
Normal file
580
src/floBlockchainAPI.js
Normal file
@ -0,0 +1,580 @@
|
||||
(function (EXPORTS) { //floBlockchainAPI v2.3.3e
|
||||
/* FLO Blockchain Operator to send/receive data from blockchain using API calls*/
|
||||
'use strict';
|
||||
const floBlockchainAPI = EXPORTS;
|
||||
|
||||
const DEFAULT = {
|
||||
blockchain: floGlobals.blockchain,
|
||||
apiURL: {
|
||||
FLO: ['https://flosight.duckdns.org/', 'https://flosight.ranchimall.net/'],
|
||||
FLO_TEST: ['https://testnet-flosight.duckdns.org', 'https://testnet.flocha.in/']
|
||||
},
|
||||
sendAmt: 0.001,
|
||||
fee: 0.0005,
|
||||
minChangeAmt: 0.0005,
|
||||
receiverID: floGlobals.adminID
|
||||
};
|
||||
|
||||
Object.defineProperties(floBlockchainAPI, {
|
||||
sendAmt: {
|
||||
get: () => DEFAULT.sendAmt,
|
||||
set: amt => !isNaN(amt) ? DEFAULT.sendAmt = amt : null
|
||||
},
|
||||
fee: {
|
||||
get: () => DEFAULT.fee,
|
||||
set: fee => !isNaN(fee) ? DEFAULT.fee = fee : null
|
||||
},
|
||||
defaultReceiver: {
|
||||
get: () => DEFAULT.receiverID,
|
||||
set: floID => DEFAULT.receiverID = floID
|
||||
},
|
||||
blockchain: {
|
||||
get: () => DEFAULT.blockchain
|
||||
}
|
||||
});
|
||||
|
||||
if (floGlobals.sendAmt) floBlockchainAPI.sendAmt = floGlobals.sendAmt;
|
||||
if (floGlobals.fee) floBlockchainAPI.fee = floGlobals.fee;
|
||||
|
||||
Object.defineProperties(floGlobals, {
|
||||
sendAmt: {
|
||||
get: () => DEFAULT.sendAmt,
|
||||
set: amt => !isNaN(amt) ? DEFAULT.sendAmt = amt : null
|
||||
},
|
||||
fee: {
|
||||
get: () => DEFAULT.fee,
|
||||
set: fee => !isNaN(fee) ? DEFAULT.fee = fee : null
|
||||
}
|
||||
});
|
||||
|
||||
const allServerList = new Set(floGlobals.apiURL && floGlobals.apiURL[DEFAULT.blockchain] ? floGlobals.apiURL[DEFAULT.blockchain] : DEFAULT.apiURL[DEFAULT.blockchain]);
|
||||
|
||||
var serverList = Array.from(allServerList);
|
||||
var curPos = floCrypto.randInt(0, serverList.length - 1);
|
||||
|
||||
function fetch_retry(apicall, rm_flosight) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let i = serverList.indexOf(rm_flosight)
|
||||
if (i != -1) serverList.splice(i, 1);
|
||||
curPos = floCrypto.randInt(0, serverList.length - 1);
|
||||
fetch_api(apicall, false)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
|
||||
function fetch_api(apicall, ic = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (serverList.length === 0) {
|
||||
if (ic) {
|
||||
serverList = Array.from(allServerList);
|
||||
curPos = floCrypto.randInt(0, serverList.length - 1);
|
||||
fetch_api(apicall, false)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
} else
|
||||
reject("No floSight server working");
|
||||
} else {
|
||||
let flosight = serverList[curPos];
|
||||
fetch(flosight + apicall).then(response => {
|
||||
if (response.ok)
|
||||
response.json().then(data => resolve(data));
|
||||
else {
|
||||
fetch_retry(apicall, flosight)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
}
|
||||
}).catch(error => {
|
||||
fetch_retry(apicall, flosight)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Object.defineProperties(floBlockchainAPI, {
|
||||
serverList: {
|
||||
get: () => Array.from(serverList)
|
||||
},
|
||||
current_server: {
|
||||
get: () => serverList[curPos]
|
||||
}
|
||||
});
|
||||
|
||||
//Promised function to get data from API
|
||||
const promisedAPI = floBlockchainAPI.promisedAPI = floBlockchainAPI.fetch = function (apicall) {
|
||||
return new Promise((resolve, reject) => {
|
||||
//console.log(apicall);
|
||||
fetch_api(apicall)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
//Get balance for the given Address
|
||||
const getBalance = floBlockchainAPI.getBalance = function (addr) {
|
||||
return new Promise((resolve, reject) => {
|
||||
promisedAPI(`api/addr/${addr}/balance`)
|
||||
.then(balance => resolve(parseFloat(balance)))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
//Send Tx to blockchain
|
||||
const sendTx = floBlockchainAPI.sendTx = function (senderAddr, receiverAddr, sendAmt, privKey, floData = '', strict_utxo = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateASCII(floData))
|
||||
return reject("Invalid FLO_Data: only printable ASCII characters are allowed");
|
||||
else if (!floCrypto.validateFloID(senderAddr))
|
||||
return reject(`Invalid address : ${senderAddr}`);
|
||||
else if (!floCrypto.validateFloID(receiverAddr))
|
||||
return reject(`Invalid address : ${receiverAddr}`);
|
||||
else if (privKey.length < 1 || !floCrypto.verifyPrivKey(privKey, senderAddr))
|
||||
return reject("Invalid Private key!");
|
||||
else if (typeof sendAmt !== 'number' || sendAmt <= 0)
|
||||
return reject(`Invalid sendAmt : ${sendAmt}`);
|
||||
|
||||
getBalance(senderAddr).then(balance => {
|
||||
var fee = DEFAULT.fee;
|
||||
if (balance < sendAmt + fee)
|
||||
return reject("Insufficient FLO balance!");
|
||||
//get unconfirmed tx list
|
||||
promisedAPI(`api/addr/${senderAddr}`).then(result => {
|
||||
readTxs(senderAddr, 0, result.unconfirmedTxApperances).then(result => {
|
||||
let unconfirmedSpent = {};
|
||||
for (let tx of result.items)
|
||||
if (tx.confirmations == 0)
|
||||
for (let vin of tx.vin)
|
||||
if (vin.addr === senderAddr) {
|
||||
if (Array.isArray(unconfirmedSpent[vin.txid]))
|
||||
unconfirmedSpent[vin.txid].push(vin.vout);
|
||||
else
|
||||
unconfirmedSpent[vin.txid] = [vin.vout];
|
||||
}
|
||||
//get utxos list
|
||||
promisedAPI(`api/addr/${senderAddr}/utxo`).then(utxos => {
|
||||
//form/construct the transaction data
|
||||
var trx = bitjs.transaction();
|
||||
var utxoAmt = 0.0;
|
||||
for (var i = utxos.length - 1;
|
||||
(i >= 0) && (utxoAmt < sendAmt + fee); i--) {
|
||||
//use only utxos with confirmations (strict_utxo mode)
|
||||
if (utxos[i].confirmations || !strict_utxo) {
|
||||
if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout))
|
||||
continue; //A transaction has already used the utxo, but is unconfirmed.
|
||||
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
|
||||
utxoAmt += utxos[i].amount;
|
||||
};
|
||||
}
|
||||
if (utxoAmt < sendAmt + fee)
|
||||
reject("Insufficient FLO: Some UTXOs are unconfirmed");
|
||||
else {
|
||||
trx.addoutput(receiverAddr, sendAmt);
|
||||
var change = utxoAmt - sendAmt - fee;
|
||||
if (change > DEFAULT.minChangeAmt)
|
||||
trx.addoutput(senderAddr, change);
|
||||
trx.addflodata(floData.replace(/\n/g, ' '));
|
||||
var signedTxHash = trx.sign(privKey, 1);
|
||||
broadcastTx(signedTxHash)
|
||||
.then(txid => resolve(txid))
|
||||
.catch(error => reject(error))
|
||||
}
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
}
|
||||
|
||||
//Write Data into blockchain
|
||||
floBlockchainAPI.writeData = function (senderAddr, data, privKey, receiverAddr = DEFAULT.receiverID, options = {}) {
|
||||
let strict_utxo = options.strict_utxo === false ? false : true,
|
||||
sendAmt = isNaN(options.sendAmt) ? DEFAULT.sendAmt : options.sendAmt;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof data != "string")
|
||||
data = JSON.stringify(data);
|
||||
sendTx(senderAddr, receiverAddr, sendAmt, privKey, data, strict_utxo)
|
||||
.then(txid => resolve(txid))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
//merge all UTXOs of a given floID into a single UTXO
|
||||
floBlockchainAPI.mergeUTXOs = function (floID, privKey, floData = '') {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateFloID(floID))
|
||||
return reject(`Invalid floID`);
|
||||
if (!floCrypto.verifyPrivKey(privKey, floID))
|
||||
return reject("Invalid Private Key");
|
||||
if (!floCrypto.validateASCII(floData))
|
||||
return reject("Invalid FLO_Data: only printable ASCII characters are allowed");
|
||||
var trx = bitjs.transaction();
|
||||
var utxoAmt = 0.0;
|
||||
var fee = DEFAULT.fee;
|
||||
promisedAPI(`api/addr/${floID}/utxo`).then(utxos => {
|
||||
for (var i = utxos.length - 1; i >= 0; i--)
|
||||
if (utxos[i].confirmations) {
|
||||
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
|
||||
utxoAmt += utxos[i].amount;
|
||||
}
|
||||
trx.addoutput(floID, utxoAmt - fee);
|
||||
trx.addflodata(floData.replace(/\n/g, ' '));
|
||||
var signedTxHash = trx.sign(privKey, 1);
|
||||
broadcastTx(signedTxHash)
|
||||
.then(txid => resolve(txid))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
/**Write data into blockchain from (and/or) to multiple floID
|
||||
* @param {Array} senderPrivKeys List of sender private-keys
|
||||
* @param {string} data FLO data of the txn
|
||||
* @param {Array} receivers List of receivers
|
||||
* @param {boolean} preserveRatio (optional) preserve ratio or equal contribution
|
||||
* @return {Promise}
|
||||
*/
|
||||
floBlockchainAPI.writeDataMultiple = function (senderPrivKeys, data, receivers = [DEFAULT.receiverID], preserveRatio = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!Array.isArray(senderPrivKeys))
|
||||
return reject("Invalid senderPrivKeys: SenderPrivKeys must be Array");
|
||||
if (!preserveRatio) {
|
||||
let tmp = {};
|
||||
let amount = (DEFAULT.sendAmt * receivers.length) / senderPrivKeys.length;
|
||||
senderPrivKeys.forEach(key => tmp[key] = amount);
|
||||
senderPrivKeys = tmp;
|
||||
}
|
||||
if (!Array.isArray(receivers))
|
||||
return reject("Invalid receivers: Receivers must be Array");
|
||||
else {
|
||||
let tmp = {};
|
||||
let amount = DEFAULT.sendAmt;
|
||||
receivers.forEach(floID => tmp[floID] = amount);
|
||||
receivers = tmp
|
||||
}
|
||||
if (typeof data != "string")
|
||||
data = JSON.stringify(data);
|
||||
sendTxMultiple(senderPrivKeys, receivers, data)
|
||||
.then(txid => resolve(txid))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
/**Send Tx from (and/or) to multiple floID
|
||||
* @param {Array or Object} senderPrivKeys List of sender private-key (optional: with coins to be sent)
|
||||
* @param {Object} receivers List of receivers with respective amount to be sent
|
||||
* @param {string} floData FLO data of the txn
|
||||
* @return {Promise}
|
||||
*/
|
||||
const sendTxMultiple = floBlockchainAPI.sendTxMultiple = function (senderPrivKeys, receivers, floData = '') {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateASCII(floData))
|
||||
return reject("Invalid FLO_Data: only printable ASCII characters are allowed");
|
||||
let senders = {},
|
||||
preserveRatio;
|
||||
//check for argument validations
|
||||
try {
|
||||
let invalids = {
|
||||
InvalidSenderPrivKeys: [],
|
||||
InvalidSenderAmountFor: [],
|
||||
InvalidReceiverIDs: [],
|
||||
InvalidReceiveAmountFor: []
|
||||
}
|
||||
let inputVal = 0,
|
||||
outputVal = 0;
|
||||
//Validate sender privatekeys (and send amount if passed)
|
||||
//conversion when only privateKeys are passed (preserveRatio mode)
|
||||
if (Array.isArray(senderPrivKeys)) {
|
||||
senderPrivKeys.forEach(key => {
|
||||
try {
|
||||
if (!key)
|
||||
invalids.InvalidSenderPrivKeys.push(key);
|
||||
else {
|
||||
let floID = floCrypto.getFloID(key);
|
||||
senders[floID] = {
|
||||
wif: key
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
invalids.InvalidSenderPrivKeys.push(key)
|
||||
}
|
||||
})
|
||||
preserveRatio = true;
|
||||
}
|
||||
//conversion when privatekeys are passed with send amount
|
||||
else {
|
||||
for (let key in senderPrivKeys) {
|
||||
try {
|
||||
if (!key)
|
||||
invalids.InvalidSenderPrivKeys.push(key);
|
||||
else {
|
||||
if (typeof senderPrivKeys[key] !== 'number' || senderPrivKeys[key] <= 0)
|
||||
invalids.InvalidSenderAmountFor.push(key);
|
||||
else
|
||||
inputVal += senderPrivKeys[key];
|
||||
let floID = floCrypto.getFloID(key);
|
||||
senders[floID] = {
|
||||
wif: key,
|
||||
coins: senderPrivKeys[key]
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
invalids.InvalidSenderPrivKeys.push(key)
|
||||
}
|
||||
}
|
||||
preserveRatio = false;
|
||||
}
|
||||
//Validate the receiver IDs and receive amount
|
||||
for (let floID in receivers) {
|
||||
if (!floCrypto.validateFloID(floID))
|
||||
invalids.InvalidReceiverIDs.push(floID);
|
||||
if (typeof receivers[floID] !== 'number' || receivers[floID] <= 0)
|
||||
invalids.InvalidReceiveAmountFor.push(floID);
|
||||
else
|
||||
outputVal += receivers[floID];
|
||||
}
|
||||
//Reject if any invalids are found
|
||||
for (let i in invalids)
|
||||
if (!invalids[i].length)
|
||||
delete invalids[i];
|
||||
if (Object.keys(invalids).length)
|
||||
return reject(invalids);
|
||||
//Reject if given inputVal and outputVal are not equal
|
||||
if (!preserveRatio && inputVal != outputVal)
|
||||
return reject(`Input Amount (${inputVal}) not equal to Output Amount (${outputVal})`);
|
||||
} catch (error) {
|
||||
return reject(error)
|
||||
}
|
||||
//Get balance of senders
|
||||
let promises = [];
|
||||
for (let floID in senders)
|
||||
promises.push(getBalance(floID));
|
||||
Promise.all(promises).then(results => {
|
||||
let totalBalance = 0,
|
||||
totalFee = DEFAULT.fee,
|
||||
balance = {};
|
||||
//Divide fee among sender if not for preserveRatio
|
||||
if (!preserveRatio)
|
||||
var dividedFee = totalFee / Object.keys(senders).length;
|
||||
//Check if balance of each sender is sufficient enough
|
||||
let insufficient = [];
|
||||
for (let floID in senders) {
|
||||
balance[floID] = parseFloat(results.shift());
|
||||
if (isNaN(balance[floID]) || (preserveRatio && balance[floID] <= totalFee) ||
|
||||
(!preserveRatio && balance[floID] < senders[floID].coins + dividedFee))
|
||||
insufficient.push(floID);
|
||||
totalBalance += balance[floID];
|
||||
}
|
||||
if (insufficient.length)
|
||||
return reject({
|
||||
InsufficientBalance: insufficient
|
||||
})
|
||||
//Calculate totalSentAmount and check if totalBalance is sufficient
|
||||
let totalSendAmt = totalFee;
|
||||
for (let floID in receivers)
|
||||
totalSendAmt += receivers[floID];
|
||||
if (totalBalance < totalSendAmt)
|
||||
return reject("Insufficient total Balance");
|
||||
//Get the UTXOs of the senders
|
||||
let promises = [];
|
||||
for (let floID in senders)
|
||||
promises.push(promisedAPI(`api/addr/${floID}/utxo`));
|
||||
Promise.all(promises).then(results => {
|
||||
let wifSeq = [];
|
||||
var trx = bitjs.transaction();
|
||||
for (let floID in senders) {
|
||||
let utxos = results.shift();
|
||||
let sendAmt;
|
||||
if (preserveRatio) {
|
||||
let ratio = (balance[floID] / totalBalance);
|
||||
sendAmt = totalSendAmt * ratio;
|
||||
} else
|
||||
sendAmt = senders[floID].coins + dividedFee;
|
||||
let wif = senders[floID].wif;
|
||||
let utxoAmt = 0.0;
|
||||
for (let i = utxos.length - 1;
|
||||
(i >= 0) && (utxoAmt < sendAmt); i--) {
|
||||
if (utxos[i].confirmations) {
|
||||
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
|
||||
wifSeq.push(wif);
|
||||
utxoAmt += utxos[i].amount;
|
||||
}
|
||||
}
|
||||
if (utxoAmt < sendAmt)
|
||||
return reject("Insufficient balance:" + floID);
|
||||
let change = (utxoAmt - sendAmt);
|
||||
if (change > 0)
|
||||
trx.addoutput(floID, change);
|
||||
}
|
||||
for (let floID in receivers)
|
||||
trx.addoutput(floID, receivers[floID]);
|
||||
trx.addflodata(floData.replace(/\n/g, ' '));
|
||||
for (let i = 0; i < wifSeq.length; i++)
|
||||
trx.signinput(i, wifSeq[i], 1);
|
||||
var signedTxHash = trx.serialize();
|
||||
broadcastTx(signedTxHash)
|
||||
.then(txid => resolve(txid))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
//Broadcast signed Tx in blockchain using API
|
||||
const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (signedTxHash.length < 1)
|
||||
return reject("Empty Signature");
|
||||
var url = serverList[curPos] + 'api/tx/send';
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: `{"rawtx":"${signedTxHash}"}`
|
||||
}).then(response => {
|
||||
if (response.ok)
|
||||
response.json().then(data => resolve(data.txid.result));
|
||||
else
|
||||
response.text().then(data => resolve(data));
|
||||
}).catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
|
||||
floBlockchainAPI.getTx = function (txid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
promisedAPI(`api/tx/${txid}`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
//Read Txs of Address between from and to
|
||||
const readTxs = floBlockchainAPI.readTxs = function (addr, from, to) {
|
||||
return new Promise((resolve, reject) => {
|
||||
promisedAPI(`api/addrs/${addr}/txs?from=${from}&to=${to}`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error))
|
||||
});
|
||||
}
|
||||
|
||||
//Read All Txs of Address (newest first)
|
||||
floBlockchainAPI.readAllTxs = function (addr) {
|
||||
return new Promise((resolve, reject) => {
|
||||
promisedAPI(`api/addrs/${addr}/txs?from=0&to=1`).then(response => {
|
||||
promisedAPI(`api/addrs/${addr}/txs?from=0&to=${response.totalItems}0`)
|
||||
.then(response => resolve(response.items))
|
||||
.catch(error => reject(error));
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
}
|
||||
|
||||
/*Read flo Data from txs of given Address
|
||||
options can be used to filter data
|
||||
limit : maximum number of filtered data (default = 1000, negative = no limit)
|
||||
ignoreOld : ignore old txs (default = 0)
|
||||
sentOnly : filters only sent data
|
||||
receivedOnly: filters only received data
|
||||
pattern : filters data that with JSON pattern
|
||||
filter : custom filter funtion for floData (eg . filter: d => {return d[0] == '$'})
|
||||
tx : (boolean) resolve tx data or not (resolves an Array of Object with tx details)
|
||||
sender : flo-id(s) of sender
|
||||
receiver : flo-id(s) of receiver
|
||||
*/
|
||||
floBlockchainAPI.readData = function (addr, options = {}) {
|
||||
options.limit = options.limit || 0;
|
||||
options.ignoreOld = options.ignoreOld || 0;
|
||||
if (typeof options.senders === "string") options.senders = [options.senders];
|
||||
if (typeof options.receivers === "string") options.receivers = [options.receivers];
|
||||
return new Promise((resolve, reject) => {
|
||||
promisedAPI(`api/addrs/${addr}/txs?from=0&to=1`).then(response => {
|
||||
var newItems = response.totalItems - options.ignoreOld;
|
||||
promisedAPI(`api/addrs/${addr}/txs?from=0&to=${newItems * 2}`).then(response => {
|
||||
if (options.limit <= 0)
|
||||
options.limit = response.items.length;
|
||||
var filteredData = [];
|
||||
let numToRead = response.totalItems - options.ignoreOld,
|
||||
unconfirmedCount = 0;
|
||||
for (let i = 0; i < numToRead && filteredData.length < options.limit; i++) {
|
||||
if (!response.items[i].confirmations) { //unconfirmed transactions
|
||||
unconfirmedCount++;
|
||||
if (numToRead < response.items[i].length)
|
||||
numToRead++;
|
||||
continue;
|
||||
}
|
||||
if (options.pattern) {
|
||||
try {
|
||||
let jsonContent = JSON.parse(response.items[i].floData);
|
||||
if (!Object.keys(jsonContent).includes(options.pattern))
|
||||
continue;
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (options.sentOnly) {
|
||||
let flag = false;
|
||||
for (let vin of response.items[i].vin)
|
||||
if (vin.addr === addr) {
|
||||
flag = true;
|
||||
break;
|
||||
}
|
||||
if (!flag) continue;
|
||||
}
|
||||
if (Array.isArray(options.senders)) {
|
||||
let flag = false;
|
||||
for (let vin of response.items[i].vin)
|
||||
if (options.senders.includes(vin.addr)) {
|
||||
flag = true;
|
||||
break;
|
||||
}
|
||||
if (!flag) continue;
|
||||
}
|
||||
if (options.receivedOnly) {
|
||||
let flag = false;
|
||||
for (let vout of response.items[i].vout)
|
||||
if (vout.scriptPubKey.addresses[0] === addr) {
|
||||
flag = true;
|
||||
break;
|
||||
}
|
||||
if (!flag) continue;
|
||||
}
|
||||
if (Array.isArray(options.receivers)) {
|
||||
let flag = false;
|
||||
for (let vout of response.items[i].vout)
|
||||
if (options.receivers.includes(vout.scriptPubKey.addresses[0])) {
|
||||
flag = true;
|
||||
break;
|
||||
}
|
||||
if (!flag) continue;
|
||||
}
|
||||
if (options.filter && !options.filter(response.items[i].floData))
|
||||
continue;
|
||||
|
||||
if (options.tx) {
|
||||
let d = {}
|
||||
d.txid = response.items[i].txid;
|
||||
d.time = response.items[i].time;
|
||||
d.blockheight = response.items[i].blockheight;
|
||||
d.senders = new Set(response.items[i].vin.map(v => v.addr));
|
||||
d.receivers = new Set(response.items[i].vout.map(v => v.scriptPubKey.addresses[0]));
|
||||
d.data = response.items[i].floData;
|
||||
filteredData.push(d);
|
||||
} else
|
||||
filteredData.push(response.items[i].floData);
|
||||
}
|
||||
resolve({
|
||||
totalTxs: response.totalItems - unconfirmedCount,
|
||||
data: filteredData
|
||||
});
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
})('object' === typeof module ? module.exports : window.floBlockchainAPI = {});
|
||||
370
src/floCrypto.js
Normal file
370
src/floCrypto.js
Normal file
@ -0,0 +1,370 @@
|
||||
(function(EXPORTS) { //floCrypto v2.3.3
|
||||
/* FLO Crypto Operators */
|
||||
'use strict';
|
||||
const floCrypto = EXPORTS;
|
||||
|
||||
const p = BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16);
|
||||
const ecparams = EllipticCurve.getSECCurveByName("secp256k1");
|
||||
const ascii_alternatives = `‘ '\n’ '\n“ "\n” "\n– --\n— ---\n≥ >=\n≤ <=\n≠ !=\n× *\n÷ /\n← <-\n→ ->\n↔ <->\n⇒ =>\n⇐ <=\n⇔ <=>`;
|
||||
const exponent1 = () => p.add(BigInteger.ONE).divide(BigInteger("4"));
|
||||
|
||||
function calculateY(x) {
|
||||
let exp = exponent1();
|
||||
// x is x value of public key in BigInteger format without 02 or 03 or 04 prefix
|
||||
return x.modPow(BigInteger("3"), p).add(BigInteger("7")).mod(p).modPow(exp, p)
|
||||
}
|
||||
|
||||
function getUncompressedPublicKey(compressedPublicKey) {
|
||||
// Fetch x from compressedPublicKey
|
||||
let pubKeyBytes = Crypto.util.hexToBytes(compressedPublicKey);
|
||||
const prefix = pubKeyBytes.shift() // remove prefix
|
||||
let prefix_modulus = prefix % 2;
|
||||
pubKeyBytes.unshift(0) // add prefix 0
|
||||
let x = new BigInteger(pubKeyBytes)
|
||||
let xDecimalValue = x.toString()
|
||||
// Fetch y
|
||||
let y = calculateY(x);
|
||||
let yDecimalValue = y.toString();
|
||||
// verify y value
|
||||
let resultBigInt = y.mod(BigInteger("2"));
|
||||
let check = resultBigInt.toString() % 2;
|
||||
if (prefix_modulus !== check)
|
||||
yDecimalValue = y.negate().mod(p).toString();
|
||||
return {
|
||||
x: xDecimalValue,
|
||||
y: yDecimalValue
|
||||
};
|
||||
}
|
||||
|
||||
function getSenderPublicKeyString() {
|
||||
let privateKey = ellipticCurveEncryption.senderRandom();
|
||||
var senderPublicKeyString = ellipticCurveEncryption.senderPublicString(privateKey);
|
||||
return {
|
||||
privateKey: privateKey,
|
||||
senderPublicKeyString: senderPublicKeyString
|
||||
}
|
||||
}
|
||||
|
||||
function deriveSharedKeySender(receiverPublicKeyHex, senderPrivateKey) {
|
||||
let receiverPublicKeyString = getUncompressedPublicKey(receiverPublicKeyHex);
|
||||
var senderDerivedKey = ellipticCurveEncryption.senderSharedKeyDerivation(
|
||||
receiverPublicKeyString.x, receiverPublicKeyString.y, senderPrivateKey);
|
||||
return senderDerivedKey;
|
||||
}
|
||||
|
||||
function deriveSharedKeyReceiver(senderPublicKeyString, receiverPrivateKey) {
|
||||
return ellipticCurveEncryption.receiverSharedKeyDerivation(
|
||||
senderPublicKeyString.XValuePublicString, senderPublicKeyString.YValuePublicString, receiverPrivateKey);
|
||||
}
|
||||
|
||||
function getReceiverPublicKeyString(privateKey) {
|
||||
return ellipticCurveEncryption.receiverPublicString(privateKey);
|
||||
}
|
||||
|
||||
function wifToDecimal(pk_wif, isPubKeyCompressed = false) {
|
||||
let pk = Bitcoin.Base58.decode(pk_wif)
|
||||
pk.shift()
|
||||
pk.splice(-4, 4)
|
||||
//If the private key corresponded to a compressed public key, also drop the last byte (it should be 0x01).
|
||||
if (isPubKeyCompressed == true) pk.pop()
|
||||
pk.unshift(0)
|
||||
let privateKeyDecimal = BigInteger(pk).toString()
|
||||
let privateKeyHex = Crypto.util.bytesToHex(pk)
|
||||
return {
|
||||
privateKeyDecimal: privateKeyDecimal,
|
||||
privateKeyHex: privateKeyHex
|
||||
}
|
||||
}
|
||||
|
||||
//generate a random Interger within range
|
||||
floCrypto.randInt = function(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
//generate a random String within length (options : alphaNumeric chars only)
|
||||
floCrypto.randString = function(length, alphaNumeric = true) {
|
||||
var result = '';
|
||||
var characters = alphaNumeric ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' :
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+-./*?@#&$<>=[]{}():';
|
||||
for (var i = 0; i < length; i++)
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
return result;
|
||||
}
|
||||
|
||||
//Encrypt Data using public-key
|
||||
floCrypto.encryptData = function(data, receiverPublicKeyHex) {
|
||||
var senderECKeyData = getSenderPublicKeyString();
|
||||
var senderDerivedKey = deriveSharedKeySender(receiverPublicKeyHex, senderECKeyData.privateKey);
|
||||
let senderKey = senderDerivedKey.XValue + senderDerivedKey.YValue;
|
||||
let secret = Crypto.AES.encrypt(data, senderKey);
|
||||
return {
|
||||
secret: secret,
|
||||
senderPublicKeyString: senderECKeyData.senderPublicKeyString
|
||||
};
|
||||
}
|
||||
|
||||
//Decrypt Data using private-key
|
||||
floCrypto.decryptData = function(data, privateKeyHex) {
|
||||
var receiverECKeyData = {};
|
||||
if (typeof privateKeyHex !== "string") throw new Error("No private key found.");
|
||||
let privateKey = wifToDecimal(privateKeyHex, true);
|
||||
if (typeof privateKey.privateKeyDecimal !== "string") throw new Error("Failed to detremine your private key.");
|
||||
receiverECKeyData.privateKey = privateKey.privateKeyDecimal;
|
||||
var receiverDerivedKey = deriveSharedKeyReceiver(data.senderPublicKeyString, receiverECKeyData.privateKey);
|
||||
let receiverKey = receiverDerivedKey.XValue + receiverDerivedKey.YValue;
|
||||
let decryptMsg = Crypto.AES.decrypt(data.secret, receiverKey);
|
||||
return decryptMsg;
|
||||
}
|
||||
|
||||
//Sign data using private-key
|
||||
floCrypto.signData = function(data, privateKeyHex) {
|
||||
var key = new Bitcoin.ECKey(privateKeyHex);
|
||||
var messageHash = Crypto.SHA256(data);
|
||||
var messageSign = Bitcoin.ECDSA.sign(messageHash, key.priv);
|
||||
var sighex = Crypto.util.bytesToHex(messageSign);
|
||||
return sighex;
|
||||
}
|
||||
|
||||
//Verify signatue of the data using public-key
|
||||
floCrypto.verifySign = function(data, signatureHex, publicKeyHex) {
|
||||
var msgHash = Crypto.SHA256(data);
|
||||
var sigBytes = Crypto.util.hexToBytes(signatureHex);
|
||||
var publicKeyPoint = ecparams.getCurve().decodePointHex(publicKeyHex);
|
||||
var verify = Bitcoin.ECDSA.verify(msgHash, sigBytes, publicKeyPoint);
|
||||
return verify;
|
||||
}
|
||||
|
||||
//Generates a new flo ID and returns private-key, public-key and floID
|
||||
const generateNewID = floCrypto.generateNewID = function() {
|
||||
var key = new Bitcoin.ECKey(false);
|
||||
key.setCompressed(true);
|
||||
return {
|
||||
floID: key.getBitcoinAddress(),
|
||||
pubKey: key.getPubKeyHex(),
|
||||
privKey: key.getBitcoinWalletImportFormat()
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(floCrypto, 'newID', {
|
||||
get: () => generateNewID()
|
||||
});
|
||||
|
||||
//Returns public-key from private-key
|
||||
floCrypto.getPubKeyHex = function(privateKeyHex) {
|
||||
if (!privateKeyHex)
|
||||
return null;
|
||||
var key = new Bitcoin.ECKey(privateKeyHex);
|
||||
if (key.priv == null)
|
||||
return null;
|
||||
key.setCompressed(true);
|
||||
return key.getPubKeyHex();
|
||||
}
|
||||
|
||||
//Returns flo-ID from public-key or private-key
|
||||
floCrypto.getFloID = function(keyHex) {
|
||||
if (!keyHex)
|
||||
return null;
|
||||
try {
|
||||
var key = new Bitcoin.ECKey(keyHex);
|
||||
if (key.priv == null)
|
||||
key.setPub(keyHex);
|
||||
return key.getBitcoinAddress();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//Verify the private-key for the given public-key or flo-ID
|
||||
floCrypto.verifyPrivKey = function(privateKeyHex, pubKey_floID, isfloID = true) {
|
||||
if (!privateKeyHex || !pubKey_floID)
|
||||
return false;
|
||||
try {
|
||||
var key = new Bitcoin.ECKey(privateKeyHex);
|
||||
if (key.priv == null)
|
||||
return false;
|
||||
key.setCompressed(true);
|
||||
if (isfloID && pubKey_floID == key.getBitcoinAddress())
|
||||
return true;
|
||||
else if (!isfloID && pubKey_floID == key.getPubKeyHex())
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//Check if the given flo-id is valid or not
|
||||
floCrypto.validateFloID = function(floID) {
|
||||
if (!floID)
|
||||
return false;
|
||||
try {
|
||||
let addr = new Bitcoin.Address(floID);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//Check if the given address (any blockchain) is valid or not
|
||||
floCrypto.validateAddr = function(address, std = true, bech = true) {
|
||||
if (address.length == 34) { //legacy or segwit encoding
|
||||
if (std === false)
|
||||
return false;
|
||||
let decode = bitjs.Base58.decode(address);
|
||||
var raw = decode.slice(0, decode.length - 4),
|
||||
checksum = decode.slice(decode.length - 4);
|
||||
var hash = Crypto.SHA256(Crypto.SHA256(raw, {
|
||||
asBytes: true
|
||||
}), {
|
||||
asBytes: true
|
||||
});
|
||||
if (hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3])
|
||||
return false;
|
||||
else if (std === true || (!Array.isArray(std) && std === raw[0]) || (Array.isArray(std) && std.includes(raw[0])))
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
} else if (address.length == 42 || address.length == 62) { //bech encoding
|
||||
if (bech === false)
|
||||
return false;
|
||||
let decode = coinjs.bech32_decode(address);
|
||||
if (!decode)
|
||||
return false;
|
||||
var raw = decode.data;
|
||||
if (bech === true || (!Array.isArray(bech) && bech === raw[0]) || (Array.isArray(bech) && bech.includes(raw[0])))
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
} else //unknown length
|
||||
return false;
|
||||
}
|
||||
|
||||
floCrypto.verifyPubKey = function(pubKeyHex, address) {
|
||||
let pub_hash = Crypto.util.bytesToHex(ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubKeyHex), {
|
||||
asBytes: true
|
||||
})));
|
||||
if (address.length == 34) { //legacy encoding
|
||||
let decode = bitjs.Base58.decode(address);
|
||||
var raw = decode.slice(0, decode.length - 4),
|
||||
checksum = decode.slice(decode.length - 4);
|
||||
var hash = Crypto.SHA256(Crypto.SHA256(raw, {
|
||||
asBytes: true
|
||||
}), {
|
||||
asBytes: true
|
||||
});
|
||||
if (hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3])
|
||||
return false;
|
||||
raw.shift();
|
||||
return pub_hash === Crypto.util.bytesToHex(raw);
|
||||
} else if (address.length == 42 || address.length == 62) { //bech encoding
|
||||
let decode = coinjs.bech32_decode(address);
|
||||
if (!decode)
|
||||
return false;
|
||||
var raw = decode.data;
|
||||
raw.shift();
|
||||
raw = coinjs.bech32_convert(raw, 5, 8, false);
|
||||
return pub_hash === Crypto.util.bytesToHex(raw);
|
||||
} else //unknown length
|
||||
return false;
|
||||
}
|
||||
|
||||
//Split the str using shamir's Secret and Returns the shares
|
||||
floCrypto.createShamirsSecretShares = function(str, total_shares, threshold_limit) {
|
||||
try {
|
||||
if (str.length > 0) {
|
||||
var strHex = shamirSecretShare.str2hex(str);
|
||||
var shares = shamirSecretShare.share(strHex, total_shares, threshold_limit);
|
||||
return shares;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
//Returns the retrived secret by combining the shamirs shares
|
||||
const retrieveShamirSecret = floCrypto.retrieveShamirSecret = function(sharesArray) {
|
||||
try {
|
||||
if (sharesArray.length > 0) {
|
||||
var comb = shamirSecretShare.combine(sharesArray.slice(0, sharesArray.length));
|
||||
comb = shamirSecretShare.hex2str(comb);
|
||||
return comb;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//Verifies the shares and str
|
||||
floCrypto.verifyShamirsSecret = function(sharesArray, str) {
|
||||
if (!str)
|
||||
return null;
|
||||
else if (retrieveShamirSecret(sharesArray) === str)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
const validateASCII = floCrypto.validateASCII = function(string, bool = true) {
|
||||
if (typeof string !== "string")
|
||||
return null;
|
||||
if (bool) {
|
||||
let x;
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
x = string.charCodeAt(i);
|
||||
if (x < 32 || x > 127)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
let x, invalids = {};
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
x = string.charCodeAt(i);
|
||||
if (x < 32 || x > 127)
|
||||
if (x in invalids)
|
||||
invalids[string[i]].push(i)
|
||||
else
|
||||
invalids[string[i]] = [i];
|
||||
}
|
||||
if (Object.keys(invalids).length)
|
||||
return invalids;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
floCrypto.convertToASCII = function(string, mode = 'soft-remove') {
|
||||
let chars = validateASCII(string, false);
|
||||
if (chars === true)
|
||||
return string;
|
||||
else if (chars === null)
|
||||
return null;
|
||||
let convertor, result = string,
|
||||
refAlt = {};
|
||||
ascii_alternatives.split('\n').forEach(a => refAlt[a[0]] = a.slice(2));
|
||||
mode = mode.toLowerCase();
|
||||
if (mode === "hard-unicode")
|
||||
convertor = (c) => `\\u${('000'+c.charCodeAt().toString(16)).slice(-4)}`;
|
||||
else if (mode === "soft-unicode")
|
||||
convertor = (c) => refAlt[c] || `\\u${('000'+c.charCodeAt().toString(16)).slice(-4)}`;
|
||||
else if (mode === "hard-remove")
|
||||
convertor = c => "";
|
||||
else if (mode === "soft-remove")
|
||||
convertor = c => refAlt[c] || "";
|
||||
else
|
||||
return null;
|
||||
for (let c in chars)
|
||||
result = result.replaceAll(c, convertor(c));
|
||||
return result;
|
||||
}
|
||||
|
||||
floCrypto.revertUnicode = function(string) {
|
||||
return string.replace(/\\u[\dA-F]{4}/gi,
|
||||
m => String.fromCharCode(parseInt(m.replace(/\\u/g, ''), 16)));
|
||||
}
|
||||
|
||||
})('object' === typeof module ? module.exports : window.floCrypto = {});
|
||||
24
src/floGlobals.js
Normal file
24
src/floGlobals.js
Normal file
@ -0,0 +1,24 @@
|
||||
const floGlobals = {
|
||||
|
||||
//Required for all
|
||||
blockchain: "FLO",
|
||||
|
||||
SNStorageID: "FNaN9McoBAEFUjkRmNQRYLmBF8SpS7Tgfk",
|
||||
|
||||
//Required for Supernode operations
|
||||
supernodes: {}, //each supnernode must be stored as floID : {uri:<uri>,pubKey:<publicKey>}
|
||||
appList: {},
|
||||
appSubAdmins: {},
|
||||
sn_config: {}
|
||||
/* List of supernode configurations (all blockchain controlled by SNStorageID)
|
||||
backupDepth - (Interger) Number of backup nodes
|
||||
refreshDelay - (Interger) Count of requests for triggering read-blockchain and autodelete
|
||||
deleteDelay - (Interger) Maximum duration (milliseconds) an unauthorised data is stored
|
||||
errorFeedback - (Boolean) Send error (if any) feedback to the requestor
|
||||
delayDelta - (Interger) Maximum allowed delay from the data-time
|
||||
blockInterval - (Interger) Duration (milliseconds) of a single block (for backup sync)
|
||||
*/
|
||||
};
|
||||
|
||||
(typeof global !== "undefined" ? global : window).cryptocoin = floGlobals.blockchain;
|
||||
('object' === typeof module) ? module.exports = floGlobals : null;
|
||||
37
src/keys.js
Normal file
37
src/keys.js
Normal file
@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
const PRIV_EKEY_MIN = 32,
|
||||
PRIV_EKEY_MAX = 48;
|
||||
|
||||
var node_priv, e_key, node_id, node_pub; //containers for node-key wrapper
|
||||
const _x = {
|
||||
get node_priv() {
|
||||
if (!node_priv || !e_key)
|
||||
throw Error("keys not set");
|
||||
return Crypto.AES.decrypt(node_priv, e_key);
|
||||
},
|
||||
set node_priv(key) {
|
||||
node_pub = floCrypto.getPubKeyHex(key);
|
||||
node_id = floCrypto.getFloID(node_pub);
|
||||
if (!key || !node_pub || !node_id)
|
||||
throw Error("Invalid Keys");
|
||||
let n = floCrypto.randInt(PRIV_EKEY_MIN, PRIV_EKEY_MAX)
|
||||
e_key = floCrypto.randString(n);
|
||||
node_priv = Crypto.AES.encrypt(key, e_key);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
set node_priv(key) {
|
||||
_x.node_priv = key;
|
||||
},
|
||||
get node_priv() {
|
||||
return _x.node_priv;
|
||||
},
|
||||
get node_id() {
|
||||
return node_id;
|
||||
},
|
||||
get node_pub() {
|
||||
return node_pub;
|
||||
}
|
||||
}
|
||||
9345
src/lib.js
Normal file
9345
src/lib.js
Normal file
File diff suppressed because it is too large
Load Diff
288
src/main.js
Normal file
288
src/main.js
Normal file
@ -0,0 +1,288 @@
|
||||
global.floGlobals = require("./floGlobals");
|
||||
require('./set_globals');
|
||||
require('./lib');
|
||||
global.cloud = require('./cloud');
|
||||
global.floCrypto = require('./floCrypto');
|
||||
global.floBlockchainAPI = require('./floBlockchainAPI');
|
||||
const Database = require("./database");
|
||||
const intra = require('./backup/intra');
|
||||
const Server = require('./server');
|
||||
const keys = require("./keys");
|
||||
const { INTERVAL_REFRESH_TIME } = require("./_constants");
|
||||
|
||||
const DB = Database.DB;
|
||||
|
||||
function startNode() {
|
||||
|
||||
const config = require(`../args/config.json`);
|
||||
let _pass;
|
||||
for (let arg of process.argv)
|
||||
if (/^-password=/i.test(arg))
|
||||
_pass = arg.split(/=(.*)/s)[1];
|
||||
try {
|
||||
let _tmp = require(`../args/keys.json`);
|
||||
_tmp = floCrypto.retrieveShamirSecret(_tmp);
|
||||
if (!_pass) {
|
||||
console.error('Password not entered!');
|
||||
process.exit(1);
|
||||
}
|
||||
keys.node_priv = Crypto.AES.decrypt(_tmp, _pass);
|
||||
} catch (error) {
|
||||
console.error('Unable to load private key!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.info("Logged in as", keys.node_id);
|
||||
|
||||
//DB connect
|
||||
Database.init(config["sql_user"], config["sql_pwd"], config["sql_db"], config["sql_host"]).then(db => {
|
||||
console.info("Connected to Database");
|
||||
loadBase().then(base => {
|
||||
console.log("Load Database successful");
|
||||
//Set base data from DB to floGlobals
|
||||
floGlobals.supernodes = base.supernodes;
|
||||
floGlobals.sn_config = base.sn_config;
|
||||
floGlobals.appList = base.appList;
|
||||
floGlobals.appSubAdmins = base.appSubAdmins;
|
||||
floGlobals.appTrustedIDs = base.appTrustedIDs;
|
||||
refreshData.base = base;
|
||||
refreshData.invoke(null)
|
||||
.then(_ => intra.reconnectNextNode()).catch(_ => null);
|
||||
//Start Server
|
||||
const server = new Server(config["port"]);
|
||||
server.refresher = refreshData;
|
||||
intra.refresher = refreshData;
|
||||
}).catch(error => console.error(error));
|
||||
}).catch(error => console.error(error));
|
||||
};
|
||||
|
||||
function loadBase() {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.createBase().then(result => {
|
||||
DB.getBase()
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error));
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
const refreshData = {
|
||||
count: null,
|
||||
base: null,
|
||||
refresh_instance: null,
|
||||
invoke(flag = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const self = this;
|
||||
console.info("Refresher processor has started at " + Date());
|
||||
if (self.refresh_instance !== null) {
|
||||
clearInterval(self.refresh_instance);
|
||||
self.refresh_instance = null;
|
||||
}
|
||||
refreshBlockchainData(self.base, flag).then(result => {
|
||||
console.log(result);
|
||||
self.count = floGlobals.sn_config.refreshDelay;
|
||||
diskCleanUp(self.base)
|
||||
.then(result => console.log(result))
|
||||
.catch(warn => console.warn(warn))
|
||||
.finally(_ => {
|
||||
console.info("Refresher processor has finished at " + Date());
|
||||
resolve(true);
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
reject(false);
|
||||
}).finally(_ => {
|
||||
self.refresh_instance = setInterval(() => {
|
||||
refreshBlockchainData(self.base, true)
|
||||
.then(result => console.log(result))
|
||||
.catch(error => console.error(error))
|
||||
}, INTERVAL_REFRESH_TIME)
|
||||
});
|
||||
});
|
||||
},
|
||||
get countdown() {
|
||||
this.count--;
|
||||
if (this.count <= 0)
|
||||
this.invoke().then(_ => null).catch(_ => null);
|
||||
}
|
||||
};
|
||||
|
||||
function refreshBlockchainData(base, flag) {
|
||||
return new Promise((resolve, reject) => {
|
||||
readSupernodeConfigFromAPI(base, flag).then(result => {
|
||||
console.log(result);
|
||||
cloud(floGlobals.SNStorageID, Object.keys(floGlobals.supernodes));
|
||||
console.log("NodeList (ordered):", cloud.order);
|
||||
readAppSubAdminListFromAPI(base)
|
||||
.then(result => console.log(result))
|
||||
.catch(warn => console.warn(warn))
|
||||
.finally(_ => resolve("Refreshed Data from blockchain"));
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
function readSupernodeConfigFromAPI(base, flag) {
|
||||
return new Promise((resolve, reject) => {
|
||||
floBlockchainAPI.readData(floGlobals.SNStorageID, {
|
||||
ignoreOld: base.lastTx[floGlobals.SNStorageID],
|
||||
sentOnly: true,
|
||||
pattern: "SuperNodeStorage"
|
||||
}).then(result => {
|
||||
let promises = [],
|
||||
node_change = {},
|
||||
node_update = new Set();
|
||||
result.data.reverse().forEach(data => {
|
||||
var content = JSON.parse(data).SuperNodeStorage;
|
||||
if (content.removeNodes)
|
||||
for (let sn of content.removeNodes) {
|
||||
promises.push(DB.rmSuperNode(sn));
|
||||
delete base.supernodes[sn];
|
||||
if (node_change[sn] === true)
|
||||
delete node_change[sn];
|
||||
else
|
||||
node_change[sn] = false;
|
||||
};
|
||||
if (content.newNodes)
|
||||
for (let sn in content.newNodes) {
|
||||
promises.push(DB.addSuperNode(sn, content.newNodes[sn].pubKey, content.newNodes[sn].uri));
|
||||
base.supernodes[sn] = {
|
||||
pubKey: content.newNodes[sn].pubKey,
|
||||
uri: content.newNodes[sn].uri
|
||||
};
|
||||
if (node_change[sn] === false)
|
||||
delete node_change[sn];
|
||||
else
|
||||
node_change[sn] = true;
|
||||
};
|
||||
if (content.updateNodes)
|
||||
for (let sn in content.updateNodes) {
|
||||
promises.push(DB.updateSuperNode(sn, content.updateNodes[sn]));
|
||||
base.supernodes[sn].uri = content.updateNodes[sn];
|
||||
node_update.add(sn);
|
||||
}
|
||||
if (content.config)
|
||||
for (let c in content.config) {
|
||||
promises.push(DB.setConfig(c, content.config[c]));
|
||||
base.sn_config[c] = content.config[c];
|
||||
};
|
||||
if (content.removeApps)
|
||||
for (let app of content.removeApps) {
|
||||
promises.push(DB.rmApp(app));
|
||||
delete base.appList;
|
||||
};
|
||||
if (content.addApps)
|
||||
for (let app in content.addApps) {
|
||||
promises.push(DB.addApp(app, content.addApps[app]));
|
||||
base.appList[app] = content.addApps[app];
|
||||
};
|
||||
});
|
||||
promises.push(DB.setLastTx(floGlobals.SNStorageID, result.totalTxs));
|
||||
//Check if all save process were successful
|
||||
Promise.allSettled(promises).then(results => {
|
||||
if (results.reduce((a, r) => r.status === "rejected" ? ++a : a, 0))
|
||||
console.warn("Some data might not have been saved in database correctly");
|
||||
});
|
||||
//Process data migration if nodes are changed
|
||||
if (Object.keys(node_change).length || node_update.size) {
|
||||
if (flag === null) {
|
||||
if (Object.keys(node_change).length)
|
||||
selfDiskMigration(node_change); //When node starts for the 1st time after been inactive, but migration has already taken place.
|
||||
}
|
||||
else
|
||||
intra.dataMigration(node_change, flag);
|
||||
}
|
||||
|
||||
resolve('Updated Supernode Configuration');
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
function readAppSubAdminListFromAPI(base) {
|
||||
var promises = [];
|
||||
//Load for each apps
|
||||
for (let app in base.appList) {
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
floBlockchainAPI.readData(base.appList[app], {
|
||||
ignoreOld: base.lastTx[base.appList[app]] || 0,
|
||||
sentOnly: true,
|
||||
pattern: app
|
||||
}).then(result => {
|
||||
let subAdmins = new Set(base.appSubAdmins[app]),
|
||||
trustedIDs = new Set(base.appTrustedIDs[app]);
|
||||
result.data.reverse().forEach(data => {
|
||||
let content = JSON.parse(data)[app];
|
||||
if (Array.isArray(content.removeSubAdmin))
|
||||
content.removeSubAdmin.forEach(sa => subAdmins.delete(sa));
|
||||
if (Array.isArray(content.addSubAdmin))
|
||||
content.addSubAdmin.forEach(sa => subAdmins.add(sa));
|
||||
if (Array.isArray(content.removeTrustedID))
|
||||
content.removeTrustedID.forEach(sa => trustedIDs.delete(sa));
|
||||
if (Array.isArray(content.addTrustedID))
|
||||
content.addTrustedID.forEach(sa => trustedIDs.add(sa));
|
||||
});
|
||||
base.appSubAdmins[app] = Array.from(subAdmins);
|
||||
base.appTrustedIDs[app] = Array.from(trustedIDs);
|
||||
Promise.allSettled([
|
||||
DB.setLastTx(base.appList[app], result.totalTxs),
|
||||
DB.setSubAdmin(app, base.appSubAdmins[app]),
|
||||
DB.setTrustedIDs(app, base.appTrustedIDs[app])
|
||||
]).then(results => {
|
||||
if (results.reduce((a, r) => r.status === "rejected" ? ++a : a, 0))
|
||||
console.warn(`SubAdmin list for app(${app}) might not have been saved in database`);
|
||||
});
|
||||
resolve("Loaded subAdmin List for APP:" + app);
|
||||
}).catch(error => reject([app, error]));
|
||||
}));
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
Promise.allSettled(promises).then(results => {
|
||||
if (results.reduce((a, r) => r.status === "rejected" ? ++a : a, 0)) {
|
||||
let error = Object.fromEntries(results.filter(r => r.status === "rejected").map(r => r.reason));
|
||||
console.debug(error);
|
||||
reject(`subAdmin List for APPS(${Object.keys(error)}) might not have loaded correctly`);
|
||||
} else
|
||||
resolve("Loaded subAdmin List for all APPs successfully");
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function diskCleanUp(base) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let time = Date.now() - base.sn_config.deleteDelay,
|
||||
promises = [];
|
||||
|
||||
intra._list.serving.forEach(sn => {
|
||||
//delete all when app is not authorised.
|
||||
promises.push(DB.clearUnauthorisedAppData(sn, Object.keys(base.appList), time));
|
||||
//for each authorised app: delete unofficial data (untaged, unknown sender/receiver)
|
||||
for (let app in base.appList)
|
||||
promises.push(DB.clearAuthorisedAppData(sn, app, base.appList[app], base.appSubAdmins[app], time));
|
||||
});
|
||||
|
||||
Promise.allSettled(promises).then(results => {
|
||||
let failed = results.filter(r => r.status === "rejected").map(r => r.reason);
|
||||
if (failed.length) {
|
||||
console.error(JSON.stringify(failed));
|
||||
let success = results.length - failed.length;
|
||||
reject(`Disk clean-up process has failed at ${100 * success / results.length}%. (Success:${success}|Failed:${failed.count})`);
|
||||
} else
|
||||
resolve("Disk clean-up process finished successfully (100%)");
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
function selfDiskMigration(node_change) {
|
||||
DB.listTable("SHOW TABLES").then(disks => {
|
||||
disks.forEach(n => {
|
||||
if (node_change[n] === false)
|
||||
DB.dropTable(n).then(_ => null).catch(e => console.error(e));
|
||||
DB.readAllDataStream(n, 0, d => {
|
||||
let closest = cloud.closestNode(d.receiverID);
|
||||
if (closest !== n)
|
||||
DB.deleteData(n, d.vectorClock).then(_ => null).catch(e => console.error(e));
|
||||
}).then(result => console.debug(`Completed self-disk migration for ${n}`)).catch(error => console.error(error));
|
||||
});
|
||||
}).catch(error => console.error(error));
|
||||
};
|
||||
|
||||
module.exports = startNode;
|
||||
114
src/server.js
Normal file
114
src/server.js
Normal file
@ -0,0 +1,114 @@
|
||||
const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
const url = require('url');
|
||||
const intra = require('./backup/intra');
|
||||
const client = require('./client');
|
||||
const { INVALID, INVALID_E_CODE, INTERNAL_E_CODE } = require('./_constants');
|
||||
|
||||
module.exports = function Server(port) {
|
||||
|
||||
var refresher; //container for refresher
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
if (req.method === "GET") {
|
||||
//GET: requesting data
|
||||
let u = url.parse(req.url, true);
|
||||
if (!u.search)
|
||||
return res.end("");
|
||||
console.debug("GET:", u.search);
|
||||
client.processRequestFromUser(u.query)
|
||||
.then(result => res.end(JSON.stringify(result[0])))
|
||||
.catch(error => {
|
||||
if (error instanceof INVALID)
|
||||
res.writeHead(INVALID_E_CODE).end(error.message);
|
||||
else {
|
||||
console.error(error);
|
||||
res.writeHead(INTERNAL_E_CODE).end("Unable to process request");
|
||||
}
|
||||
});
|
||||
} else if (req.method === "POST") {
|
||||
//POST: All data processing (required JSON input)
|
||||
let data = '';
|
||||
req.on('data', chunk => data += chunk);
|
||||
req.on('end', () => {
|
||||
console.debug("POST:", data);
|
||||
//process the all data request types
|
||||
client.processIncomingData(data).then(result => {
|
||||
res.end(JSON.stringify(result[0]));
|
||||
if (result[1]) {
|
||||
refresher.countdown;
|
||||
if (['DATA', 'TAG', 'NOTE'].includes(result[1]))
|
||||
sendToLiveRequests(result[0]);
|
||||
intra.forwardToNextNode(result[2], result[0]);
|
||||
};
|
||||
}).catch(error => {
|
||||
if (error instanceof INVALID)
|
||||
res.writeHead(INVALID_E_CODE).end(error.message);
|
||||
else {
|
||||
console.error(error);
|
||||
res.writeHead(INTERNAL_E_CODE).end("Unable to process request");
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
server.listen(port, (err) => {
|
||||
if (!err)
|
||||
console.log(`Server running at port ${port}`);
|
||||
});
|
||||
|
||||
const wsServer = new WebSocket.Server({
|
||||
server
|
||||
});
|
||||
wsServer.on('connection', function connection(ws) {
|
||||
ws.onmessage = function (evt) {
|
||||
let message = evt.data;
|
||||
if (message.startsWith(intra.SUPERNODE_INDICATOR))
|
||||
intra.processTaskFromSupernode(message, ws);
|
||||
else {
|
||||
console.debug("WS: ", message);
|
||||
try {
|
||||
var request = JSON.parse(message);
|
||||
if (request.status !== undefined)
|
||||
client.processStatusFromUser(request, ws);
|
||||
else
|
||||
client.processRequestFromUser(request).then(result => {
|
||||
ws.send(JSON.stringify(result[0]));
|
||||
ws._liveReq = request;
|
||||
}).catch(error => {
|
||||
if (floGlobals.sn_config.errorFeedback) {
|
||||
if (error instanceof INVALID)
|
||||
ws.send(`${INVALID_E_CODE}: ${error.message}`);
|
||||
else {
|
||||
console.error(error);
|
||||
ws.send(`${INTERNAL_E_CODE}: Unable to process request`);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
//console.error(error);
|
||||
if (floGlobals.sn_config.errorFeedback)
|
||||
ws.send(`${INVALID_E_CODE}: Request not in JSON format`);
|
||||
};
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
function sendToLiveRequests(data) {
|
||||
wsServer.clients.forEach(ws => {
|
||||
if (client.checkIfRequestSatisfy(ws._liveReq, data))
|
||||
ws.send(JSON.stringify(data));
|
||||
});
|
||||
};
|
||||
|
||||
Object.defineProperty(this, "http", {
|
||||
get: () => server
|
||||
});
|
||||
Object.defineProperty(this, "webSocket", {
|
||||
get: () => wsServer
|
||||
});
|
||||
Object.defineProperty(this, "refresher", {
|
||||
set: (r) => refresher = r
|
||||
});
|
||||
};
|
||||
17
src/set_globals.js
Normal file
17
src/set_globals.js
Normal file
@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
//fetch for node js (used in floBlockchainAPI.js)
|
||||
global.fetch = require("node-fetch");
|
||||
|
||||
//Set browser paramaters from param.json (or param-default.json)
|
||||
var param;
|
||||
try {
|
||||
param = require('../args/param.json');
|
||||
} catch {
|
||||
param = require('../args/param-default.json');
|
||||
} finally {
|
||||
for (let p in param)
|
||||
global[p] = param[p];
|
||||
}
|
||||
|
||||
if (!process.argv.includes("--debug"))
|
||||
global.console.debug = () => null;
|
||||
@ -1,19 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
current_date=$(date)
|
||||
echo "$current_date : Starting Supernode" >> logs/app.log
|
||||
|
||||
#Read configurations
|
||||
IFS="="
|
||||
while read -r var value
|
||||
do
|
||||
export "$var"="${value}"
|
||||
done < .config
|
||||
|
||||
#Start the app
|
||||
echo $current_date >> logs/server.log
|
||||
app/supernodeWSS.bin $PORT $SERVER_PWD >> logs/server.log &
|
||||
echo $current_date >> logs/browser.log
|
||||
$BROWSER app/index.html >> logs/browser.log &
|
||||
wait
|
||||
|
||||
16548
util/mongoose.c
16548
util/mongoose.c
File diff suppressed because it is too large
Load Diff
6685
util/mongoose.h
6685
util/mongoose.h
File diff suppressed because it is too large
Load Diff
177
util/websocket.c
177
util/websocket.c
@ -1,177 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2014 Cesanta Software Limited
|
||||
* All rights reserved
|
||||
*/
|
||||
|
||||
#include "mongoose.h"
|
||||
|
||||
static sig_atomic_t s_signal_received = 0;
|
||||
static struct mg_serve_http_opts s_http_server_opts;
|
||||
static char *s_http_port, *server_pwd;
|
||||
static struct mg_connection *sn_c = NULL; //supernode_js_connection
|
||||
|
||||
static void signal_handler(int sig_num) {
|
||||
signal(sig_num, signal_handler); // Reinstantiate signal handler
|
||||
s_signal_received = sig_num;
|
||||
}
|
||||
|
||||
static int is_websocket(const struct mg_connection *nc) {
|
||||
return nc->flags & MG_F_IS_WEBSOCKET;
|
||||
}
|
||||
|
||||
//System_display [wss => console]
|
||||
static void sys_display(struct mg_connection *nc, char type[25]){
|
||||
char addr[32];
|
||||
mg_sock_addr_to_str(&nc->sa, addr, sizeof(addr), MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT);
|
||||
printf("%s\t%s\n", addr, type);
|
||||
}
|
||||
|
||||
//System_unicast [wss => nc]
|
||||
static void sys_unicast(struct mg_connection *nc, const struct mg_str msg) {
|
||||
if (nc != NULL)
|
||||
mg_send_websocket_frame(nc, WEBSOCKET_OP_TEXT, msg.p, (int)msg.len);
|
||||
}
|
||||
|
||||
//System_broadcast [wss => all]
|
||||
static void sys_broadcast(struct mg_connection *nc, const struct mg_str msg) {
|
||||
struct mg_connection *c;
|
||||
for (c = mg_next(nc->mgr, NULL); c != NULL; c = mg_next(nc->mgr, c)) {
|
||||
if (c == nc) continue;
|
||||
mg_send_websocket_frame(c, WEBSOCKET_OP_TEXT, msg.p, (int)msg.len);
|
||||
}
|
||||
}
|
||||
|
||||
//Broadcast incoming message [sn_c => all]
|
||||
static void broadcast(const struct mg_str msg) {
|
||||
printf("BROADCAST\t[%d]\n", (int)msg.len-1);
|
||||
struct mg_connection *c;
|
||||
for (c = mg_next(sn_c->mgr, NULL); c != NULL; c = mg_next(sn_c->mgr, c)) {
|
||||
if (c == sn_c) continue;
|
||||
mg_send_websocket_frame(c, WEBSOCKET_OP_TEXT, msg.p, (int)msg.len);
|
||||
}
|
||||
}
|
||||
|
||||
//Groupcast incoming message [sn_c => gid]
|
||||
static void groupcast(const struct mg_str msg) {
|
||||
char gid[35];
|
||||
snprintf(gid, sizeof(gid), "%.*s", 34, &msg.p[1]);
|
||||
printf("GROUPCAST\t[%d]\n", (int)msg.len - 36);
|
||||
struct mg_connection *c;
|
||||
for (c = mg_next(sn_c->mgr, NULL); c != NULL; c = mg_next(sn_c->mgr, c)) {
|
||||
if (c == sn_c) continue;
|
||||
else if (!strcmp(c->gid, gid))
|
||||
mg_send_websocket_frame(c, WEBSOCKET_OP_TEXT, msg.p, (int)msg.len);
|
||||
}
|
||||
}
|
||||
|
||||
//Unicast incoming message [sn_c => uid-gid]
|
||||
static void unicast(const struct mg_str msg) {
|
||||
char uid[6], gid[35];
|
||||
snprintf(uid, sizeof(uid), "%.*s", 5, &msg.p[1]);
|
||||
snprintf(gid, sizeof(gid), "%.*s", 34, &msg.p[7]);
|
||||
printf("UNICAST\t\t[%d]\n", (int)msg.len - 42);
|
||||
struct mg_connection *c;
|
||||
for (c = mg_next(sn_c->mgr, NULL); c != NULL; c = mg_next(sn_c->mgr, c)) {
|
||||
if (c == sn_c) continue;
|
||||
else if (!strcmp(c->gid, gid) && !strcmp(c->uid, uid))
|
||||
mg_send_websocket_frame(c, WEBSOCKET_OP_TEXT, msg.p, (int)msg.len);
|
||||
}
|
||||
}
|
||||
|
||||
//Forward incoming message [user => sn_c]
|
||||
static void forward(const struct mg_str msg) {
|
||||
printf("FORWARD\t\t[%d]\n", (int)msg.len - 41);
|
||||
if(sn_c != NULL)
|
||||
mg_send_websocket_frame(sn_c, WEBSOCKET_OP_TEXT, msg.p, (int)msg.len);
|
||||
else
|
||||
printf("WARNING: No supernode connected\n");
|
||||
}
|
||||
|
||||
static void ev_handler(struct mg_connection *nc, int ev, void *ev_data) {
|
||||
switch (ev) {
|
||||
case MG_EV_WEBSOCKET_HANDSHAKE_DONE: {
|
||||
/*New Websocket Connection*/
|
||||
sys_display(nc, "+Connected+");
|
||||
if (sn_c != NULL)
|
||||
sys_unicast(nc, mg_mk_str("$+"));
|
||||
else
|
||||
sys_unicast(nc, mg_mk_str("$-"));
|
||||
break;
|
||||
}
|
||||
case MG_EV_WEBSOCKET_FRAME: {
|
||||
/* New Websocket Message*/
|
||||
struct websocket_message *wm = (struct websocket_message *) ev_data;
|
||||
struct mg_str d = {(char *) wm->data, wm->size};
|
||||
if (d.p[0] == '$') {
|
||||
if (sn_c == NULL) {
|
||||
char pass[100];
|
||||
snprintf(pass, sizeof(pass), "%.*s",(int)d.len-1, &d.p[1]);
|
||||
if (!strcmp(pass, server_pwd)) {
|
||||
sn_c = nc;
|
||||
sys_unicast(nc, mg_mk_str("$Access Granted"));
|
||||
sys_display(nc, "*Supernode LoggedIn*");
|
||||
sys_broadcast(nc, mg_mk_str("$+"));
|
||||
} else
|
||||
sys_unicast(nc, mg_mk_str("$Access Denied"));
|
||||
} else
|
||||
sys_unicast(nc, mg_mk_str("$Access Locked"));
|
||||
} else if (nc == sn_c) {
|
||||
switch (d.p[0]) {
|
||||
case '@': unicast(d); break;
|
||||
case '#': groupcast(d); break;
|
||||
default: broadcast(d);
|
||||
}
|
||||
} else {
|
||||
snprintf(nc->gid, sizeof(nc->gid), "%.*s", 34, &d.p[0]);
|
||||
snprintf(nc->uid, sizeof(nc->uid), "%.*s", 5, &d.p[35]);
|
||||
forward(d);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MG_EV_HTTP_REQUEST: {
|
||||
mg_serve_http(nc, (struct http_message *) ev_data, s_http_server_opts);
|
||||
break;
|
||||
}
|
||||
case MG_EV_CLOSE: {
|
||||
/* Websocket Disconnected */
|
||||
if (is_websocket(nc)) {
|
||||
if(nc == sn_c) {
|
||||
sn_c = NULL;
|
||||
sys_display(nc,"!Supernode LoggedOut!");
|
||||
sys_broadcast(nc, mg_mk_str("$-"));
|
||||
} else
|
||||
sys_display(nc, "-Disconnected-");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
s_http_port = argv[1];
|
||||
server_pwd = argv[2];
|
||||
|
||||
struct mg_mgr mgr;
|
||||
struct mg_connection *nc;
|
||||
|
||||
signal(SIGTERM, signal_handler);
|
||||
signal(SIGINT, signal_handler);
|
||||
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||
setvbuf(stderr, NULL, _IOLBF, 0);
|
||||
|
||||
mg_mgr_init(&mgr, NULL);
|
||||
|
||||
nc = mg_bind(&mgr, s_http_port, ev_handler);
|
||||
mg_set_protocol_http_websocket(nc);
|
||||
s_http_server_opts.document_root = "app/"; // Serve current directory
|
||||
s_http_server_opts.enable_directory_listing = "no";
|
||||
|
||||
printf("Started on port %s\n", s_http_port);
|
||||
while (s_signal_received == 0) {
|
||||
mg_mgr_poll(&mgr, 200);
|
||||
}
|
||||
mg_mgr_free(&mgr);
|
||||
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user