diff --git a/client/index.js b/client/index.js
deleted file mode 100644
index 35c60d9..0000000
--- a/client/index.js
+++ /dev/null
@@ -1,26 +0,0 @@
-(async () => {
-const { title } = require('../lib/title');
-const net = require('net');
-const os = require('os');
-const log = require('signale');
-const { config } = require('./../package.json');
-const { hri } = require('human-readable-ids');
-const { Profiles } = require('../lib/Profiles');
-const profiles = new Profiles('client');
-const yargs = require('yargs').argv;
-const identity = yargs.profile ?
- await profiles.get(yargs.profile) :
- await profiles.get((await profiles.all())[0]);
-
-// const id = hri.random();
-
-// console.log(id)
-
-// title(id);
-// title(identity.name.replace(/-/g, ' '));
-
-// await profiles.create();
-
-
-
-})();
\ No newline at end of file
diff --git a/lib/Gateway.js b/lib/Gateway.js
new file mode 100644
index 0000000..dfe3732
--- /dev/null
+++ b/lib/Gateway.js
@@ -0,0 +1,136 @@
+const { config } = require('./config');
+const Keyv = require('keyv');
+const { KeyvFile } = require('keyv-file');
+const { Signale } = require('signale');
+const log = new Signale().scope('GTWY');
+const interactive = new Signale({ interactive: true, scope: 'GTWY' });
+const stp = require('./STP');
+const appdata = require('./appdata');
+
+class Gateway {
+ constructor(identity, endpoints) {
+ this.identity = identity;
+
+ this.endpoints = new Keyv({
+ store: new KeyvFile({
+ filename: `${appdata}/valnet/relay/${this.identity.name}-endpoints.json`
+ })
+ });
+
+ this.ready = this.insertEndpoints(endpoints)
+ .then(this.networkTest.bind(this));
+ }
+
+ async insertEndpoints(endpoints) {
+ for (const endpoint of endpoints) {
+ const storeValue = await this.endpoints.get(endpoint);
+ if (storeValue) continue;
+
+ const [host, port] = endpoint.split(':');
+ const record = new EndpointRecord(host, port, null, 'unknown');
+ const currentEnpoints = await this.endpoints.get('cache') || [];
+
+ if (currentEnpoints.indexOf(endpoint) === -1) {
+ currentEnpoints.push(endpoint);
+ await this.endpoints.set('cache', currentEnpoints);
+ }
+
+ await this.endpoints.set(endpoint, record);
+ }
+
+ log.info('gateway endpoints:');
+ log.info('\t' + (await this.endpoints.get('cache')).join('\n\t'));
+ }
+
+ async networkTest() {
+ const endpoints = (await Promise.all(
+ (await this.endpoints.get('cache'))
+ .map(endpoint => this.endpoints.get(endpoint))
+ )).map(EndpointRecord.fromJson);
+
+ for (const endpoint of endpoints) {
+ await this.testEndpoint(endpoint.host, endpoint.port);
+ }
+ }
+
+ async testEndpoint(host, port) {
+ await new Promise(async (res, rej) => {
+ let pings = [];
+ let maxPings = 10;
+ let connectionAttempts = 0;
+ log.info(`Connecting to ${host}:${port}...`);
+
+ while (connectionAttempts < 10 && pings.length < maxPings) {
+
+ await new Promise(async (res) => {
+ const client = stp.connect({
+ identity: this.identity,
+ ip: host,
+ port: parseInt(port)
+ });
+
+ client.on('error', _ => _);
+
+ client.on('ready', async () => {
+ while (pings.length < maxPings) {
+ interactive.info(`[${pings.length + 1}/${maxPings}] Testing connection...`);
+ pings.push(await client.ping());
+ // pings.push(10);
+ await new Promise(res => setTimeout(res, 100));
+ }
+ const average = Math.round(pings.reduce((a, v) => a + v, 0) / maxPings);
+ interactive.success(`Test complete. Average Ping: ${average}`)
+ client.tcpSocket.destroy();
+ res();
+ });
+
+ client.on('close', () => {
+ res();
+ });
+
+ });
+
+ }
+ });
+ }
+}
+
+class EndpointRecord {
+
+ /**
+ * @param {Object|string} json string / object representation
+ * @returns {EndpointRecord}
+ */
+ static fromJson(obj) {
+ if (typeof obj === 'string')
+ return EndpointRecord.fromJson(JSON.parse(obj));
+
+ return new EndpointRecord(
+ obj.host,
+ obj.port,
+ obj.lastPing ? new PingRecord(
+ obj.lastPing.average,
+ obj.lastPing.tests,
+ obj.lastPing.date
+ ) : null,
+ obj.status
+ );
+ }
+
+ constructor(host, port, lastPing, status) {
+ this.host = host;
+ this.port = port;
+ this.lastPing = lastPing;
+ this.status = status;
+ }
+}
+
+class PingRecord {
+ constructor(average, tests, date) {
+ this.average = average;
+ this.tests = tests;
+ this.date = date;
+ }
+}
+
+module.exports = Gateway
\ No newline at end of file
diff --git a/lib/Identity.js b/lib/Identity.js
index 811f079..a4256d6 100644
--- a/lib/Identity.js
+++ b/lib/Identity.js
@@ -4,6 +4,7 @@ const { hri } = require('human-readable-ids');
const os = require('os');
const NodeRSA = require('node-rsa');
let log = require('signale').scope('Identity(null)');
+const appdata = require('./appdata');
module.exports.Identity = class Identity {
kv;
@@ -13,9 +14,10 @@ module.exports.Identity = class Identity {
/// ASYNC CONSTRUCTOR
constructor(module, id) {
return new Promise(async (res, rej) => {
+
const kv = new Keyv({
store: new KeyvFile({
- filename: `${os.tmpdir()}/valnet/${module}/${id}.json`
+ filename: `${appdata}/valnet/${module}/${id}.json`
})
});
@@ -67,6 +69,6 @@ module.exports.Identity = class Identity {
}
toString() {
- return `[Identity(${this.name})]`;
+ return `[Identity ${this.name}]`;
}
}
\ No newline at end of file
diff --git a/lib/STP/index.js b/lib/STP/index.js
index 52fda13..64bd6f2 100644
--- a/lib/STP/index.js
+++ b/lib/STP/index.js
@@ -1,12 +1,13 @@
const net = require('net');
const EventEmitter = require('events');
const NodeRSA = require('node-rsa');
-const log = require('signale').scope('stp');
+const log = require('signale').scope('_STP');
const {
KeyExchangePacket,
- AckPacket
+ AckPacket,
+ PingPacket,
+ PongPacket
} = require('./packets');
-const { rejects } = require('assert');
module.exports.createServer = function({identity = {}, port = 5000} = {}, cb = _ => _) {
const server = new Server(identity, port);
@@ -37,7 +38,7 @@ class Server extends EventEmitter {
}
openServer() {
- log.info(`opening STP server on ${this.port}`);
+ // log.info(`opening STP server on ${this.port}`);
this.tcpServer = net.createServer(this.tcpConnectClient.bind(this));
this.tcpServer.on('error', e => {
log.warn(e)
@@ -56,11 +57,17 @@ class Server extends EventEmitter {
class STPSocket extends EventEmitter {
tcpSocket;
- readyState = 0;
buffer = '';
externalKey;
identity;
+ CONNECTING = Symbol('connecting');
+ EXCHANGE = Symbol('exchange');
+ SECURED = Symbol('secured');
+ readyState = this.CONNECTING;
+
+ pingCallbacks = new Map();
+
get loopback() {
return this.identity.publicKey ===
this.externalKey.exportKey('pkcs8-public-pem');
@@ -113,24 +120,33 @@ class STPSocket extends EventEmitter {
}
processMessage(obj) {
- switch(obj.cmd) {
- case 'KEY': {
- if(this.readyState === 0) {
- this.externalKey = new NodeRSA();
- this.externalKey.importKey(obj.data.key, 'pkcs8-public-pem');
- this.tcpSocket.write(new AckPacket().toBuffer());
- this.readyState = 1;
- }
- break;
- }
- case 'ACK': {
- if(this.readyState === 1) {
- this.readyState = 2;
- this.emit('ready');
- }
- break;
- }
+
+ if(this.readyState === this.CONNECTING && obj.cmd === 'KEY') {
+ this.externalKey = new NodeRSA();
+ this.externalKey.importKey(obj.data.key, 'pkcs8-public-pem');
+ this.tcpSocket.write(new AckPacket().toBuffer());
+ this.readyState = this.EXCHANGE;
+ return;
}
+
+ if(this.readyState === this.EXCHANGE && obj.cmd === 'ACK') {
+ this.readyState = this.SECURED;
+ this.emit('ready');
+ return;
+ }
+
+ if (this.readyState === this.SECURED && obj.cmd === 'PING') {
+ this.tcpSocket.write(new PongPacket(obj.data.id).toBuffer());
+ return;
+ }
+
+ if (this.readyState === this.SECURED && obj.cmd === 'PONG') {
+ if (this.pingCallbacks.has(obj.data.id)) {
+ this.pingCallbacks.get(obj.data.id)();
+ }
+ return;
+ }
+
}
handshake() {
@@ -139,4 +155,17 @@ class STPSocket extends EventEmitter {
const buffer = packet.toBuffer();
this.tcpSocket.write(buffer);
}
+
+ async ping() {
+ const startTime = new Date().getTime();
+ return await new Promise(async (res) => {
+ const packet = new PingPacket();
+ this.pingCallbacks.set(packet.data.id, _ => {
+ res(new Date().getTime() - startTime);
+ this.pingCallbacks.delete(packet.data.id);
+ });
+ this.tcpSocket.write(packet.toBuffer());
+ })
+
+ }
}
diff --git a/lib/STP/packets.js b/lib/STP/packets.js
index 233ae89..58b1832 100644
--- a/lib/STP/packets.js
+++ b/lib/STP/packets.js
@@ -1,4 +1,6 @@
+const md5 = require('md5');
+// #region === [ private lib functions ] ===
class STPPacket {
cmd = 'NOOP';
@@ -14,15 +16,6 @@ class STPPacket {
}
}
-class KeyExchangePacket extends STPPacket {
- constructor(key, type = 'pkcs8-pem') {
- super();
- this.cmd = 'KEY';
- this.data.key = key;
- this.meta.type = type;
- }
-}
-
function basicPacket(commandName) {
return class extends STPPacket {
constructor() {
@@ -32,7 +25,87 @@ function basicPacket(commandName) {
}
}
-module.exports.STPPacket = STPPacket;
+// #endregion
+
+// #region === [ exotic packet classes ] ===
+
+class KeyExchangePacket extends STPPacket {
+ constructor(key, type = 'pkcs8-pem') {
+ super();
+ this.cmd = 'KEY';
+ this.data.key = key;
+ this.meta.type = type;
+ }
+}
+
+class ClientsPacket extends STPPacket {
+ constructor(clients) {
+ super();
+ this.cmd = 'NODES'
+ this.data.clients = clients;
+ }
+}
+
+class PingPacket extends STPPacket {
+ constructor() {
+ super();
+ this.cmd = 'PING';
+ this.data.id = md5(Date());
+ }
+}
+
+class PongPacket extends STPPacket {
+ constructor(id) {
+ super();
+ this.cmd = 'PONG';
+ this.data.id = id;
+ }
+}
+
+// #endregion
+
+// #region === [ ordinary packet classes ] ===
+
+const AckPacket = basicPacket('ACK');
+const GetClientsPacket = basicPacket('QNODES');
+
+// #endregion
+
+// #region === [ public lib functions ] ===
+
+function reconstructPacket(packet) {
+
+ if(packet.startsWith('\02'))
+ return reconstructPacket(packet.substr(1));
+ if(packet.endsWith('\x03'))
+ return reconstructPacket(packet.substr(0, packet.length - 1));
+
+ const obj = JSON.parse(packet);
+
+ switch(obj.cmd) {
+ case 'KEY': return new KeyExchangePacket(obj.data.key, obj.meta.type);
+ case 'NODES': return new ClientsPacket(obj.data.clients);
+ case 'QNODES': return new GetClientsPacket();
+ case 'ACK': return new AckPacket();
+
+ case 'NOOP': return new STPPacket();
+ default: throw new TypeError(`Unknown command ${obj.cmd}`);
+ }
+
+}
+
+// #endregion
+
+// #region === [ exports ] ===
+
module.exports.KeyExchangePacket = KeyExchangePacket;
-module.exports.AckPacket = basicPacket('ACK')
-module.exports.GetClientsPacket = basicPacket('NODES')
+module.exports.ClientsPacket = ClientsPacket;
+module.exports.PingPacket = PingPacket;
+module.exports.PongPacket = PongPacket;
+
+module.exports.AckPacket = AckPacket;
+module.exports.GetClientsPacket = GetClientsPacket;
+
+module.exports.reconstructPacket = reconstructPacket;
+
+// #endregion
\ No newline at end of file
diff --git a/lib/appdata.js b/lib/appdata.js
new file mode 100644
index 0000000..cac3379
--- /dev/null
+++ b/lib/appdata.js
@@ -0,0 +1,3 @@
+const appdata = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share");
+
+module.exports = appdata;
\ No newline at end of file
diff --git a/lib/config.js b/lib/config.js
new file mode 100644
index 0000000..f4167dd
--- /dev/null
+++ b/lib/config.js
@@ -0,0 +1,39 @@
+const pkg = require('./../package.json');
+const { readFileSync, writeFileSync } = require('fs');
+const { ensureDirSync } = require('fs-extra');
+
+const appdata = require('./appdata');
+ensureDirSync(`${appdata}/valnet/relay`);
+const filepath = `${appdata}/valnet/relay/config.json`;
+
+const configObject = {}
+
+module.exports.config = configObject;
+
+module.exports.write = write;
+
+function write() {
+ writeFileSync(filepath, JSON.stringify(configObject, null, 2));
+}
+
+function importFromPackage() {
+ loadObject(pkg.config);
+}
+
+function loadObject(obj) {
+ for(const key in obj) {
+ configObject[key] = obj[key];
+ }
+
+ write();
+}
+
+try {
+ const json = readFileSync(filepath);
+ const data = JSON.parse(json);
+
+ loadObject(data);
+
+} catch(e) {
+ importFromPackage();
+}
\ No newline at end of file
diff --git a/lib/node.js b/lib/node.js
index 8b76499..233e1d7 100644
--- a/lib/node.js
+++ b/lib/node.js
@@ -1,11 +1,111 @@
const EventEmitter = require('events')
-
+const stp = require('./STP');
+const upnp = require('./upnp');
+const md5 = require('md5');
+const pkg = require('./../package.json');
+const { config, write } = require('./config.js');
+const log = require('signale').scope('NODE');
+const bonjour = require('bonjour')();
+const Gateway = require('./Gateway');
class Node extends EventEmitter {
- constructor() {
+ clients = [];
+ hash = null;
+ name = null;
+ readyPromise = null;
+ port = null;
+ identity;
+ multicastAd = null;
+ multicastBrowser = null;
+ connected = false;
+
+ constructor(identity) {
+ super();
+ this.identity = identity;
+ this.hash = md5(identity.publicKey);
+ this.name = `valnet-node-${identity.name}`;
+
+ this.readyPromise = this.negotiatePort()
+ .then(this.startServer.bind(this))
+ .catch(this.serverStartupFailed.bind(this))
+ .then(this.connectNetwork.bind(this))
+ }
+
+ async connectNetwork() {
+ const gateway = new Gateway(this.identity, config.endpoints);
+ }
+
+ async serverStartupFailed(error) {
+ log.warn('Failed to set up Valet server on node.')
+ }
+
+ async startServer() {
+ log.info('creating Valnet Node on port ' + this.port + '...');
+
+ stp.createServer({
+ identity: this.identity,
+ port: this.port
+ }, (connection) => {
+ log.info('incomming connection from ' + connection.remoteAddress);
+ });
+ log.info('advertising node on multicast...')
+ this.multicastAd = bonjour.publish({
+ name: this.name,
+ type: 'STP',
+ port: this.port
+ });
+
+ this.multicastBrowser = bonjour.find({});
+
+ this.multicastBrowser.on('up', this.serviceUp.bind(this));
+ this.multicastBrowser.on('down', this.serviceDown.bind(this));
+
+ // log.success('Node successfully registered!');
+ }
+
+ async serviceUp({name, address, port, protocol}) {
+ // log.debug(`Found ${name} @ ${address}:${port} using ${protocol}`);
+ }
+
+ async serviceDown(service) {
+ log.debug('down', service);
+ }
+
+ async negotiatePort() {
+ const mappings = await upnp.mappings();
+ const matchingMappings = mappings.filter(mapping => {
+ return mapping.description === this.name
+ });
+ const alreadyMapped = matchingMappings.length > 0;
+ const takenPorts = mappings.map(mapping => mapping.public.port);
+
+ if(alreadyMapped) {
+ this.port = matchingMappings[0].public.port;
+ log.success(`upnp port ${this.port} already registered!`);
+ return;
+ }
+
+ for(let port = config.ports.relay; port <= config.ports.relayEnd; port ++) {
+ if(takenPorts.indexOf(port) === -1) {
+ await upnp.mapIndefinite(port, this.name);
+ this.port = port;
+ log.success(`registered upnp port ${this.port}`);
+ return;
+ }
+ }
+
+ // console.log(mappings, this.hash);
+ }
+
+ static get Node() {
+ return Node;
+ }
+
+ get ready() {
+ return this.readyPromise;
}
}
-module.exports.Node = Node;
\ No newline at end of file
+module.exports = Node;
\ No newline at end of file
diff --git a/lib/packet.js b/lib/packet.js
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/upnp.js b/lib/upnp.js
index 49ccfe3..918af49 100644
--- a/lib/upnp.js
+++ b/lib/upnp.js
@@ -1,31 +1,34 @@
const natUpnp = require('nat-upnp');
const client = natUpnp.createClient();
-module.exports.map = function(port) {
+
+module.exports.map = function(port, ttl = 10, name = 'upnp application') {
return new Promise((res, rej) => {
client.portMapping({
private: port,
public: port,
- ttl: 10,
- description: 'valnet'
+ ttl,
+ description: name
}, (err) => {
if(err) rej(err);
res();
});
});
-}
-module.exports.mapIndefinite = function(port) {
+};
+
+module.exports.mapIndefinite = function(port, name = 'upnp application') {
return new Promise((res, rej) => {
client.portMapping({
private: port,
public: port,
ttl: 0,
- description: 'valnet'
+ description: name
}, (err) => {
if(err) rej(err);
res();
});
});
-}
+};
+
module.exports.unmap = function(port) {
return new Promise((res, rej) => {
client.portUnmapping({
@@ -36,7 +39,8 @@ module.exports.unmap = function(port) {
res();
});
});
-}
+};
+
module.exports.mappings = function() {
return new Promise((res, rej) => {
client.getMappings((err, mappings) => {
@@ -44,7 +48,7 @@ module.exports.mappings = function() {
res(mappings);
});
});
-}
+};
/*
diff --git a/lib/vns.js b/lib/vns.js
deleted file mode 100644
index e69de29..0000000
diff --git a/name-server/index.js b/name-server/index.js
deleted file mode 100644
index a146745..0000000
--- a/name-server/index.js
+++ /dev/null
@@ -1,27 +0,0 @@
-(async () => {
-const { config } = require('./../package.json');
-const net = require('net');
-const dns = require('dns');
-const stp = require('../lib/STP');
-const os = require('os');
-const { title } = require('../lib/title');
-const { hri } = require('human-readable-ids');
-const log = require('signale');
-const Keyv = require('keyv');
-const { KeyvFile } = require('keyv-file');
-const kv = new Keyv({
- store: new KeyvFile({
- filename: `${os.tmpdir()}/valnet/name-server/data.json`
- })
-});
-const { Identity } = require('./../lib/Identity');
-title('Name Server');
-
-const identity = new Identity('name-server', 'default');
-
-
-
-
-
-
-})();
\ No newline at end of file
diff --git a/package.json b/package.json
index 8fe9e47..a2c18f0 100644
--- a/package.json
+++ b/package.json
@@ -6,29 +6,34 @@
"config": {
"ports": {
"relay": 5600,
+ "relayEnd": 5699,
"http": 5700,
"service": 5000
},
"addresses": {
"relay": "valnet.xyz"
- }
+ },
+ "endpoints": [
+ "valnet.xyz:5500"
+ ]
},
"scripts": {
- "name-server": "supervisor -w name-server,lib -n exit name-server/index.js",
"relay": "supervisor -w relay,lib -n exit relay/index.js",
"relay:service": "supervisor -- relay/service",
- "client:a": "supervisor -w client,lib -n exit -- client/index.js --profile J2aV59rsIgcdd5k2",
- "client:b": "supervisor -w client,lib -n exit -- client/index.js --profile LsE8OnVzr1iYrkT0"
+ "sloc": "find lib -type f | xargs wc -l"
},
"dependencies": {
+ "bonjour": "^3.5.0",
"express": "^4.17.1",
"express-ws": "^4.0.0",
"font-ascii": "^1.2.1",
+ "fs-extra": "^9.1.0",
"gradient-string": "^1.2.0",
"human-readable-ids": "^1.0.4",
"ip": "^1.1.5",
"keyv": "^4.0.3",
"keyv-file": "^0.2.0",
+ "md5": "^2.3.0",
"nat-upnp": "^1.1.1",
"nedb": "^1.8.0",
"node-rsa": "^1.1.1",
diff --git a/relay/index.js b/relay/index.js
index 05f9528..ac6016a 100644
--- a/relay/index.js
+++ b/relay/index.js
@@ -1,51 +1,16 @@
(async () => {
const { title } = require('../lib/title');
-const net = require('net');
-const log = require('signale').scope('relay');
-const { config } = require('../package.json');
+const log = require('signale').scope('RLAY');
const { Identity } = require('../lib/Identity');
-const stp = require('../lib/STP');
title('relay', false);
const identity = await new Identity('relay', 'default');
-const upnp = require('../lib/upnp');
+const Node = require('../lib/node');
+const { config } = require('../lib/config');
+const { ensureDirSync } = require('fs-extra');
+const appdata = require('../lib/appdata');
-const clients = [];
-
-// const client = stp.connect(identity, config.ports.relay, '127.0.0.1');
-
-// upnp.mapIndefinite(5600);
-
-// ==================================== [STP SERVER]
-stp.createServer({
- identity: identity,
- port: config.ports.relay
-}, socket => {
- log.info('secured connection from ' + socket.remoteAddress);
- clients.push(socket);
-});
-
-function connectNetwork(t = 1000) {
- if(t > 60000) t /= 2;
-
- const client = stp.connect({
- identity,
- port: config.ports.relay,
- ip: config.addresses.relay
- });
- client.on('ready', () => {
- log.success('connectd to relay!');
- t = 500;
- })
- client.on('error', e => {
- });
- client.on('close', e => {
- t *= 2;
- setTimeout(connectNetwork.bind(global, t), t);
- log.warn('disconnected from relay');
- log.warn('retrying connection... ' + (t/1000) + 's')
- });
-}
-connectNetwork();
+ensureDirSync(`${appdata}/valnet/relay`);
+const node = new Node(identity);
// ==================================== [EXPRESS]
const express = require('express');
diff --git a/relay/service.js b/relay/service.js
index b1f668c..5f6b3ff 100644
--- a/relay/service.js
+++ b/relay/service.js
@@ -1,5 +1,5 @@
(async () => {
-const log = require('signale').scope('service');
+const log = require('signale').scope('SRVC');
const { execSync, spawn } = require('child_process');
const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
let proc;
@@ -12,6 +12,12 @@ const { config } = require('../package.json');
const express = require('express');
const app = express();
+try {
+ appendLogs('yarn', execSync(`yarn`));
+} catch (e) {
+ appendLogs('failed to yarn install...')
+}
+
logp('==================================');
logp('Starting Valnet Node as a Service!');
logp('Syncing to branch: ' + branch);
@@ -130,14 +136,14 @@ ${docs.map(logItem => logItem.message).join('').replace(/\u001B\[.*?[A-Za-z]/g,