diff --git a/app/controllers/webmin.controller.js b/app/controllers/webmin.controller.js index 4f9d6b59a61532aa56dd3d27a6ed55190c74a92c..3d8ec3705efc9acd16fe1be137e34253ab5da522 100644 --- a/app/controllers/webmin.controller.js +++ b/app/controllers/webmin.controller.js @@ -1,5 +1,6 @@ "use strict"; +const path = require('path'); const util = require('util'); const es = require('event-stream'); const stream = require('stream'); @@ -48,14 +49,18 @@ function WebAdmin (dbConf, overConf) { let pluggedConfP = plugForConf(); - let pluggedDALP = co(function *() { - yield pluggedConfP; + let pluggedDALP = replugDAL(); - // Routing documents - server.routing(); + function replugDAL() { + return co(function *() { + yield pluggedConfP; - return plugForDAL(); - }); + // Routing documents + server.routing(); + + return plugForDAL(); + }); + } this.summary = () => co(function *() { yield pluggedDALP; @@ -67,6 +72,9 @@ function WebAdmin (dbConf, overConf) { "host": host, "current": current, "pubkey": server.keyPair.publicKey, + "conf": { + "cpu": server.conf.cpu + }, "parameters": parameters }; }); @@ -75,7 +83,7 @@ function WebAdmin (dbConf, overConf) { const conf = http2raw.conf(req); const pair = yield keyring.scryptKeyPair(conf.idty_entropy, conf.idty_password); return { - "pubkey": base58.encode(pair.publicKey) + "pubkey": pair.publicKey }; }); @@ -244,9 +252,16 @@ function WebAdmin (dbConf, overConf) { sec: base58.encode(secretKey) } })); - pluggedConfP = co(function *() { - yield server.loadConf(); - }); + pluggedConfP = yield server.loadConf(); + yield pluggedConfP; + return {}; + }); + + this.applyCPUConf = (req) => co(function *() { + yield pluggedConfP; + server.conf.cpu = http2raw.cpu(req); + yield server.dal.saveConf(server.conf); + pluggedConfP = yield server.loadConf(); yield pluggedConfP; return {}; }); @@ -417,6 +432,43 @@ function WebAdmin (dbConf, overConf) { return {}; }); + this.exportData = () => co(function *() { + yield pluggedDALP; + return server.exportAllDataAsZIP(); + }); + + this.importData = (req) => co(function *() { + yield that.stopHTTP(); + yield that.stopAllServices(); + yield server.unplugFileSystem(); + yield pluggedDALP; + if (!req.files.importData) { + throw "Wrong upload file name"; + } + const importZipPath = path.join(server.home, 'import.zip'); + yield new Promise((resolve, reject) => { + req.files.importData.mv(importZipPath, (err) => { + err ? reject(err) : resolve(); + }); + }); + yield server.importAllDataFromZIP(importZipPath); + pluggedConfP = plugForConf(); + pluggedDALP = replugDAL(); + return {}; + }); + + this.testPeer = (req) => co(function *() { + return server.testForSync(req.body.host, parseInt(req.body.port)); + }); + + this.loadData = (dunFile) => co(function *() { + yield pluggedDALP; + // We have to wait for a non-breaking window to process reset + yield server.unplugFileSystem(); + yield server.cleanDBData(); + return {}; + }); + function plugForConf() { return co(function *() { yield server.plugFileSystem(); diff --git a/app/lib/blockchainContext.js b/app/lib/blockchainContext.js deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/app/lib/computation/blockchainContext.js b/app/lib/computation/blockchainContext.js index 578b62289286d7d20cc442e7ca8f8ba3477a7892..84ed55db2939c1c289860f8bdac8c4ca39475a30 100644 --- a/app/lib/computation/blockchainContext.js +++ b/app/lib/computation/blockchainContext.js @@ -84,6 +84,8 @@ function BlockchainContext() { yield dal.blockDAL.setSideBlock(block, previousBlock); yield undoCertifications(block); yield undoLinks(block); + yield dal.unflagExpiredIdentitiesOf(block.number); + yield dal.unflagExpiredCertificationsOf(block.number); if (previousBlock) { yield dal.undoObsoleteLinks(previousBlock.medianTime - conf.sigValidity); } @@ -132,6 +134,12 @@ function BlockchainContext() { yield that.computeObsoleteLinks(block); // Compute obsolete memberships (active, joiner) yield that.computeObsoleteMemberships(block); + // Compute obsolete identities + yield that.computeExpiredIdentities(block); + // Compute obsolete certifications + yield that.computeExpiredCertifications(block); + // Compute obsolete memberships + yield that.computeExpiredMemberships(block); // Update consumed sources & create new ones yield that.updateSources(block); // Delete eventually present transactions @@ -408,6 +416,27 @@ function BlockchainContext() { } }); + this.computeExpiredIdentities = (block) => co(function *() { + let lastForExpiry = yield dal.getIdentityExpiringBlock(block, conf.idtyWindow); + if (lastForExpiry) { + yield dal.flagExpiredIdentities(lastForExpiry.number, block.number); + } + }); + + this.computeExpiredCertifications = (block) => co(function *() { + let lastForExpiry = yield dal.getCertificationExpiringBlock(block, conf.certWindow); + if (lastForExpiry) { + yield dal.flagExpiredCertifications(lastForExpiry.number, block.number); + } + }); + + this.computeExpiredMemberships = (block) => co(function *() { + let lastForExpiry = yield dal.getMembershipExpiringBlock(block, conf.certWindow); + if (lastForExpiry) { + yield dal.flagExpiredMemberships(lastForExpiry.number, block.number); + } + }); + this.updateSources = (block) => co(function*() { if (block.dividend) { const idties = yield dal.getMembers(); diff --git a/app/lib/constants.js b/app/lib/constants.js index 8716f701357d555e3e79eeb437b628eafe2cd521..b7b71e632f900bf64f63a4918f8fd9266c823e7d 100644 --- a/app/lib/constants.js +++ b/app/lib/constants.js @@ -53,6 +53,11 @@ module.exports = { SIGNATURE_DOES_NOT_MATCH: { httpCode: 400, uerr: { ucode: 1003, message: "Signature does not match" }}, ALREADY_UP_TO_DATE: { httpCode: 400, uerr: { ucode: 1004, message: "Already up-to-date" }}, WRONG_DOCUMENT: { httpCode: 400, uerr: { ucode: 1005, message: "Document has unkown fields or wrong line ending format" }}, + HTTP_LIMITATION: { httpCode: 503, uerr: { ucode: 1006, message: "This URI has reached its maximum usage quota. Please retry later." }}, + SANDBOX_FOR_IDENTITY_IS_FULL: { httpCode: 503, uerr: { ucode: 1007, message: "The identities' sandbox is full. Please retry with another document or retry later." }}, + SANDBOX_FOR_CERT_IS_FULL: { httpCode: 503, uerr: { ucode: 1008, message: "The certifications' sandbox is full. Please retry with another document or retry later." }}, + SANDBOX_FOR_MEMERSHIP_IS_FULL: { httpCode: 503, uerr: { ucode: 1009, message: "The memberships' sandbox is full. Please retry with another document or retry later." }}, + SANDBOX_FOR_TRANSACTION_IS_FULL: { httpCode: 503, uerr: { ucode: 1010, message: "The transactions' sandbox is full. Please retry with another document or retry later." }}, HTTP_PARAM_PUBKEY_REQUIRED: { httpCode: 400, uerr: { ucode: 1101, message: "Parameter `pubkey` is required" }}, HTTP_PARAM_IDENTITY_REQUIRED: { httpCode: 400, uerr: { ucode: 1102, message: "Parameter `identity` is required" }}, @@ -64,6 +69,7 @@ module.exports = { HTTP_PARAM_CERT_REQUIRED: { httpCode: 400, uerr: { ucode: 1108, message: "Parameter `cert` is required" }}, HTTP_PARAM_REVOCATION_REQUIRED: { httpCode: 400, uerr: { ucode: 1109, message: "Parameter `revocation` is required" }}, HTTP_PARAM_CONF_REQUIRED: { httpCode: 400, uerr: { ucode: 1110, message: "Parameter `conf` is required" }}, + HTTP_PARAM_CPU_REQUIRED: { httpCode: 400, uerr: { ucode: 1111, message: "Parameter `cpu` is required" }}, // Business errors NO_MATCHING_IDENTITY: { httpCode: 404, uerr: { ucode: 2001, message: "No matching identity" }}, diff --git a/app/lib/dal/fileDAL.js b/app/lib/dal/fileDAL.js index 454701c15fabed662270685af27afe86045c4a79..aa70740c6ce8595731a1ebe1378bd958e4e0f32b 100644 --- a/app/lib/dal/fileDAL.js +++ b/app/lib/dal/fileDAL.js @@ -25,6 +25,10 @@ function FileDAL(params) { const rootPath = params.home; const myFS = params.fs; const sqlite = params.dbf(); + let dbOpened = false; + sqlite.once('open', () => { + dbOpened = true; + }); const wotbInstance = params.wotb; const that = this; @@ -408,187 +412,114 @@ function FileDAL(params) { return that.idtyDAL.saveIdentity(idty); }); - this.getMembershipExcludingBlock = (current, msValidtyTime) => co(function *() { - let currentExcluding; - if (current.number > 0) { - try { - currentExcluding = yield that.indicatorsDAL.getCurrentMembershipExcludingBlock(); - } catch (e) { - currentExcluding = null; - } - } - if (!currentExcluding) { - const root = yield that.getRootBlock(); - const delaySinceStart = current.medianTime - root.medianTime; - if (delaySinceStart > msValidtyTime) { - return that.indicatorsDAL.writeCurrentExcluding(root).then(() => root); - } - } else { - const start = currentExcluding.number; - let newExcluding; - let top = current.number, bottom = start; - // Binary tree search - do { - let middle = top - bottom; - if (middle % 2 != 0) { - middle = middle + 1; - } - middle /= 2; - middle += bottom; - if (middle == top) { - middle--; - bottom--; // Helps not being stuck looking at 'top' + this.getMembershipExcludingBlock = (current, msValidtyTime) => getCurrentExcludingOrExpiring( + current, + msValidtyTime, + that.indicatorsDAL.getCurrentMembershipExcludingBlock.bind(that.indicatorsDAL), + that.indicatorsDAL.writeCurrentExcluding.bind(that.indicatorsDAL) + ); + + this.getMembershipRevocatingBlock = (current, msValidtyTime) => getCurrentExcludingOrExpiring( + current, + msValidtyTime, + that.indicatorsDAL.getCurrentMembershipRevocatingBlock.bind(that.indicatorsDAL), + that.indicatorsDAL.writeCurrentRevocating.bind(that.indicatorsDAL) + ); + + this.getCertificationExcludingBlock = (current, certValidtyTime) => getCurrentExcludingOrExpiring( + current, + certValidtyTime, + that.indicatorsDAL.getCurrentCertificationExcludingBlock.bind(that.indicatorsDAL), + that.indicatorsDAL.writeCurrentExcludingForCert.bind(that.indicatorsDAL) + ); + + this.getIdentityExpiringBlock = (current, idtyValidtyTime) => getCurrentExcludingOrExpiring( + current, + idtyValidtyTime, + that.indicatorsDAL.getCurrentIdentityExpiringBlock.bind(that.indicatorsDAL), + that.indicatorsDAL.writeCurrentExpiringForIdty.bind(that.indicatorsDAL) + ); + + this.getCertificationExpiringBlock = (current, certWindow) => getCurrentExcludingOrExpiring( + current, + certWindow, + that.indicatorsDAL.getCurrentCertificationExpiringBlock.bind(that.indicatorsDAL), + that.indicatorsDAL.writeCurrentExpiringForCert.bind(that.indicatorsDAL) + ); + + this.getMembershipExpiringBlock = (current, msWindow) => getCurrentExcludingOrExpiring( + current, + msWindow, + that.indicatorsDAL.getCurrentMembershipExpiringBlock.bind(that.indicatorsDAL), + that.indicatorsDAL.writeCurrentExpiringForMembership.bind(that.indicatorsDAL) + ); + + function getCurrentExcludingOrExpiring(current, delayMax, currentGetter, currentSetter) { + return co(function *() { + let currentExcluding; + if (current.number > 0) { + try { + currentExcluding = yield currentGetter(); + } catch (e) { + currentExcluding = null; } - const middleBlock = yield that.getBlock(middle); - const middleNextB = yield that.getBlock(middle + 1); - const delaySinceMiddle = current.medianTime - middleBlock.medianTime; - const delaySinceNextB = current.medianTime - middleNextB.medianTime; - const isValidPeriod = delaySinceMiddle <= msValidtyTime; - const isValidPeriodB = delaySinceNextB <= msValidtyTime; - const isExcludin = !isValidPeriod && isValidPeriodB; - //console.log('MS: Search between %s and %s: %s => %s,%s', bottom, top, middle, isValidPeriod ? 'DOWN' : 'UP', isValidPeriodB ? 'DOWN' : 'UP'); - if (isExcludin) { - // Found - yield that.indicatorsDAL.writeCurrentExcluding(middleBlock); - newExcluding = middleBlock; - } - else if (isValidPeriod) { - // Look down in the blockchain - top = middle; - } - else { - // Look up in the blockchain - bottom = middle; - } - } while (!newExcluding); - return newExcluding; - } - }); - - // TODO: this is complete duplicate of getMembershipExcludingBlock()n but with two different calls: - // * getCurrentMembershipRevocatingBlock() - // * writeCurrentRevocating - this.getMembershipRevocatingBlock = (current, msValidtyTime) => co(function *() { - let currentExcluding; - if (current.number > 0) { - try { - currentExcluding = yield that.indicatorsDAL.getCurrentMembershipRevocatingBlock(); - } catch (e) { - currentExcluding = null; - } - } - if (!currentExcluding) { - const root = yield that.getRootBlock(); - const delaySinceStart = current.medianTime - root.medianTime; - if (delaySinceStart > msValidtyTime) { - return that.indicatorsDAL.writeCurrentRevocating(root).then(() => root); } - } else { - const start = currentExcluding.number; - let newRevocating; - let top = current.number, bottom = start; - // Binary tree search - do { - let middle = top - bottom; - if (middle % 2 != 0) { - middle = middle + 1; - } - middle /= 2; - middle += bottom; - if (middle == top) { - middle--; - bottom--; // Helps not being stuck looking at 'top' - } - const middleBlock = yield that.getBlock(middle); - const middleNextB = yield that.getBlock(middle + 1); - const delaySinceMiddle = current.medianTime - middleBlock.medianTime; - const delaySinceNextB = current.medianTime - middleNextB.medianTime; - const isValidPeriod = delaySinceMiddle <= msValidtyTime; - const isValidPeriodB = delaySinceNextB <= msValidtyTime; - const isExcludin = !isValidPeriod && isValidPeriodB; - //console.log('MS: Search between %s and %s: %s => %s,%s', bottom, top, middle, isValidPeriod ? 'DOWN' : 'UP', isValidPeriodB ? 'DOWN' : 'UP'); - if (isExcludin) { - // Found - yield that.indicatorsDAL.writeCurrentRevocating(middleBlock); - newRevocating = middleBlock; + if (!currentExcluding) { + const root = yield that.getRootBlock(); + const delaySinceStart = current.medianTime - root.medianTime; + if (delaySinceStart > delayMax) { + return currentSetter(root).then(() => root); } - else if (isValidPeriod) { - // Look down in the blockchain - top = middle; - } - else { - // Look up in the blockchain - bottom = middle; - } - } while (!newRevocating); - return newRevocating; - } - }); - - this.getCertificationExcludingBlock = (current, certValidtyTime) => co(function *() { - let currentExcluding; - if (current.number > 0) { - try { - currentExcluding = yield that.indicatorsDAL.getCurrentCertificationExcludingBlock(); - } catch (e) { - currentExcluding = null; - } - } - if (!currentExcluding) { - const root = yield that.getRootBlock(); - const delaySinceStart = current.medianTime - root.medianTime; - if (delaySinceStart > certValidtyTime) { - return that.indicatorsDAL.writeCurrentExcludingForCert(root).then(() => root); - } - } else { - // Check current position - const currentNextBlock = yield that.getBlock(currentExcluding.number + 1); - if (isExcluding(current, currentExcluding, currentNextBlock, certValidtyTime)) { - return currentExcluding; } else { - // Have to look for new one - const start = currentExcluding.number; - let newExcluding; - let top = current.number; - let bottom = start; - // Binary tree search - do { - let middle = top - bottom; - if (middle % 2 != 0) { - middle = middle + 1; - } - middle /= 2; - middle += bottom; - if (middle == top) { - middle--; - bottom--; // Helps not being stuck looking at 'top' - } - const middleBlock = yield that.getBlock(middle); - const middleNextB = yield that.getBlock(middle + 1); - const delaySinceMiddle = current.medianTime - middleBlock.medianTime; - const delaySinceNextB = current.medianTime - middleNextB.medianTime; - const isValidPeriod = delaySinceMiddle <= certValidtyTime; - const isValidPeriodB = delaySinceNextB <= certValidtyTime; - const isExcludin = !isValidPeriod && isValidPeriodB; - //console.log('CRT: Search between %s and %s: %s => %s,%s', bottom, top, middle, isValidPeriod ? 'DOWN' : 'UP', isValidPeriodB ? 'DOWN' : 'UP'); - if (isExcludin) { - // Found - yield that.indicatorsDAL.writeCurrentExcludingForCert(middleBlock); - newExcluding = middleBlock; - } - else if (isValidPeriod) { - // Look down in the blockchain - top = middle; - } - else { - // Look up in the blockchain - bottom = middle; - } - } while (!newExcluding); - return newExcluding; + // Check current position + const currentNextBlock = yield that.getBlock(currentExcluding.number + 1); + if (isExcluding(current, currentExcluding, currentNextBlock, delayMax)) { + return currentExcluding; + } else { + // Have to look for new one + const start = currentExcluding.number; + let newExcluding; + let top = current.number; + let bottom = start; + // Binary tree search + do { + let middle = top - bottom; + if (middle % 2 != 0) { + middle = middle + 1; + } + middle /= 2; + middle += bottom; + if (middle == top) { + middle--; + bottom--; // Helps not being stuck looking at 'top' + } + const middleBlock = yield that.getBlock(middle); + const middleNextB = yield that.getBlock(middle + 1); + const delaySinceMiddle = current.medianTime - middleBlock.medianTime; + const delaySinceNextB = current.medianTime - middleNextB.medianTime; + const isValidPeriod = delaySinceMiddle <= delayMax; + const isValidPeriodB = delaySinceNextB <= delayMax; + const isExcludin = !isValidPeriod && isValidPeriodB; + //console.log('CRT: Search between %s and %s: %s => %s,%s', bottom, top, middle, isValidPeriod ? 'DOWN' : 'UP', isValidPeriodB ? 'DOWN' : 'UP'); + if (isExcludin) { + // Found + yield currentSetter(middleBlock); + newExcluding = middleBlock; + } + else if (isValidPeriod) { + // Look down in the blockchain + top = middle; + } + else { + // Look up in the blockchain + bottom = middle; + } + } while (!newExcluding); + return newExcluding; + } } - } - }); + }); + } const isExcluding = (current, excluding, nextBlock, certValidtyTime) => { const delaySinceMiddle = current.medianTime - excluding.medianTime; @@ -598,6 +529,9 @@ function FileDAL(params) { return !isValidPeriod && isValidPeriodB; }; + this.flagExpiredIdentities = (maxNumber, onNumber) => this.idtyDAL.flagExpiredIdentities(maxNumber, onNumber); + this.flagExpiredCertifications = (maxNumber, onNumber) => this.certDAL.flagExpiredCertifications(maxNumber, onNumber); + this.flagExpiredMemberships = (maxNumber, onNumber) => this.msDAL.flagExpiredMemberships(maxNumber, onNumber); this.kickWithOutdatedMemberships = (maxNumber) => this.idtyDAL.kickMembersForMembershipBelow(maxNumber); this.revokeWithOutdatedMemberships = (maxNumber) => this.idtyDAL.revokeMembersForMembershipBelow(maxNumber); @@ -716,6 +650,12 @@ function FileDAL(params) { this.unConsumeSource = (identifier, noffset) => that.sourcesDAL.unConsumeSource(identifier, noffset); + this.unflagExpiredIdentitiesOf = (number) => that.idtyDAL.unflagExpiredIdentitiesOf(number); + + this.unflagExpiredCertificationsOf = (number) => that.certDAL.unflagExpiredCertificationsOf(number); + + this.unflagExpiredMembershipsOf = (number) => that.msDAL.unflagExpiredMembershipsOf(number); + this.saveSource = (src) => that.sourcesDAL.addSource(src.type, src.number, src.identifier, src.noffset, src.amount, src.base, src.block_hash, src.time, src.conditions); @@ -881,7 +821,11 @@ function FileDAL(params) { this.close = () => co(function *() { yield _.values(that.newDals).map((dal) => dal.cleanCache && dal.cleanCache()); return new Promise((resolve, reject) => { - if (!sqlite.open) { + let isOpened = !dbOpened; + if (process.platform === 'win32') { + isOpened = sqlite.open; // For an unknown reason, we need this line. + } + if (!isOpened) { return resolve(); } logger.debug('Trying to close SQLite...'); @@ -889,7 +833,12 @@ function FileDAL(params) { logger.info('Database closed.'); resolve(); }); - sqlite.on('error', (err) => reject(err)); + sqlite.on('error', (err) => { + if (err && err.message === 'SQLITE_MISUSE: Database is closed') { + return resolve(); + } + reject(err); + }); sqlite.close(); }); }); diff --git a/app/lib/dal/fileDALs/IndicatorsDAL.js b/app/lib/dal/fileDALs/IndicatorsDAL.js index c923d7eaa53b158e7914ecf39f17c6b13369c391..a90087bde2062d2d51447f5e155a34d6f61033cb 100644 --- a/app/lib/dal/fileDALs/IndicatorsDAL.js +++ b/app/lib/dal/fileDALs/IndicatorsDAL.js @@ -29,9 +29,21 @@ function IndicatorsDAL(rootPath, qioFS, parentCore, localDAL, AbstractStorage) { this.writeCurrentExcludingForCert = (excluding) => that.coreFS.writeJSON('indicators/excludingCRT.json', excluding); + this.writeCurrentExpiringForCert = (excluding) => that.coreFS.writeJSON('indicators/expiringCRT.json', excluding); + + this.writeCurrentExpiringForIdty = (excluding) => that.coreFS.writeJSON('indicators/expiringIDTY.json', excluding); + + this.writeCurrentExpiringForMembership = (excluding) => that.coreFS.writeJSON('indicators/expiringMS.json', excluding); + this.getCurrentMembershipExcludingBlock = () => that.coreFS.readJSON('indicators/excludingMS.json'); this.getCurrentMembershipRevocatingBlock = () => that.coreFS.readJSON('indicators/revocatingMS.json'); + this.getCurrentCertificationExpiringBlock = () => that.coreFS.readJSON('indicators/expiringCRT.json'); + this.getCurrentCertificationExcludingBlock = () => that.coreFS.readJSON('indicators/excludingCRT.json'); + + this.getCurrentIdentityExpiringBlock = () => that.coreFS.readJSON('indicators/expiringIDTY.json'); + + this.getCurrentMembershipExpiringBlock = () => that.coreFS.readJSON('indicators/expiringMS.json'); } diff --git a/app/lib/dal/sqliteDAL/AbstractSQLite.js b/app/lib/dal/sqliteDAL/AbstractSQLite.js index 58d5738d6f64b768659e845fbb780f5df801cde4..6e3fbafa7fbe492e22abe5d6e14e4aed5ecb407e 100644 --- a/app/lib/dal/sqliteDAL/AbstractSQLite.js +++ b/app/lib/dal/sqliteDAL/AbstractSQLite.js @@ -144,6 +144,7 @@ function AbstractSQLite(db) { this.exec = (sql) => co(function *() { try { + // logger.trace(sql); return Q.nbind(db.exec, db)(sql); } catch (e) { logger.error('ERROR >> %s', sql); @@ -291,8 +292,12 @@ function AbstractSQLite(db) { for (const f of that.booleans) { row[f] = Boolean(row[f]); } + // Transient + for (const f of (that.transientFields || [])) { + row[f] = row[f]; + } return row; - }; + } function toRow(entity) { let row = _.clone(entity); diff --git a/app/lib/dal/sqliteDAL/CertDAL.js b/app/lib/dal/sqliteDAL/CertDAL.js index b0cd51cdb9ae53c013a54b846041ddd61333d59c..9779d04d901a65973e5307a9cb1d1c033a2dcdce 100644 --- a/app/lib/dal/sqliteDAL/CertDAL.js +++ b/app/lib/dal/sqliteDAL/CertDAL.js @@ -5,6 +5,7 @@ const Q = require('q'); const co = require('co'); const AbstractSQLite = require('./AbstractSQLite'); +const SandBox = require('./SandBox'); module.exports = CertDAL; @@ -28,7 +29,8 @@ function CertDAL(db) { 'target', 'to', 'from', - 'block' + 'block', + 'expired' ]; this.arrays = []; this.booleans = ['linked', 'written']; @@ -102,4 +104,44 @@ function CertDAL(db) { return that.exec(queries.join('\n')); } }); + + this.flagExpiredCertifications = (maxNumber, onNumber) => co(function *() { + yield that.exec('UPDATE ' + that.table + ' ' + + 'SET expired = ' + onNumber + ' ' + + 'WHERE expired IS NULL ' + + 'AND block_number <= ' + maxNumber); + }); + + this.unflagExpiredCertificationsOf = (onNumber) => co(function *() { + yield that.exec('UPDATE ' + that.table + ' ' + + 'SET expired = NULL ' + + 'WHERE expired = ' + onNumber); + }); + + /************************** + * SANDBOX STUFF + */ + + this.getSandboxCertifications = () => that.query('SELECT ' + + '* ' + + 'FROM ' + that.table + ' ' + + 'WHERE expired IS NULL ' + + 'AND written_block IS NULL ' + + 'ORDER BY block_number ASC ' + + 'LIMIT ' + (that.sandbox.maxSize), []); + + this.sandbox = new SandBox(30, this.getSandboxCertifications.bind(this), (compared, reference) => { + if (compared.block_number > reference.block_number) { + return -1; + } + else if (compared.block_number < reference.block_number) { + return 1; + } + else { + return 0; + } + }); + + this.getSandboxRoom = () => this.sandbox.getSandboxRoom(); + this.setSandboxSize = (maxSize) => this.sandbox.maxSize = maxSize; } \ No newline at end of file diff --git a/app/lib/dal/sqliteDAL/IdentityDAL.js b/app/lib/dal/sqliteDAL/IdentityDAL.js index 6bc56dc6d0231d1669f695a384abfa68f6a77981..647f731fba54d47294251a7f9f586b6ee8332f67 100644 --- a/app/lib/dal/sqliteDAL/IdentityDAL.js +++ b/app/lib/dal/sqliteDAL/IdentityDAL.js @@ -6,6 +6,7 @@ const Q = require('q'); const co = require('co'); const logger = require('../../logger')('idtyDAL'); const AbstractSQLite = require('./AbstractSQLite'); +const SandBox = require('./SandBox'); module.exports = IdentityDAL; @@ -33,11 +34,13 @@ function IdentityDAL(db, wotb) { 'sig', 'hash', 'written', - 'wotb_id' + 'wotb_id', + 'expired' ]; this.arrays = []; this.booleans = ['revoked', 'member', 'kick', 'leaving', 'wasMember', 'written']; this.pkFields = ['pubkey', 'uid', 'hash']; + this.transientFields = ['certsCount', 'ref_block']; this.translated = {}; this.init = () => co(function *() { @@ -233,6 +236,19 @@ function IdentityDAL(db, wotb) { uid: "%" + search + "%" }); + this.flagExpiredIdentities = (maxNumber, onNumber) => co(function *() { + yield that.exec('UPDATE ' + that.table + ' ' + + 'SET expired = ' + onNumber + ' ' + + 'WHERE expired IS NULL ' + + 'AND CAST(SUBSTR(buid, 0, INSTR(buid, "-")) as number) <= ' + maxNumber); + }); + + this.unflagExpiredIdentitiesOf = (onNumber) => co(function *() { + yield that.exec('UPDATE ' + that.table + ' ' + + 'SET expired = NULL ' + + 'WHERE expired = ' + onNumber); + }); + this.kickMembersForMembershipBelow = (maxNumber) => co(function *() { const toKick = yield that.sqlFind({ currentINN: { $lte: maxNumber }, @@ -258,4 +274,40 @@ function IdentityDAL(db, wotb) { yield that.saveEntity(idty); } }); + + /************************** + * SANDBOX STUFF + */ + + this.getSandboxIdentities = () => that.query('SELECT ' + + 'I.*, ' + + 'I.hash, ' + + '(SELECT COUNT(*) FROM cert C where C.target = I.hash) AS certsCount, ' + + 'CAST(SUBSTR(buid, 0, INSTR(buid, "-")) as number) AS ref_block ' + + 'FROM ' + that.table + ' as I ' + + 'WHERE NOT I.member ' + + 'AND I.expired IS NULL ' + + 'ORDER BY certsCount DESC, ref_block ASC ' + + 'LIMIT ' + (that.sandbox.maxSize), []); + + this.sandbox = new SandBox(10, this.getSandboxIdentities.bind(this), (compared, reference) => { + if (compared.certsCount < reference.certsCount) { + return -1; + } + else if (compared.certsCount > reference.certsCount) { + return 1; + } + else if (compared.ref_block > reference.ref_block) { + return -1; + } + else if (compared.ref_block < reference.ref_block) { + return 1; + } + else { + return 0; + } + }); + + this.getSandboxRoom = () => this.sandbox.getSandboxRoom(); + this.setSandboxSize = (maxSize) => this.sandbox.maxSize = maxSize; } diff --git a/app/lib/dal/sqliteDAL/MembershipDAL.js b/app/lib/dal/sqliteDAL/MembershipDAL.js index 10a60b134b8bf46d85ffe30a50122fcc638ee8f3..99a3276eb33b3372647b45945b3a326640ce3e55 100644 --- a/app/lib/dal/sqliteDAL/MembershipDAL.js +++ b/app/lib/dal/sqliteDAL/MembershipDAL.js @@ -6,6 +6,7 @@ const Q = require('q'); const co = require('co'); const _ = require('underscore'); const AbstractSQLite = require('./AbstractSQLite'); +const SandBox = require('./SandBox'); module.exports = MembershipDAL; @@ -31,7 +32,8 @@ function MembershipDAL(db) { 'idtyHash', 'written', 'written_number', - 'signature' + 'signature', + 'expired' ]; this.arrays = []; this.booleans = ['written']; @@ -155,4 +157,44 @@ function MembershipDAL(db) { return that.exec(queries.join('\n')); } }); + + this.flagExpiredMemberships = (maxNumber, onNumber) => co(function *() { + yield that.exec('UPDATE ' + that.table + ' ' + + 'SET expired = ' + onNumber + ' ' + + 'WHERE expired IS NULL ' + + 'AND blockNumber <= ' + maxNumber); + }); + + this.unflagExpiredMembershipsOf = (onNumber) => co(function *() { + yield that.exec('UPDATE ' + that.table + ' ' + + 'SET expired = NULL ' + + 'WHERE expired = ' + onNumber); + }); + + /************************** + * SANDBOX STUFF + */ + + this.getSandboxMemberships = () => that.query('SELECT ' + + '* ' + + 'FROM ' + that.table + ' ' + + 'WHERE expired IS NULL ' + + 'AND written_number IS NULL ' + + 'ORDER BY blockNumber ASC ' + + 'LIMIT ' + (that.sandbox.maxSize), []); + + this.sandbox = new SandBox(30, this.getSandboxMemberships.bind(this), (compared, reference) => { + if (compared.block_number > reference.block_number) { + return -1; + } + else if (compared.block_number < reference.block_number) { + return 1; + } + else { + return 0; + } + }); + + this.getSandboxRoom = () => this.sandbox.getSandboxRoom(); + this.setSandboxSize = (maxSize) => this.sandbox.maxSize = maxSize; } diff --git a/app/lib/dal/sqliteDAL/MetaDAL.js b/app/lib/dal/sqliteDAL/MetaDAL.js index b319f9a39ce8b95e96da7f96a03ee2c56111e6c4..7d234043b86fb66c875ee22d29dc0c7754ccd821 100644 --- a/app/lib/dal/sqliteDAL/MetaDAL.js +++ b/app/lib/dal/sqliteDAL/MetaDAL.js @@ -99,7 +99,15 @@ function MetaDAL(db) { } // Blocks since last UD have the same monetary mass as last UD block yield blockDAL.exec('UPDATE block SET monetaryMass = ' + monetaryMass + ' WHERE number >= ' + lastUDBlock); - }) + }), + + 6: 'BEGIN; ALTER TABLE idty ADD COLUMN expired INTEGER NULL; COMMIT;', + 7: 'BEGIN; ALTER TABLE cert ADD COLUMN expired INTEGER NULL; COMMIT;', + 8: 'BEGIN; ALTER TABLE membership ADD COLUMN expired INTEGER NULL; COMMIT;', + 9: 'BEGIN;' + + 'ALTER TABLE txs ADD COLUMN output_base INTEGER NULL;' + + 'ALTER TABLE txs ADD COLUMN output_amount INTEGER NULL;' + + 'COMMIT;' }; this.init = () => co(function *() { @@ -112,28 +120,41 @@ function MetaDAL(db) { 'COMMIT;', []); }); - this.upgradeDatabase = () => co(function *() { - let version = yield that.getVersion(); - while(migrations[version]) { - logger.debug("Upgrading from v%s to v%s...", version, version + 1); - - if (typeof migrations[version] == "string") { + function executeMigration(migration) { + return co(function *() { + if (typeof migration == "string") { // Simple SQL script to pass - yield that.exec(migrations[version]); + yield that.exec(migration); - } else if (typeof migrations[version] == "function") { + } else if (typeof migration == "function") { // JS function to execute - yield migrations[version](); - + yield migration(); + } + }); + } + + this.upgradeDatabase = () => co(function *() { + let version = yield that.getVersion(); + while(migrations[version]) { + logger.debug("Upgrading from v%s to v%s...", version, version + 1); + + yield executeMigration(migrations[version]); // Automated increment yield that.exec('UPDATE meta SET version = version + 1'); version++; } }); + this.upgradeDatabaseVersions = (versions) => co(function *() { + for (const version of versions) { + logger.debug("Upgrading from to v%s...", version, version + 1); + yield executeMigration(migrations[version]); + } + }); + this.getRow = () => that.sqlFindOne({ id: 1 }); this.getVersion = () => co(function *() { diff --git a/app/lib/dal/sqliteDAL/SandBox.js b/app/lib/dal/sqliteDAL/SandBox.js new file mode 100644 index 0000000000000000000000000000000000000000..e452310a348976dac7abbd39de533f23d7a2819d --- /dev/null +++ b/app/lib/dal/sqliteDAL/SandBox.js @@ -0,0 +1,31 @@ +"use strict"; + +const co = require('co'); +const colors = require('colors'); +const logger = require('../../logger')('sqlite'); + +module.exports = SandBox; + +function SandBox(maxSize, findElements, compareElements) { + + const that = this; + this.maxSize = maxSize || 10; + + this.acceptNewSandBoxEntry = (element, pubkey) => co(function *() { + if (element.pubkey === pubkey) { + return true; + } + const elements = yield findElements(); + if (elements.length < that.maxSize) { + return true; + } + const lowestElement = elements[elements.length - 1]; + const comparison = compareElements(element, lowestElement); + return comparison > 0; + }); + + this.getSandboxRoom = (underBlock) => co(function *() { + const elems = yield findElements(); + return that.maxSize - elems.length; + }); +} \ No newline at end of file diff --git a/app/lib/dal/sqliteDAL/TxsDAL.js b/app/lib/dal/sqliteDAL/TxsDAL.js index 19ce44c16c8578fe1844b61bb08bf250816d2133..1aa5660a11d6dd659fe7111bbc55af60c7a7a57e 100644 --- a/app/lib/dal/sqliteDAL/TxsDAL.js +++ b/app/lib/dal/sqliteDAL/TxsDAL.js @@ -7,6 +7,7 @@ const co = require('co'); const moment = require('moment'); const Transaction = require('../../entity/transaction'); const AbstractSQLite = require('./AbstractSQLite'); +const SandBox = require('./SandBox'); module.exports = TxsDAL; @@ -36,7 +37,9 @@ function TxsDAL(db) { 'issuers', 'signatories', 'signatures', - 'recipients' + 'recipients', + 'output_base', + 'output_amount' ]; this.arrays = ['inputs','unlocks','outputs','issuers','signatories','signatures','recipients']; this.booleans = ['written','removed']; @@ -145,4 +148,36 @@ function TxsDAL(db) { return that.exec(queries.join('\n')); } }); + + /************************** + * SANDBOX STUFF + */ + + this.getSandboxMemberships = () => that.query('SELECT ' + + '* ' + + 'FROM ' + that.table + ' ' + + 'WHERE NOT written ' + + 'AND NOT removed ' + + 'LIMIT ' + (that.sandbox.maxSize), []); + + this.sandbox = new SandBox(30, this.getSandboxMemberships.bind(this), (compared, reference) => { + if (compared.output_base < reference.output_base) { + return -1; + } + else if (compared.output_base > reference.output_base) { + return 1; + } + else if (compared.output_amount > reference.output_amount) { + return -1; + } + else if (compared.output_amount < reference.output_amount) { + return 1; + } + else { + return 0; + } + }); + + this.getSandboxRoom = () => this.sandbox.getSandboxRoom(); + this.setSandboxSize = (maxSize) => this.sandbox.maxSize = maxSize; } diff --git a/app/lib/entity/transaction.js b/app/lib/entity/transaction.js index 568267bba8c019d309e9ec2a52374be7787df98a..786bd9c08c346fe00a654ffe94a9845207037662 100644 --- a/app/lib/entity/transaction.js +++ b/app/lib/entity/transaction.js @@ -18,6 +18,10 @@ let Transaction = function(obj, currency) { this[key] = json[key]; }); + // Store the maximum output base + this.output_amount = this.outputs.reduce((sum, output) => sum + parseInt(output.split(':')[0]), 0); + this.output_base = this.outputs.reduce((maxBase, output) => Math.max(maxBase, parseInt(output.split(':')[1])), 0); + this.version = constants.DOCUMENTS_VERSION; this.currency = currency || this.currency; diff --git a/app/lib/helpers/http2raw.js b/app/lib/helpers/http2raw.js index 28b3f994176a3c9727e2254cd1dc3ef2a284fea6..3b70063279cdc1182df50715cf62cd15512b9ba1 100644 --- a/app/lib/helpers/http2raw.js +++ b/app/lib/helpers/http2raw.js @@ -10,7 +10,8 @@ module.exports = { peer: requiresParameter('peer', constants.ERRORS.HTTP_PARAM_PEER_REQUIRED), membership: Http2RawMembership, block: requiresParameter('block', constants.ERRORS.HTTP_PARAM_BLOCK_REQUIRED), - conf: requiresParameter('conf', constants.ERRORS.HTTP_PARAM_CONF_REQUIRED) + conf: requiresParameter('conf', constants.ERRORS.HTTP_PARAM_CONF_REQUIRED), + cpu: requiresParameter('cpu', constants.ERRORS.HTTP_PARAM_CPU_REQUIRED) }; function requiresParameter(parameter, err) { diff --git a/app/lib/logger/index.js b/app/lib/logger/index.js index 5c149af281e894a24e4857ca05873d60c3374578..7ce20d16f7eefdec2452ca720c49c806acc07e86 100644 --- a/app/lib/logger/index.js +++ b/app/lib/logger/index.js @@ -33,7 +33,7 @@ const logger = new (winston.Logger)({ transports: [ // setup console logging new (winston.transports.Console)({ - level: 'trace', + level: 'debug', levels: customLevels.levels, handleExceptions: false, colorize: true, @@ -51,7 +51,7 @@ logger.addCallbackLogs = (callbackForLog) => { loggerAttached = true; logger.add(cbLogger, { callback: callbackForLog, - level: 'trace', + level: 'debug', levels: customLevels.levels, handleExceptions: false, colorize: true, diff --git a/app/lib/streams/bma.js b/app/lib/streams/bma.js index 310312985568b255b4840078e2122e745402d1b5..f7a7400b8afb45e133643e5bbe6db4e7a6486094 100644 --- a/app/lib/streams/bma.js +++ b/app/lib/streams/bma.js @@ -1,12 +1,7 @@ "use strict"; -const co = require('co'); -const es = require('event-stream'); const network = require('../system/network'); -const dtos = require('./dtos'); -const sanitize = require('./sanitize'); - -let WebSocketServer = require('ws').Server; +const routes = require('./routes'); module.exports = function(server, interfaces, httpLogs) { @@ -27,93 +22,8 @@ module.exports = function(server, interfaces, httpLogs) { } return network.createServersAndListen('Duniter server', interfaces, httpLogs, null, (app, httpMethods) => { + + routes.bma(server, '', app, httpMethods); - const node = require('../../controllers/node')(server); - const blockchain = require('../../controllers/blockchain')(server); - const net = require('../../controllers/network')(server, server.conf); - const wot = require('../../controllers/wot')(server); - const transactions = require('../../controllers/transactions')(server); - const dividend = require('../../controllers/uds')(server); - httpMethods.httpGET( '/node/summary', node.summary, dtos.Summary); - httpMethods.httpGET( '/blockchain/parameters', blockchain.parameters, dtos.Parameters); - httpMethods.httpPOST( '/blockchain/membership', blockchain.parseMembership, dtos.Membership); - httpMethods.httpGET( '/blockchain/memberships/:search', blockchain.memberships, dtos.Memberships); - httpMethods.httpPOST( '/blockchain/block', blockchain.parseBlock, dtos.Block); - httpMethods.httpGET( '/blockchain/block/:number', blockchain.promoted, dtos.Block); - httpMethods.httpGET( '/blockchain/blocks/:count/:from', blockchain.blocks, dtos.Blocks); - httpMethods.httpGET( '/blockchain/current', blockchain.current, dtos.Block); - httpMethods.httpGET( '/blockchain/hardship/:search', blockchain.hardship, dtos.Hardship); - httpMethods.httpGET( '/blockchain/difficulties', blockchain.difficulties, dtos.Difficulties); - httpMethods.httpGET( '/blockchain/with/newcomers', blockchain.with.newcomers, dtos.Stat); - httpMethods.httpGET( '/blockchain/with/certs', blockchain.with.certs, dtos.Stat); - httpMethods.httpGET( '/blockchain/with/joiners', blockchain.with.joiners, dtos.Stat); - httpMethods.httpGET( '/blockchain/with/actives', blockchain.with.actives, dtos.Stat); - httpMethods.httpGET( '/blockchain/with/leavers', blockchain.with.leavers, dtos.Stat); - httpMethods.httpGET( '/blockchain/with/excluded', blockchain.with.excluded, dtos.Stat); - httpMethods.httpGET( '/blockchain/with/revoked', blockchain.with.revoked, dtos.Stat); - httpMethods.httpGET( '/blockchain/with/ud', blockchain.with.ud, dtos.Stat); - httpMethods.httpGET( '/blockchain/with/tx', blockchain.with.tx, dtos.Stat); - httpMethods.httpGET( '/blockchain/branches', blockchain.branches, dtos.Branches); - httpMethods.httpGET( '/network/peering', net.peer, dtos.Peer); - httpMethods.httpGET( '/network/peering/peers', net.peersGet, dtos.MerkleOfPeers); - httpMethods.httpPOST( '/network/peering/peers', net.peersPost, dtos.Peer); - httpMethods.httpGET( '/network/peers', net.peers, dtos.Peers); - httpMethods.httpPOST( '/wot/add', wot.add, dtos.Identity); - httpMethods.httpPOST( '/wot/certify', wot.certify, dtos.Cert); - httpMethods.httpPOST( '/wot/revoke', wot.revoke, dtos.Result); - httpMethods.httpGET( '/wot/lookup/:search', wot.lookup, dtos.Lookup); - httpMethods.httpGET( '/wot/members', wot.members, dtos.Members); - httpMethods.httpGET( '/wot/requirements/:search', wot.requirements, dtos.Requirements); - httpMethods.httpGET( '/wot/certifiers-of/:search', wot.certifiersOf, dtos.Certifications); - httpMethods.httpGET( '/wot/certified-by/:search', wot.certifiedBy, dtos.Certifications); - httpMethods.httpGET( '/wot/identity-of/:search', wot.identityOf, dtos.SimpleIdentity); - httpMethods.httpPOST( '/tx/process', transactions.parseTransaction, dtos.Transaction); - httpMethods.httpGET( '/tx/sources/:pubkey', transactions.getSources, dtos.Sources); - httpMethods.httpGET( '/tx/history/:pubkey', transactions.getHistory, dtos.TxHistory); - httpMethods.httpGET( '/tx/history/:pubkey/blocks/:from/:to', transactions.getHistoryBetweenBlocks, dtos.TxHistory); - httpMethods.httpGET( '/tx/history/:pubkey/times/:from/:to', transactions.getHistoryBetweenTimes, dtos.TxHistory); - httpMethods.httpGET( '/tx/history/:pubkey/pending', transactions.getPendingForPubkey, dtos.TxHistory); - httpMethods.httpGET( '/tx/pending', transactions.getPending, dtos.TxPending); - httpMethods.httpGET( '/ud/history/:pubkey', dividend.getHistory, dtos.UDHistory); - httpMethods.httpGET( '/ud/history/:pubkey/blocks/:from/:to', dividend.getHistoryBetweenBlocks, dtos.UDHistory); - httpMethods.httpGET( '/ud/history/:pubkey/times/:from/:to', dividend.getHistoryBetweenTimes, dtos.UDHistory); - - }, (httpServer) => { - - let currentBlock = {}; - let wssBlock = new WebSocketServer({ - server: httpServer, - path: '/ws/block' - }); - let wssPeer = new WebSocketServer({ - server: httpServer, - path: '/ws/peer' - }); - - wssBlock.on('connection', function connection(ws) { - co(function *() { - currentBlock = yield server.dal.getCurrentBlockOrNull(); - if (currentBlock) { - ws.send(JSON.stringify(sanitize(currentBlock, dtos.Block))); - } - }); - }); - - wssBlock.broadcast = (data) => wssBlock.clients.forEach((client) => client.send(data)); - wssPeer.broadcast = (data) => wssPeer.clients.forEach((client) => client.send(data)); - - // Forward blocks & peers - server - .pipe(es.mapSync(function(data) { - // Broadcast block - if (data.joiners) { - currentBlock = data; - wssBlock.broadcast(JSON.stringify(sanitize(currentBlock, dtos.Block))); - } - // Broadcast peer - if (data.endpoints) { - wssPeer.broadcast(JSON.stringify(sanitize(data, dtos.Peer))); - } - })); - }); + }, routes.bmaWS(server, '')); }; diff --git a/app/lib/streams/dtos.js b/app/lib/streams/dtos.js index d271c0260bb765120c8f199a694c102027bef284..06abf79fea7985142632b7cb4475f9ae7b0095c7 100644 --- a/app/lib/streams/dtos.js +++ b/app/lib/streams/dtos.js @@ -409,11 +409,16 @@ dtos.Boolean = { "success": Boolean }; +dtos.SummaryConf = { + "cpu": Number +}; + dtos.AdminSummary = { "version": String, "host": String, "current": dtos.Block, "pubkey": String, + "conf": dtos.SummaryConf, "parameters": dtos.Parameters }; diff --git a/app/lib/streams/routes.js b/app/lib/streams/routes.js new file mode 100644 index 0000000000000000000000000000000000000000..08ebb1e90b49a79f5870252d1d4e27ba3b87c90c --- /dev/null +++ b/app/lib/streams/routes.js @@ -0,0 +1,228 @@ +"use strict"; + +const co = require('co'); +const es = require('event-stream'); +const dtos = require('./dtos'); +const sanitize = require('./sanitize'); +const limiter = require('../system/limiter'); +const constants = require('../../lib/constants'); +const logger = require('../logger')('webmin'); + +const WebSocketServer = require('ws').Server; + +module.exports = { + + bma: function(server, prefix, app, httpMethods) { + + const node = require('../../controllers/node')(server); + const blockchain = require('../../controllers/blockchain')(server); + const net = require('../../controllers/network')(server, server.conf); + const wot = require('../../controllers/wot')(server); + const transactions = require('../../controllers/transactions')(server); + const dividend = require('../../controllers/uds')(server); + httpMethods.httpGET( prefix + '/node/summary', node.summary, dtos.Summary, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/parameters', blockchain.parameters, dtos.Parameters, limiter.limitAsHighUsage()); + httpMethods.httpPOST( prefix + '/blockchain/membership', blockchain.parseMembership, dtos.Membership, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/memberships/:search', blockchain.memberships, dtos.Memberships, limiter.limitAsHighUsage()); + httpMethods.httpPOST( prefix + '/blockchain/block', blockchain.parseBlock, dtos.Block, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/block/:number', blockchain.promoted, dtos.Block, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/blocks/:count/:from', blockchain.blocks, dtos.Blocks, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/current', blockchain.current, dtos.Block, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/hardship/:search', blockchain.hardship, dtos.Hardship, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/difficulties', blockchain.difficulties, dtos.Difficulties, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/with/newcomers', blockchain.with.newcomers, dtos.Stat, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/with/certs', blockchain.with.certs, dtos.Stat, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/with/joiners', blockchain.with.joiners, dtos.Stat, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/with/actives', blockchain.with.actives, dtos.Stat, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/with/leavers', blockchain.with.leavers, dtos.Stat, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/with/excluded', blockchain.with.excluded, dtos.Stat, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/with/revoked', blockchain.with.revoked, dtos.Stat, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/with/ud', blockchain.with.ud, dtos.Stat, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/with/tx', blockchain.with.tx, dtos.Stat, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/blockchain/branches', blockchain.branches, dtos.Branches, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/network/peering', net.peer, dtos.Peer, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/network/peering/peers', net.peersGet, dtos.MerkleOfPeers, limiter.limitAsVeryHighUsage()); + httpMethods.httpPOST( prefix + '/network/peering/peers', net.peersPost, dtos.Peer, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/network/peers', net.peers, dtos.Peers, limiter.limitAsHighUsage()); + httpMethods.httpPOST( prefix + '/wot/add', wot.add, dtos.Identity, limiter.limitAsHighUsage()); + httpMethods.httpPOST( prefix + '/wot/certify', wot.certify, dtos.Cert, limiter.limitAsHighUsage()); + httpMethods.httpPOST( prefix + '/wot/revoke', wot.revoke, dtos.Result, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/wot/lookup/:search', wot.lookup, dtos.Lookup, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/wot/members', wot.members, dtos.Members, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/wot/requirements/:search', wot.requirements, dtos.Requirements, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/wot/certifiers-of/:search', wot.certifiersOf, dtos.Certifications, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/wot/certified-by/:search', wot.certifiedBy, dtos.Certifications, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/wot/identity-of/:search', wot.identityOf, dtos.SimpleIdentity, limiter.limitAsHighUsage()); + httpMethods.httpPOST( prefix + '/tx/process', transactions.parseTransaction, dtos.Transaction, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/tx/sources/:pubkey', transactions.getSources, dtos.Sources, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/tx/history/:pubkey', transactions.getHistory, dtos.TxHistory, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/tx/history/:pubkey/blocks/:from/:to', transactions.getHistoryBetweenBlocks, dtos.TxHistory, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/tx/history/:pubkey/times/:from/:to', transactions.getHistoryBetweenTimes, dtos.TxHistory, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/tx/history/:pubkey/pending', transactions.getPendingForPubkey, dtos.TxHistory, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/tx/pending', transactions.getPending, dtos.TxPending, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/ud/history/:pubkey', dividend.getHistory, dtos.UDHistory, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/ud/history/:pubkey/blocks/:from/:to', dividend.getHistoryBetweenBlocks, dtos.UDHistory, limiter.limitAsHighUsage()); + httpMethods.httpGET( prefix + '/ud/history/:pubkey/times/:from/:to', dividend.getHistoryBetweenTimes, dtos.UDHistory, limiter.limitAsHighUsage()); + }, + + webmin: function(webminCtrl, app, httpMethods) { + httpMethods.httpGET( '/webmin/summary', webminCtrl.summary, dtos.AdminSummary); + httpMethods.httpPOST( '/webmin/key/preview', webminCtrl.previewPubkey, dtos.PreviewPubkey); + httpMethods.httpGET( '/webmin/server/http/start', webminCtrl.startHTTP, dtos.Boolean); + httpMethods.httpGET( '/webmin/server/http/stop', webminCtrl.stopHTTP, dtos.Boolean); + httpMethods.httpGET( '/webmin/server/http/upnp/open', webminCtrl.openUPnP, dtos.Boolean); + httpMethods.httpGET( '/webmin/server/http/upnp/regular', webminCtrl.regularUPnP, dtos.Boolean); + httpMethods.httpGET( '/webmin/server/preview_next', webminCtrl.previewNext, dtos.Block); + httpMethods.httpPOST( '/webmin/server/send_conf', webminCtrl.sendConf, dtos.Identity); + httpMethods.httpPOST( '/webmin/server/net_conf', webminCtrl.applyNetworkConf, dtos.Boolean); + httpMethods.httpPOST( '/webmin/server/key_conf', webminCtrl.applyNewKeyConf, dtos.Boolean); + httpMethods.httpPOST( '/webmin/server/cpu_conf', webminCtrl.applyCPUConf, dtos.Boolean); + httpMethods.httpGET( '/webmin/server/republish_selfpeer', webminCtrl.publishANewSelfPeer, dtos.Boolean); + httpMethods.httpPOST( '/webmin/server/test_sync', webminCtrl.testPeer, dtos.Block); + httpMethods.httpPOST( '/webmin/server/start_sync', webminCtrl.startSync, dtos.Boolean); + httpMethods.httpGET( '/webmin/server/auto_conf_network', webminCtrl.autoConfNetwork, dtos.Boolean); + httpMethods.httpGET( '/webmin/server/services/start_all', webminCtrl.startAllServices, dtos.Boolean); + httpMethods.httpGET( '/webmin/server/services/stop_all', webminCtrl.stopAllServices, dtos.Boolean); + httpMethods.httpGET( '/webmin/server/reset/data', webminCtrl.resetData, dtos.Boolean); + httpMethods.httpGET( '/webmin/network/interfaces', webminCtrl.listInterfaces, dtos.NetworkInterfaces); + httpMethods.httpGETFile('/webmin/data/duniter_export', webminCtrl.exportData); + httpMethods.httpPOST( '/webmin/data/duniter_import', webminCtrl.importData); + }, + + bmaWS: function(server, prefix) { + return (httpServer) => { + + let currentBlock = {}; + let wssBlock = new WebSocketServer({ + server: httpServer, + path: prefix + '/ws/block' + }); + let wssPeer = new WebSocketServer({ + server: httpServer, + path: prefix + '/ws/peer' + }); + + wssBlock.on('connection', function connection(ws) { + co(function *() { + currentBlock = yield server.dal.getCurrentBlockOrNull(); + if (currentBlock) { + ws.send(JSON.stringify(sanitize(currentBlock, dtos.Block))); + } + }); + }); + + wssBlock.broadcast = (data) => wssBlock.clients.forEach((client) => client.send(data)); + wssPeer.broadcast = (data) => wssPeer.clients.forEach((client) => client.send(data)); + + // Forward blocks & peers + server + .pipe(es.mapSync(function(data) { + // Broadcast block + if (data.joiners) { + currentBlock = data; + wssBlock.broadcast(JSON.stringify(sanitize(currentBlock, dtos.Block))); + } + // Broadcast peer + if (data.endpoints) { + wssPeer.broadcast(JSON.stringify(sanitize(data, dtos.Peer))); + } + })); + }; + }, + + webminWS: function(webminCtrl) { + return (httpServer) => { + + // Socket for synchronization events + let wssEvents = new WebSocketServer({ + server: httpServer, + path: '/webmin/ws' + }); + + let lastLogs = []; + wssEvents.on('connection', function connection(ws) { + + ws.on('message', () => { + wssEvents.broadcast(JSON.stringify({ + type: 'log', + value: lastLogs + })); + }); + + wssEvents.broadcast(JSON.stringify({ + type: 'log', + value: lastLogs + })); + + // The callback which write each new log message to websocket + logger.addCallbackLogs((level, msg, timestamp) => { + lastLogs.splice(0, Math.max(0, lastLogs.length - constants.WEBMIN_LOGS_CACHE + 1)); + lastLogs.push({ + timestamp: timestamp, + level: level, + msg: msg + }); + wssEvents.broadcast(JSON.stringify({ + type: 'log', + value: [{ + timestamp: timestamp, + level: level, + msg: msg + }] + })); + }); + }); + + wssEvents.broadcast = (data) => wssEvents.clients.forEach((client) => { + try { + client.send(data); + } catch (e) { + console.log(e); + } + }); + + // Forward blocks & peers + webminCtrl + .pipe(es.mapSync(function(data) { + // Broadcast block + if (data.download !== undefined) { + wssEvents.broadcast(JSON.stringify({ + type: 'download', + value: data.download + })); + } + if (data.applied !== undefined) { + wssEvents.broadcast(JSON.stringify({ + type: 'applied', + value: data.applied + })); + } + if (data.sync !== undefined) { + wssEvents.broadcast(JSON.stringify({ + type: 'sync', + value: data.sync, + msg: (data.msg && (data.msg.message || data.msg)) + })); + } + if (data.started !== undefined) { + wssEvents.broadcast(JSON.stringify({ + type: 'started', + value: data.started + })); + } + if (data.stopped !== undefined) { + wssEvents.broadcast(JSON.stringify({ + type: 'stopped', + value: data.stopped + })); + } + if (data.pulling !== undefined) { + wssEvents.broadcast(JSON.stringify({ + type: 'pulling', + value: data.pulling + })); + } + })); + }; + } +}; diff --git a/app/lib/streams/webmin.js b/app/lib/streams/webmin.js index 5aac53edb7c3485b8822ca0e31f5e480ce5affed..7702503801d90ef8ed091408eaf3f1b7f53f067e 100644 --- a/app/lib/streams/webmin.js +++ b/app/lib/streams/webmin.js @@ -1,11 +1,11 @@ "use strict"; const path = require('path'); -const es = require('event-stream'); -const constants = require('../../lib/constants'); +const routes = require('./routes'); const network = require('../system/network'); const dtos = require('../../lib/streams/dtos'); -const logger = require('../logger')('webmin'); + +const ENABLE_FILE_UPLOAD = true; let WebSocketServer = require('ws').Server; @@ -17,116 +17,13 @@ module.exports = function(dbConf, overConf, interfaces, httpLogs) { let httpLayer = network.createServersAndListen('Duniter web admin', interfaces, httpLogs, fullPath, (app, httpMethods) => { - httpMethods.httpGET( '/webmin/summary', webminCtrl.summary, dtos.AdminSummary); - httpMethods.httpPOST( '/webmin/key/preview', webminCtrl.previewPubkey, dtos.PreviewPubkey); - httpMethods.httpGET( '/webmin/server/http/start', webminCtrl.startHTTP, dtos.Boolean); - httpMethods.httpGET( '/webmin/server/http/stop', webminCtrl.stopHTTP, dtos.Boolean); - httpMethods.httpGET( '/webmin/server/http/upnp/open', webminCtrl.openUPnP, dtos.Boolean); - httpMethods.httpGET( '/webmin/server/http/upnp/regular', webminCtrl.regularUPnP, dtos.Boolean); - httpMethods.httpGET( '/webmin/server/preview_next', webminCtrl.previewNext, dtos.Block); - httpMethods.httpPOST( '/webmin/server/send_conf', webminCtrl.sendConf, dtos.Identity); - httpMethods.httpPOST( '/webmin/server/net_conf', webminCtrl.applyNetworkConf, dtos.Boolean); - httpMethods.httpPOST( '/webmin/server/key_conf', webminCtrl.applyNewKeyConf, dtos.Boolean); - httpMethods.httpGET( '/webmin/server/republish_selfpeer', webminCtrl.publishANewSelfPeer, dtos.Boolean); - httpMethods.httpPOST( '/webmin/server/start_sync', webminCtrl.startSync, dtos.Boolean); - httpMethods.httpGET( '/webmin/server/auto_conf_network', webminCtrl.autoConfNetwork, dtos.Boolean); - httpMethods.httpGET( '/webmin/server/services/start_all', webminCtrl.startAllServices, dtos.Boolean); - httpMethods.httpGET( '/webmin/server/services/stop_all', webminCtrl.stopAllServices, dtos.Boolean); - httpMethods.httpGET( '/webmin/server/reset/data', webminCtrl.resetData, dtos.Boolean); - httpMethods.httpGET( '/webmin/network/interfaces', webminCtrl.listInterfaces, dtos.NetworkInterfaces); - }, (httpServer) => { - - // Socket for synchronization events - let wssEvents = new WebSocketServer({ - server: httpServer, - path: '/webmin/ws' - }); - - let lastLogs = []; - wssEvents.on('connection', function connection(ws) { - - ws.on('message', () => { - wssEvents.broadcast(JSON.stringify({ - type: 'log', - value: lastLogs - })); - }); + routes.bma(webminCtrl.server, '/bma', app, httpMethods); + routes.webmin(webminCtrl, app, httpMethods); - wssEvents.broadcast(JSON.stringify({ - type: 'log', - value: lastLogs - })); - - // The callback which write each new log message to websocket - logger.addCallbackLogs((level, msg, timestamp) => { - lastLogs.splice(0, Math.max(0, lastLogs.length - constants.WEBMIN_LOGS_CACHE + 1)); - lastLogs.push({ - timestamp: timestamp, - level: level, - msg: msg - }); - wssEvents.broadcast(JSON.stringify({ - type: 'log', - value: [{ - timestamp: timestamp, - level: level, - msg: msg - }] - })); - }); - }); - - wssEvents.broadcast = (data) => wssEvents.clients.forEach((client) => { - try { - client.send(data); - } catch (e) { - console.log(e); - } - }); - - // Forward blocks & peers - webminCtrl - .pipe(es.mapSync(function(data) { - // Broadcast block - if (data.download !== undefined) { - wssEvents.broadcast(JSON.stringify({ - type: 'download', - value: data.download - })); - } - if (data.applied !== undefined) { - wssEvents.broadcast(JSON.stringify({ - type: 'applied', - value: data.applied - })); - } - if (data.sync !== undefined) { - wssEvents.broadcast(JSON.stringify({ - type: 'sync', - value: data.sync, - msg: (data.msg && (data.msg.message || data.msg)) - })); - } - if (data.started !== undefined) { - wssEvents.broadcast(JSON.stringify({ - type: 'started', - value: data.started - })); - } - if (data.stopped !== undefined) { - wssEvents.broadcast(JSON.stringify({ - type: 'stopped', - value: data.stopped - })); - } - if (data.pulling !== undefined) { - wssEvents.broadcast(JSON.stringify({ - type: 'pulling', - value: data.pulling - })); - } - })); - }); + }, (httpServer) => { + routes.bmaWS(webminCtrl.server, '/bma')(httpServer); + routes.webminWS(webminCtrl)(httpServer); + }, ENABLE_FILE_UPLOAD); return { httpLayer: httpLayer, diff --git a/app/lib/sync.js b/app/lib/sync.js index 6b06a7a4c9e4b97278d556ef18b6bd12aa2c1133..ca12ad5352ce1cc033b1369121f3964cdd2cae6c 100644 --- a/app/lib/sync.js +++ b/app/lib/sync.js @@ -79,6 +79,14 @@ function Synchroniser (server, host, port, conf, interactive) { } }); + this.test = (to, chunkLen, askedCautious, nopeers) => co(function*() { + const vucoin = yield getVucoin(host, port, vucoinOptions); + const peering = yield Q.nfcall(vucoin.network.peering.get); + const peer = new Peer(peering); + const node = yield peer.connect(); + return Q.nfcall(node.blockchain.current); + }); + this.sync = (to, chunkLen, askedCautious, nopeers) => co(function*() { try { @@ -219,7 +227,6 @@ function Synchroniser (server, host, port, conf, interactive) { }); entry.signature = sign; watcher.writeStatus('Peer ' + entry.pubkey); - logger.info('Peer ' + entry.pubkey); yield PeeringService.submitP(entry, false, to === undefined); } } diff --git a/app/lib/system/limiter.js b/app/lib/system/limiter.js new file mode 100644 index 0000000000000000000000000000000000000000..29959e83dbb7063e0733f724d5e14c46ce2cb479 --- /dev/null +++ b/app/lib/system/limiter.js @@ -0,0 +1,114 @@ +"use strict"; + +const A_MINUTE = 60 * 1000; +const A_SECOND = 1000; + +const Limiter = { + + /** + * Tells wether the quota is reached at current time or not. + */ + canAnswerNow() { + // Rapid decision first. + // Note: we suppose limitPerSecond < limitPerMinute + if (this.reqsSecLen < this.limitPerSecond && this.reqsMinLen < this.limitPerMinute) { + return true; + } + this.updateRequests(); + return this.reqsSecLen < this.limitPerSecond && this.reqsMinLen < this.limitPerMinute; + }, + + /** + * Filter the current requests stock to remove the too old ones + */ + updateRequests() { + // Clean current requests stock and make the test again + const now = Date.now(); + let i = 0, reqs = this.reqsMin, len = this.reqsMinLen; + // Reinit specific indicators + this.reqsSec = []; + this.reqsMin = []; + while (i < len) { + const duration = now - reqs[i]; + if (duration < A_SECOND) { + this.reqsSec.push(reqs[i]); + } + if (duration < A_MINUTE) { + this.reqsMin.push(reqs[i]); + } + i++; + } + this.reqsSecLen = this.reqsSec.length; + this.reqsMinLen = this.reqsMin.length; + }, + + processRequest() { + const now = Date.now(); + this.reqsSec.push(now); + this.reqsSecLen++; + this.reqsMin.push(now); + this.reqsMinLen++; + } +}; + +let HIGH_USAGE_STRATEGY = Object.create(Limiter); +HIGH_USAGE_STRATEGY.limitPerSecond = 10; +HIGH_USAGE_STRATEGY.limitPerMinute = 300; + +let VERY_HIGH_USAGE_STRATEGY = Object.create(Limiter); +VERY_HIGH_USAGE_STRATEGY.limitPerSecond = 30; +VERY_HIGH_USAGE_STRATEGY.limitPerMinute = 30 * 60; // Limit is only per second + +let TEST_STRATEGY = Object.create(Limiter); +TEST_STRATEGY.limitPerSecond = 5; +TEST_STRATEGY.limitPerMinute = 6; + +let NO_LIMIT_STRATEGY = Object.create(Limiter); +NO_LIMIT_STRATEGY.limitPerSecond = 1000000; +NO_LIMIT_STRATEGY.limitPerMinute = 1000000 * 60; + +let disableLimits = false; + +module.exports = { + + limitAsHighUsage() { + return disableLimits ? createObject(NO_LIMIT_STRATEGY) : createObject(HIGH_USAGE_STRATEGY); + }, + + limitAsVeryHighUsage() { + return disableLimits ? createObject(NO_LIMIT_STRATEGY) : createObject(VERY_HIGH_USAGE_STRATEGY); + }, + + limitAsUnlimited() { + return createObject(NO_LIMIT_STRATEGY); + }, + + limitAsTest() { + return disableLimits ? createObject(NO_LIMIT_STRATEGY) : createObject(TEST_STRATEGY); + }, + + noLimit() { + disableLimits = true; + }, + + withLimit() { + disableLimits = false; + } +}; + +function createObject(strategy) { + + const obj = Object.create(strategy); + + // Stock of request times + obj.reqsSec = []; + + // The length of reqs. + // It is better to have it instead of calling reqs.length + obj.reqsSecLen = 0; + + // Minute specific + obj.reqsMin = []; + obj.reqsMinLen = 0; + return obj; +} \ No newline at end of file diff --git a/app/lib/system/network.js b/app/lib/system/network.js index 8cefa8547039878a10d04badebc853f76e5adacf..6bdd5e4ec18ffd1e5183ddb3160fc025b1dcb983 100644 --- a/app/lib/system/network.js +++ b/app/lib/system/network.js @@ -11,6 +11,7 @@ const morgan = require('morgan'); const errorhandler = require('errorhandler'); const bodyParser = require('body-parser'); const cors = require('cors'); +const fileUpload = require('express-fileupload'); const constants = require('../constants'); const sanitize = require('../streams/sanitize'); const logger = require('../logger')('network'); @@ -74,7 +75,7 @@ module.exports = { } }, - createServersAndListen: (name, interfaces, httpLogs, staticPath, routingCallback, listenWebSocket) => co(function *() { + createServersAndListen: (name, interfaces, httpLogs, staticPath, routingCallback, listenWebSocket, enableFileUpload) => co(function *() { const app = express(); @@ -92,6 +93,11 @@ module.exports = { // CORS for **any** HTTP request app.use(cors()); + if (enableFileUpload) { + // File upload for backup API + app.use(fileUpload()); + } + app.use(bodyParser.urlencoded({ extended: true })); @@ -103,8 +109,9 @@ module.exports = { } routingCallback(app, { - httpGET: (uri, promiseFunc, dtoContract) => handleRequest(app.get.bind(app), uri, promiseFunc, dtoContract), - httpPOST: (uri, promiseFunc, dtoContract) => handleRequest(app.post.bind(app), uri, promiseFunc, dtoContract) + httpGET: (uri, promiseFunc, dtoContract, limiter) => handleRequest(app.get.bind(app), uri, promiseFunc, dtoContract, limiter), + httpPOST: (uri, promiseFunc, dtoContract, limiter) => handleRequest(app.post.bind(app), uri, promiseFunc, dtoContract, limiter), + httpGETFile: (uri, promiseFunc, dtoContract, limiter) => handleFileRequest(app.get.bind(app), uri, promiseFunc, limiter) }); if (staticPath) { @@ -204,12 +211,17 @@ module.exports = { }) }; -const handleRequest = (method, uri, promiseFunc, dtoContract) => { +const handleRequest = (method, uri, promiseFunc, dtoContract, theLimiter) => { + const limiter = theLimiter || require('../system/limiter').limitAsUnlimited(); method(uri, function(req, res) { res.set('Access-Control-Allow-Origin', '*'); res.type('application/json'); co(function *() { try { + if (!limiter.canAnswerNow()) { + throw constants.ERRORS.HTTP_LIMITATION; + } + limiter.processRequest(); let result = yield promiseFunc(req); // Ensure of the answer format result = sanitize(result, dtoContract); @@ -223,7 +235,30 @@ const handleRequest = (method, uri, promiseFunc, dtoContract) => { } }); }); -} +}; + +const handleFileRequest = (method, uri, promiseFunc, theLimiter) => { + const limiter = theLimiter || require('../system/limiter').limitAsUnlimited(); + method(uri, function(req, res) { + res.set('Access-Control-Allow-Origin', '*'); + co(function *() { + try { + if (!limiter.canAnswerNow()) { + throw constants.ERRORS.HTTP_LIMITATION; + } + limiter.processRequest(); + let fileStream = yield promiseFunc(req); + // HTTP answer + fileStream.pipe(res); + } catch (e) { + let error = getResultingError(e); + // HTTP error + res.status(error.httpCode).send(JSON.stringify(error.uerr, null, " ")); + throw e + } + }); + }); +}; function getResultingError(e) { // Default is 500 unknown error diff --git a/app/service/BlockchainService.js b/app/service/BlockchainService.js index 9f216f31d85dd22d830f7d53ce457493a9baa9e0..e6fd7160ef13c019316449733d2d619e1f6ef56f 100644 --- a/app/service/BlockchainService.js +++ b/app/service/BlockchainService.js @@ -518,13 +518,6 @@ function BlockchainService () { return dal.pushStats(stats); } - this.obsoleteInMainBranch = (block) => co(function*(){ - // Compute obsolete links - yield mainContext.computeObsoleteLinks(block); - // Compute obsolete memberships (active, joiner) - yield mainContext.computeObsoleteMemberships(block); - }); - this.getCertificationsExludingBlock = () => co(function*() { try { const current = yield dal.getCurrentBlockOrNull(); diff --git a/app/service/IdentityService.js b/app/service/IdentityService.js index f3f9cea4b2303b833b0dcdcecd9da0e996b5389b..f61a815e7dc18e22c91b3ea0933b9af5b0c1060c 100644 --- a/app/service/IdentityService.js +++ b/app/service/IdentityService.js @@ -10,6 +10,8 @@ const Revocation = require('../../app/lib/entity/revocation'); const AbstractService = require('./AbstractService'); const co = require('co'); +const BY_ABSORPTION = true; + module.exports = () => { return new IdentityService(); }; @@ -62,7 +64,7 @@ function IdentityService () { this.getPendingFromPubkey = (pubkey) => dal.getNonWritten(pubkey); - this.submitIdentity = (obj) => { + this.submitIdentity = (obj, byAbsorption) => { let idty = new Identity(obj); // Force usage of local currency name, do not accept other currencies documents idty.currency = conf.currency || idty.currency; @@ -89,6 +91,13 @@ function IdentityService () { throw constants.ERRORS.UID_ALREADY_USED; } idty = new Identity(idty); + if (byAbsorption === BY_ABSORPTION) { + idty.certsCount = 1; + } + idty.ref_block = parseInt(idty.buid.split('-')[0]); + if (!(yield dal.idtyDAL.sandbox.acceptNewSandBoxEntry(idty, conf.pair && conf.pair.pub))) { + throw constants.ERRORS.SANDBOX_FOR_IDENTITY_IS_FULL; + } yield dal.savePendingIdentity(idty); logger.info('✔ IDTY %s %s', idty.pubkey, idty.uid); return idty; @@ -113,7 +122,7 @@ function IdentityService () { uid: cert.idty_uid, buid: cert.idty_buid, sig: cert.idty_sig - }); + }, BY_ABSORPTION); } return that.pushFIFO(() => co(function *() { logger.info('⬇ CERT %s block#%s -> %s', cert.from, cert.block_number, idty.uid); @@ -141,14 +150,11 @@ function IdentityService () { }); let existingCert = yield dal.existsCert(mCert); if (!existingCert) { - try { - yield dal.registerNewCertification(new Certification(mCert)); - logger.info('✔ CERT %s', mCert.from); - } catch (e) { - // TODO: This is weird... - logger.error(e); - logger.info('✔ CERT %s', mCert.from); + if (!(yield dal.certDAL.sandbox.acceptNewSandBoxEntry(mCert, conf.pair && conf.pair.pub))) { + throw constants.ERRORS.SANDBOX_FOR_CERT_IS_FULL; } + yield dal.registerNewCertification(new Certification(mCert)); + logger.info('✔ CERT %s', mCert.from); } } else { logger.info('✘ CERT %s %s', cert.from, cert.err); @@ -185,6 +191,11 @@ function IdentityService () { // Create identity given by the revocation const idty = new Identity(revoc); idty.revocation_sig = revoc.signature; + idty.certsCount = 0; + idty.ref_block = parseInt(idty.buid.split('-')[0]); + if (!(yield dal.idtyDAL.sandbox.acceptNewSandBoxEntry(idty, conf.pair && conf.pair.pub))) { + throw constants.ERRORS.SANDBOX_FOR_IDENTITY_IS_FULL; + } yield dal.savePendingIdentity(idty); return jsonResultTrue(); } diff --git a/app/service/MembershipService.js b/app/service/MembershipService.js index 42679cf97d9a12ad0d3d22efee78ba5f00e4a8c8..04ad95c41b39ab9e23a2860b5281805f27903d03 100644 --- a/app/service/MembershipService.js +++ b/app/service/MembershipService.js @@ -47,6 +47,10 @@ function MembershipService () { } const current = yield dal.getCurrentBlockOrNull(); yield rules.HELPERS.checkMembershipBlock(entry, current, conf, dal); + entry.pubkey = entry.issuer; + if (!(yield dal.msDAL.sandbox.acceptNewSandBoxEntry(entry, conf.pair && conf.pair.pub))) { + throw constants.ERRORS.SANDBOX_FOR_MEMERSHIP_IS_FULL; + } // Saves entry yield dal.savePendingMembership(entry); logger.info('✔ %s %s', entry.issuer, entry.membership); diff --git a/app/service/TransactionsService.js b/app/service/TransactionsService.js index ab49618eda38b67f4439d0fda432db683f5154df..9769f611f9e3ebe677ab7bdb056b713e0a253f33 100644 --- a/app/service/TransactionsService.js +++ b/app/service/TransactionsService.js @@ -2,7 +2,7 @@ const co = require('co'); const Q = require('q'); -const moment = require('moment'); +const constants = require('../lib/constants'); const rules = require('../lib/rules'); const Transaction = require('../lib/entity/transaction'); const AbstractService = require('./AbstractService'); @@ -34,6 +34,11 @@ function TransactionService () { const nextBlockWithFakeTimeVariation = { medianTime: current.medianTime + 1 }; yield Q.nbind(rules.HELPERS.checkSingleTransactionLocally, rules.HELPERS)(transaction); yield rules.HELPERS.checkSingleTransaction(transaction, nextBlockWithFakeTimeVariation, conf, dal); + const server_pubkey = conf.pair && conf.pair.pub; + transaction.pubkey = transaction.issuers.indexOf(server_pubkey) !== -1 ? server_pubkey : ''; + if (!(yield dal.txsDAL.sandbox.acceptNewSandBoxEntry(transaction, server_pubkey))) { + throw constants.ERRORS.SANDBOX_FOR_TRANSACTION_IS_FULL; + } yield dal.saveTransaction(tx); return tx; })); diff --git a/appveyor.yml b/appveyor.yml index 713181df657dabd51aac296d3863c67935caea4a..5743f1d03afb1beac4e135353a6959e985e743f7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -79,7 +79,7 @@ artifacts: name: Duniter deploy: - release: v0.21.3 + release: v0.22.0 provider: GitHub auth_token: secure: Vp/M0r0i1yhGR2nhrPWEbTiDIF6r0cmwbNDFZUzdFe5clWxPXtuC0lgIpOQI78zt diff --git a/bin/duniter b/bin/duniter old mode 100644 new mode 100755 diff --git a/gui/index.html b/gui/index.html index e8d7f917b952a261b9e67bc9b850cfa2dcfd6d0e..f1ae1a4c9d049ca65307017018d465d99b9150d9 100644 --- a/gui/index.html +++ b/gui/index.html @@ -3,7 +3,7 @@ <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> - <title>Duniter 0.21.3</title> + <title>Duniter 0.22.0</title> <style> html { font-family: "Courier New", Courier, monospace; diff --git a/gui/package.json b/gui/package.json index 51fc1861f7c8b2ee2913233628b1c9bf9b68c8bd..fbda2444a104a9c9986560972518852fd2a09640 100644 --- a/gui/package.json +++ b/gui/package.json @@ -1,10 +1,10 @@ { - "name": "v0.21.3", + "name": "v0.22.0", "main": "index.html", "node-main": "../sources/bin/duniter", "window": { "icon": "duniter.png", - "title": "v0.21.3", + "title": "v0.22.0", "width": 800, "height": 800, "min_width": 750, diff --git a/install.sh b/install.sh index 466e556215c85e6a32c24f0fbc048da5392c3ad9..52a44d82462a7143fd21eabe0a18ae82015b24a3 100755 --- a/install.sh +++ b/install.sh @@ -11,7 +11,7 @@ if [ -z "$DUNITER_DIR" ]; then fi latest_version() { - echo "v0.21.3" + echo "v0.22.0" } repo_url() { diff --git a/package.json b/package.json index 4e6ec1037dc05ac23d93ba8c9d8fd00791b295ca..59c409d2ab2a4789c989148c79505e5908a23cf6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "duniter", - "version": "0.21.3", + "version": "0.22.0", "engines": { "node": ">=4.2.0", "npm": ">=2.11" @@ -34,6 +34,7 @@ "url": "https://github.com/duniter/duniter/issues" }, "dependencies": { + "archiver": "1.0.1", "async": "1.5.2", "bindings": "1.2.1", "body-parser": "1.14.2", @@ -46,6 +47,7 @@ "event-stream": "3.1.5", "express": "4.13.4", "express-cors": "0.0.3", + "express-fileupload": "0.0.5", "inquirer": "0.8.5", "jison": "0.4.17", "merkle": "0.4.0", @@ -65,6 +67,8 @@ "superagent": "1.4.0", "tweetnacl": "0.14.1", "underscore": "1.8.3", + "unzip": "0.1.11", + "unzip2": "0.2.5", "vucoin": "0.30.4", "winston": "2.1.1", "wotb": "0.4.10", diff --git a/server.js b/server.js index a50185af47e18f75ffdb2d77eec390f159fee477..63b33da9359fd777133003c9c03495e90deb39fb 100644 --- a/server.js +++ b/server.js @@ -6,6 +6,9 @@ const path = require('path'); const co = require('co'); const _ = require('underscore'); const Q = require('q'); +const archiver = require('archiver'); +const unzip = require('unzip2'); +const fs = require('fs'); const parsers = require('./app/lib/streams/parsers'); const constants = require('./app/lib/constants'); const fileDAL = require('./app/lib/dal/fileDAL'); @@ -29,6 +32,7 @@ function Server (dbConf, overrideConf) { const paramsP = directory.getHomeParams(dbConf && dbConf.memory, home); const logger = require('./app/lib/logger')('server'); const that = this; + that.home = home; that.conf = null; that.dal = null; that.version = jsonpckg.version; @@ -73,6 +77,11 @@ function Server (dbConf, overrideConf) { that.dal = fileDAL(params); }); + this.unplugFileSystem = () => co(function *() { + logger.debug('Unplugging file system...'); + yield that.dal.close(); + }); + this.softResetData = () => co(function *() { logger.debug('Soft data reset... [cache]'); yield that.dal.cleanCaches(); @@ -269,8 +278,18 @@ function Server (dbConf, overrideConf) { } }); + this.resetHome = () => co(function *() { + const params = yield paramsP; + const myFS = params.fs; + const rootPath = params.home; + const existsDir = yield myFS.exists(rootPath); + if (existsDir) { + yield myFS.removeTree(rootPath); + } + }); + this.resetAll = (done) => { - const files = ['stats', 'cores', 'current', 'conf', directory.UCOIN_DB_NAME, directory.UCOIN_DB_NAME + '.db', directory.WOTB_FILE]; + const files = ['stats', 'cores', 'current', 'conf', directory.UCOIN_DB_NAME, directory.UCOIN_DB_NAME + '.db', directory.WOTB_FILE, 'export.zip', 'import.zip']; const dirs = ['blocks', 'ud_history', 'branches', 'certs', 'txs', 'cores', 'sources', 'links', 'ms', 'identities', 'peers', 'indicators', 'leveldb']; return resetFiles(files, dirs, done); }; @@ -299,6 +318,35 @@ function Server (dbConf, overrideConf) { return that.dal.resetPeers(done); }; + this.exportAllDataAsZIP = () => co(function *() { + const params = yield paramsP; + const rootPath = params.home; + const myFS = params.fs; + const archive = archiver('zip'); + if (yield myFS.exists(path.join(rootPath, 'indicators'))) { + archive.directory(path.join(rootPath, 'indicators'), '/indicators', undefined, { name: 'indicators'}); + } + const files = ['duniter.db', 'stats.json', 'wotb.bin']; + for (const file of files) { + if (yield myFS.exists(path.join(rootPath, file))) { + archive.file(path.join(rootPath, file), { name: file }); + } + } + archive.finalize(); + return archive; + }); + + this.importAllDataFromZIP = (zipFile) => co(function *() { + const params = yield paramsP; + yield that.resetData(); + const output = unzip.Extract({ path: params.home }); + fs.createReadStream(zipFile).pipe(output); + return new Promise((resolve, reject) => { + output.on('error', reject); + output.on('close', resolve); + }); + }); + this.cleanDBData = () => co(function *() { yield _.values(that.dal.newDals).map((dal) => dal.cleanData && dal.cleanData()); that.dal.wotb.resetWoT(); @@ -398,6 +446,11 @@ function Server (dbConf, overrideConf) { syncPromise: syncPromise }; }; + + this.testForSync = (onHost, onPort) => { + const remote = new Synchroniser(that, onHost, onPort); + return remote.test(); + }; /** * Enable routing features: diff --git a/test/dal/dal.js b/test/dal/dal.js index 77f6a98982b4f8fd0d2e31746ede74256c6b5d0d..f15471e10d24017706d2edfa7727de866a0acab4 100644 --- a/test/dal/dal.js +++ b/test/dal/dal.js @@ -170,10 +170,10 @@ describe("DAL", function(){ return fileDAL.saveConf({ currency: "meta_brouzouf" }); })); - it('should have DB version 6', () => co(function *() { + it('should have DB version 10', () => co(function *() { let version = yield fileDAL.getDBVersion(); should.exist(version); - version.should.equal(6); + version.should.equal(10); })); it('should have no peer in a first time', function(){ diff --git a/test/fast/limiter-test.js b/test/fast/limiter-test.js new file mode 100644 index 0000000000000000000000000000000000000000..efc1ca239db1b5ec8c764f9901a2052c691e7478 --- /dev/null +++ b/test/fast/limiter-test.js @@ -0,0 +1,60 @@ +"use strict"; +const should = require('should'); +const co = require('co'); +const limiter = require('../../app/lib/system/limiter'); +const toolbox = require('../integration/tools/toolbox'); +const user = require('../integration/tools/user'); +const bma = require('../../app/lib/streams/bma'); + +limiter.noLimit(); + +const s1 = toolbox.server({ + pair: { + pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd', + sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP' + } +}); + +const cat = user('cat', { pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd', sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP'}, { server: s1 }); + +let theLimiter; + +describe('Limiter', () => { + + before(() => { + limiter.withLimit(); + theLimiter = limiter.limitAsTest(); + }); + + it('should not be able to send more than 10 reqs/s', () => { + theLimiter.canAnswerNow().should.equal(true); + for (let i = 1; i <= 4; i++) { + theLimiter.processRequest(); + } + theLimiter.canAnswerNow().should.equal(true); + theLimiter.processRequest(); // 5 in 1sec + theLimiter.canAnswerNow().should.equal(false); + }); + + it('should be able to send 1 more request (by minute constraint)', () => co(function*(){ + yield new Promise((resolve) => setTimeout(resolve, 1000)); + theLimiter.canAnswerNow().should.equal(true); + theLimiter.processRequest(); // 1 in 1sec, 6 in 1min + theLimiter.canAnswerNow().should.equal(false); + })); + + it('should work with BMA API', () => co(function*(){ + yield s1.initWithDAL().then(bma).then((bmapi) => bmapi.openConnections()); + yield cat.createIdentity(); + try { + for (let i = 0; i < 11; i++) { + yield s1.get('/wot/lookup/cat'); + } + throw 'Should have thrown a limiter error'; + } catch (e) { + e.should.have.property('error').property('ucode').equal(1006); + } + })); + + after(() => limiter.noLimit()); +}); diff --git a/test/integration/branches.js b/test/integration/branches.js index 98d2aa9cce2a7830dff575ec1ee09fa7e5dff175..e01e89a6dbce1fbae82254626f070e892584c718 100644 --- a/test/integration/branches.js +++ b/test/integration/branches.js @@ -44,7 +44,7 @@ describe("Branches", () => co(function*() { it('should have a 3 blocks fork window size', function() { return expectAnswer(rp('http://127.0.0.1:7778/node/summary', { json: true }), function(res) { res.should.have.property('duniter').property('software').equal('duniter'); - res.should.have.property('duniter').property('version').equal('0.21.3'); + res.should.have.property('duniter').property('version').equal('0.22.0'); res.should.have.property('duniter').property('forkWindowSize').equal(3); }); }); diff --git a/test/integration/documents-currency.js b/test/integration/documents-currency.js index 056d8d30420fb373609e4fe6c862d6d03e793189..2b2e82789a654ad8b328e7c9b7502ac1baa10af0 100644 --- a/test/integration/documents-currency.js +++ b/test/integration/documents-currency.js @@ -49,7 +49,7 @@ describe("Document pool currency", function() { it('Identity with wrong currency should be rejected', () => co(function*() { const idtyCat1 = yield s1.lookup2identity(cat1.pub); - console.log(idtyCat1.createIdentity()); + idtyCat1.createIdentity(); try { yield s2.postIdentity(idtyCat1); throw "Identity should not have been accepted, since it has an unknown currency name"; diff --git a/test/integration/forwarding.js b/test/integration/forwarding.js index 6155b12c812b9836a99af13fb1a42b2bdd7726f8..1fd477872f5b02712cf3c85d03196c6e8bf5a23b 100644 --- a/test/integration/forwarding.js +++ b/test/integration/forwarding.js @@ -7,6 +7,9 @@ const co = require('co'); const node = require('./tools/node'); const user = require('./tools/user'); const jspckg = require('../../package'); +const limiter = require('../../app/lib/system/limiter'); + +limiter.noLimit(); const MEMORY_MODE = true; diff --git a/test/integration/identity-same-pubkey.js b/test/integration/identity-same-pubkey.js index 966aa2bf06ac22d30a04a9b660939d898ac0f4b8..2614ab0fdffa2e7d8e7efa660f7f64a1dfaa9090 100644 --- a/test/integration/identity-same-pubkey.js +++ b/test/integration/identity-same-pubkey.js @@ -38,7 +38,6 @@ describe("Identities with shared pubkey", function() { })); it('should exit 2 pubkey result', () => s1.expect('/wot/lookup/cat', (res) => { - console.log(JSON.stringify(res, null, ' ')); res.results.should.have.length(2); res.results[0].should.have.property('pubkey').equal('HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd'); res.results[1].should.have.property('pubkey').equal('2LvDg21dVXvetTD9GdkPLURavLYEqP3whauvPWX4c2qc'); diff --git a/test/integration/identity-test.js b/test/integration/identity-test.js index b8aee52c13935c2b0c8a523432fed625cc402b1d..b3a4013e563990030ca813328030c7be4f7edeb3 100644 --- a/test/integration/identity-test.js +++ b/test/integration/identity-test.js @@ -10,6 +10,9 @@ const constants = require('../../app/lib/constants'); const rp = require('request-promise'); const httpTest = require('./tools/http'); const commit = require('./tools/commit'); +const limiter = require('../../app/lib/system/limiter'); + +limiter.noLimit(); const expectAnswer = httpTest.expectAnswer; diff --git a/test/integration/migrations.js b/test/integration/migrations.js index 54e9ee350a23b7d678a511ac57ebd7657f258cf9..81c1a13650f066f9b96fdeac18dc7a2e39f4b549 100644 --- a/test/integration/migrations.js +++ b/test/integration/migrations.js @@ -61,8 +61,7 @@ describe("Migration", function() { */ it('should be able to fix the issue', () => co(function*() { - yield node.server.dal.metaDAL.exec('UPDATE meta SET version = 3'); - yield node.server.dal.metaDAL.upgradeDatabase(); + yield node.server.dal.metaDAL.upgradeDatabaseVersions([3,4,5]); })); it('it should exist a received tx for toc', node.expectHttp('/tx/history/DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo', (res) => { diff --git a/test/integration/revocation-test.js b/test/integration/revocation-test.js index 1a78b7792e0f09e4f2aa76d3ad31a259b81cb809..27509104a65ecafeb1fb725f0fa40074205c9a67 100644 --- a/test/integration/revocation-test.js +++ b/test/integration/revocation-test.js @@ -8,6 +8,9 @@ const user = require('./tools/user'); const rp = require('request-promise'); const httpTest = require('./tools/http'); const commit = require('./tools/commit'); +const limiter = require('../../app/lib/system/limiter'); + +limiter.noLimit(); const expectAnswer = httpTest.expectAnswer; diff --git a/test/integration/server-import-export.js b/test/integration/server-import-export.js new file mode 100644 index 0000000000000000000000000000000000000000..c0372d89ea18daf3d5672c35b7f42a5cb045bf95 --- /dev/null +++ b/test/integration/server-import-export.js @@ -0,0 +1,60 @@ +"use strict"; +const _ = require('underscore'); +const should = require('should'); +const fs = require('fs'); +const co = require('co'); +const unzip = require('unzip'); +const toolbox = require('../integration/tools/toolbox'); +const user = require('../integration/tools/user'); +const bma = require('../../app/lib/streams/bma'); + +const serverConfig = { + memory: false, + pair: { + pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd', + sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP' + } +}; + +let s1; + +describe('Import/Export', () => { + + before(() => co(function *() { + const s0 = toolbox.server(_.extend({ homename: 'dev_unit_tests1' }, serverConfig)); + yield s0.resetHome(); + + s1 = toolbox.server(_.extend({ homename: 'dev_unit_tests1' }, serverConfig)); + + const cat = user('cat', { pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd', sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP'}, { server: s1 }); + const tac = user('tac', { pub: '2LvDg21dVXvetTD9GdkPLURavLYEqP3whauvPWX4c2qc', sec: '2HuRLWgKgED1bVio1tdpeXrf7zuUszv1yPHDsDj7kcMC4rVSN9RC58ogjtKNfTbH1eFz7rn38U1PywNs3m6Q7UxE'}, { server: s1 }); + + yield s1.initWithDAL().then(bma).then((bmapi) => bmapi.openConnections()); + yield cat.createIdentity(); + yield tac.createIdentity(); + yield cat.cert(tac); + yield tac.cert(cat); + yield cat.join(); + yield tac.join(); + yield s1.commit(); + })); + + it('should be able to export data', () => co(function *() { + const archive = yield s1.exportAllDataAsZIP(); + const output = require('fs').createWriteStream(s1.home + '/export.zip'); + archive.pipe(output); + return new Promise((resolve, reject) => { + archive.on('error', reject); + output.on('close', function() { + console.log(archive.pointer() + ' total bytes'); + console.log('archiver has been finalized and the output file descriptor has closed.'); + resolve(); + }); + }); + })); + + it('should be able to import data', () => co(function *() { + yield s1.unplugFileSystem(); + yield s1.importAllDataFromZIP(s1.home + '/export.zip'); + })); +}); diff --git a/test/integration/server-sandbox.js b/test/integration/server-sandbox.js new file mode 100644 index 0000000000000000000000000000000000000000..5be8d59bd04f43566b47e7d1039b83b2ca9eb33d --- /dev/null +++ b/test/integration/server-sandbox.js @@ -0,0 +1,258 @@ +"use strict"; + +const co = require('co'); +const should = require('should'); +const bma = require('../../app/lib/streams/bma'); +const user = require('./tools/user'); +const commit = require('./tools/commit'); +const until = require('./tools/until'); +const toolbox = require('./tools/toolbox'); +const multicaster = require('../../app/lib/streams/multicaster'); +const limiter = require('../../app/lib/system/limiter'); + +limiter.noLimit(); + +const s1 = toolbox.server({ + idtyWindow: 10, + certWindow: 10, + msWindow: 10, + dt: 10, + pair: { + pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd', + sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP' + } +}); + +const s2 = toolbox.server({ + pair: { + pub: 'DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV', + sec: '468Q1XtTq7h84NorZdWBZFJrGkB18CbmbHr9tkp9snt5GiERP7ySs3wM8myLccbAAGejgMRC9rqnXuW3iAfZACm7' + } +}); + +const s3 = toolbox.server({ + pair: { + pub: 'H9dtBFmJohAwMNXSbfoL6xfRtmrqMw8WZnjXMHr4vEHX', + sec: '2ANWb1qjjYRtT2TPFv1rBWA4EVfY7pqE4WqFUuzEgWG4vzcuvyUxMtyeBSf93M4V3g4MeEkELaj6NjA72jxnb4yF' + } +}); + +const i1 = user('i1', { pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd', sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP'}, { server: s1 }); +const i2 = user('i2', { pub: 'DNann1Lh55eZMEDXeYt59bzHbA3NJR46DeQYCS2qQdLV', sec: '468Q1XtTq7h84NorZdWBZFJrGkB18CbmbHr9tkp9snt5GiERP7ySs3wM8myLccbAAGejgMRC9rqnXuW3iAfZACm7'}, { server: s1 }); +const i3 = user('i3', { pub: 'DKpQPUL4ckzXYdnDRvCRKAm1gNvSdmAXnTrJZ7LvM5Qo', sec: '64EYRvdPpTfLGGmaX5nijLXRqWXaVz8r1Z1GtaahXwVSJGQRn7tqkxLb288zwSYzELMEG5ZhXSBYSxsTsz1m9y8F'}, { server: s1 }); +const i4 = user('i4', { pub: '2LvDg21dVXvetTD9GdkPLURavLYEqP3whauvPWX4c2qc', sec: '2HuRLWgKgED1bVio1tdpeXrf7zuUszv1yPHDsDj7kcMC4rVSN9RC58ogjtKNfTbH1eFz7rn38U1PywNs3m6Q7UxE'}, { server: s1 }); +const i5 = user('i5', { pub: '91dWdiyf7KaC4GAiKrwU7nGuue1vvmHqjCXbPziJFYtE', sec: '4Zno2b8ZULwBLY3RU5JcZhUf2a5FfXLVUMaYwPEzzN6i4ow9vXPsiCq7u2pEhkgJywqWdj97Hje1fdqnnzHeFgQe'}, { server: s1 }); +const i6 = user('i6', { pub: '3C95HniUZsUN55AJy7z4wkz1UwtebbNd63dVAZ6EaUNm', sec: '3iJMz8JKNeU692L7jvug8xVnKvzN9RDee2m6QkMKbWKrvoHhv6apS4LR9hP786PUyFYJWz8bReMrFK8PY3aGxB8m'}, { server: s1 }); +const i7 = user('i7', { pub: '4e9QJhJqHfMzEHgt3GtbfCXjqHVaQuJZKrKt8CNKR3AF', sec: 'TqdT99RpPEUjiz8su5QY7AQwharxPeo4ELCmeaFcvBEd3fW7wY7s9i531LMnTrCYBsgkrES494V6KjkhGppyEcF' }, { server: s1 }); +const i7onS2 = user('i7', { pub: '4e9QJhJqHfMzEHgt3GtbfCXjqHVaQuJZKrKt8CNKR3AF', sec: 'TqdT99RpPEUjiz8su5QY7AQwharxPeo4ELCmeaFcvBEd3fW7wY7s9i531LMnTrCYBsgkrES494V6KjkhGppyEcF' }, { server: s2 }); +const i8 = user('i8', { pub: '6GiiWjJxr29Stc4Ph4J4EipZJCzaQW1j6QXKANTNzRV3', sec: 'Yju625FGz6FHErshRc7jZyJUJ83MG4Zh9TXUNML62rKLXz7VJmwofnhJzeRRensranFJGQMYBLNSAeycAAsp62m' }, { server: s1 }); +const i9 = user('i9', { pub: '6v4HnmiGxNzKwEjnBqxicWAmdKo6Bk51GvfQByS5YmiB', sec: '2wXPPDYfM3a8jmpYiFihS9qzdqFZrLWryu4uwpNPRuw5TRW3JCdJPsMa64eAcpshLTnMXkrKL94argk3FGxzzBKh' }, { server: s1 }); +const i10 = user('i10', { pub: '6kr9Xr86qmrrwGq3XEjUXRVpHqS63FL52tcutcYGcRiv', sec: '2jCzQx7XUWoxboH67mMMv2z8VcrQabtYWpxS39iF6hNQnSBwN1d9RVauVC52PTRz6mgMzTjrSMETPrrB5N3oC7qQ' }, { server: s1 }); +const i11 = user('i11', { pub: '5VLVTp96iX3YAq7NXwZeM2N6RjCkmxaU4G6bwMg1ZNwf', sec: '3BJtyeH1Q8jPcKuzL35m4eVPGuFXpcfRiGSseVawToCWykz1qAic9V2wk31wzEqXjqCq7ZKW4MjtZrzKCGN5K7sT' }, { server: s1 }); +const i12 = user('i12', { pub: 'D6zJSPxZqs1bpgGpzJu9MgkCH7UxkG7D5u4xnnSH62wz', sec: '375vhCZdmVx7MaYD4bMZCevRLtebSuNPucfGevyPiPtdqpRzYLLNfd1h25Q59h4bm54dakpZ1RJ45ZofAyBmX4Et' }, { server: s1 }); +const i13 = user('i13', { pub: 'BQ1fhCsJGohYKKfCbt58zQ8RpiSy5M8vwzdXzm4rH7mZ', sec: '4bTX2rMeAv8x79xQdFWPgY8zQLbPZ4HE7MWKXoXHyCoYgeCFpiWLdfvXwTbt31UMGrkNp2CViEt68WkjAZAQkjjm' }, { server: s1 }); +const i14 = user('i14', { pub: 'H9dtBFmJohAwMNXSbfoL6xfRtmrqMw8WZnjXMHr4vEHX', sec: '2ANWb1qjjYRtT2TPFv1rBWA4EVfY7pqE4WqFUuzEgWG4vzcuvyUxMtyeBSf93M4V3g4MeEkELaj6NjA72jxnb4yF' }, { server: s1 }); +// const i15 = user('i15', { pub: '8cHWEmVrdT249w8vJdiBms9mbu6CguQgXx2gRVE8gfnT', sec: '5Fy9GXiLMyhvRLCpoFf35XXNj24WXX29wM6xeCQiy5Uk7ggNhRcZjjp8GcpjRyE94oNR2jRNK4eAGiYUFnvbEnGB' }, { server: s1 }); +// const i16 = user('i16', { pub: 'vi8hUTxss825cFCQE4SzmqBaAwLS236NmtrTQZBAAhG', sec: '5dVvAdWKcndQSaR9pzjEriRhGkCjef74HzecqKnydBVHdxXDewpAu3mcSU72PRKcCkTYTJPpgWmwuCyZubDKmoy4' }, { server: s1 }); + +describe("Sandboxes", function() { + + const now = parseInt(Date.now() / 1000); + + before(() => co(function*() { + + yield s1.initWithDAL().then(bma).then((bmapi) => bmapi.openConnections()); + yield s2.initWithDAL().then(bma).then((bmapi) => bmapi.openConnections()); + yield s3.initWithDAL().then(bma).then((bmapi) => bmapi.openConnections()); + s1.dal.idtyDAL.setSandboxSize(3); + s1.dal.certDAL.setSandboxSize(7); + s1.dal.msDAL.setSandboxSize(2); + s1.dal.txsDAL.setSandboxSize(2); + s2.dal.idtyDAL.setSandboxSize(10); + s3.dal.idtyDAL.setSandboxSize(3); + })); + + describe('Identities', () => { + + + it('should i1, i2, i3', () => co(function *() { + (yield s1.dal.idtyDAL.getSandboxRoom()).should.equal(3); + yield i1.createIdentity(); + yield i2.createIdentity(); + yield i3.createIdentity(); + (yield s1.dal.idtyDAL.getSandboxRoom()).should.equal(0); + })); + + it('should reject i4', () => shouldThrow(co(function *() { + (yield s1.dal.idtyDAL.getSandboxRoom()).should.equal(0); + yield i4.createIdentity(); + }))); + + it('should create i4 by i1->i4', () => co(function *() { + yield i4.createIdentity(null, s2); + yield i1.cert(i4, s2); + })); + + it('should accept i1 (already here) by i4->i1', () => co(function *() { + yield i4.cert(i1); + })); + + it('should commit & make room for sandbox, and commit again', () => co(function *() { + yield i1.join(); + yield i4.join(); + (yield s1.dal.idtyDAL.getSandboxRoom()).should.equal(0); + yield s1.commit(); + (yield s1.dal.idtyDAL.getSandboxRoom()).should.equal(1); // i2, i3 + yield s1.commit(); + yield s2.syncFrom(s1, 0, 1); + yield s3.syncFrom(s1, 0, 1); + })); + + it('should create i5(1)', () => co(function *() { + yield i5.createIdentity(); + (yield s1.dal.idtyDAL.getSandboxRoom()).should.equal(0); + })); + + it('should reject i7(1)', () => shouldThrow(i7.createIdentity())); + + it('should reject i7(1) by revocation', () => shouldThrow(co(function *() { + yield i7onS2.createIdentity(); + const idty = yield i7onS2.lookup(i7onS2.pub); + yield i7.revoke(idty); + }))); + + it('should accept i7(1), i8(1), i9(1) by i1->i7(1), i1->i8(1), i1->i9(1)', () => co(function *() { + yield i1.cert(i7, s2); + (yield s1.dal.idtyDAL.getSandboxRoom()).should.equal(0); + yield i8.createIdentity(null, s2); + yield i1.cert(i8, s2); + (yield s1.dal.idtyDAL.getSandboxRoom()).should.equal(0); + yield i9.createIdentity(null, s2); + yield i1.cert(i9, s2); + (yield s1.dal.idtyDAL.getSandboxRoom()).should.equal(0); + })); + + it('should reject i10(1) by i1->i10(1)', () => shouldThrow(co(function *() { + (yield s1.dal.idtyDAL.getSandboxRoom()).should.equal(0); + yield i10.createIdentity(null, s2); + yield i1.cert(i10, s2); + }))); + + it('should accept i10(0) by i1->i10(0) because of an anterior date compared to others in sandbox', () => co(function *() { + yield i10.createIdentity(true, s3); + yield i1.cert(i10, s3); + })); + + it('should accept i11(0) and i12(0) for the same reason', () => co(function *() { + yield i11.createIdentity(true, s3); + yield i1.cert(i11, s3); + yield i12.createIdentity(true, s3); + yield i1.cert(i12, s3); + })); + + it('should reject i13(0) because absolutely no more room is available', () => shouldThrow(co(function *() { + yield i13.createIdentity(true, s3); + yield i1.cert(i13, s3); + }))); + + it('should accept an identity with the same key as server, always', () => co(function *() { + (yield s3.dal.idtyDAL.getSandboxRoom()).should.equal(0); + yield i14.createIdentity(null, s3); + })); + + it('should make room as identities get expired', () => co(function *() { + yield s1.commit({ + time: now + 1000 + }); + yield s1.commit({ + time: now + 1000 + }); + yield s1.commit({ + time: now + 1000 + }); + (yield s1.dal.idtyDAL.getSandboxRoom()).should.equal(3); + })); + }); + + describe('Certifications', () => { + + it('should accept i4->i7(0),i4->i8(0),i4->i9(0)', () => co(function *() { + s1.dal.certDAL.setSandboxSize(3); + (yield s1.dal.certDAL.getSandboxRoom()).should.equal(3); + yield i4.cert(i7); + yield i4.cert(i8); + yield i4.cert(i9); + (yield s1.dal.certDAL.getSandboxRoom()).should.equal(0); + })); + + it('should reject i4->i10(0)', () => shouldThrow(co(function *() { + (yield s1.dal.certDAL.getSandboxRoom()).should.equal(0); + yield i4.cert(i10); + }))); + + it('should accept a certification from the same key as server, always', () => co(function *() { + (yield s1.dal.certDAL.getSandboxRoom()).should.equal(0); + yield i1.cert(i8); + })); + + it('should make room as certs get expired', () => co(function *() { + yield s1.commit({ + time: now + 1000 + }); + (yield s1.dal.certDAL.getSandboxRoom()).should.equal(3); + })); + }); + + describe('Memberships', () => { + + it('should accept i8,i9', () => co(function *() { + (yield s1.dal.msDAL.getSandboxRoom()).should.equal(2); + yield i8.join(); + yield i9.join(); + (yield s1.dal.msDAL.getSandboxRoom()).should.equal(0); + })); + + it('should reject i7', () => shouldThrow(co(function *() { + (yield s1.dal.msDAL.getSandboxRoom()).should.equal(0); + yield i7.join(); + }))); + + it('should accept a membership from the same key as server, always', () => co(function *() { + (yield s1.dal.msDAL.getSandboxRoom()).should.equal(0); + yield i1.join(); + })); + + it('should make room as membership get expired', () => co(function *() { + yield s1.commit({ + time: now + 1000 + }); + (yield s1.dal.msDAL.getSandboxRoom()).should.equal(2); + })); + }); + + describe('Transaction', () => { + + it('should accept 2 transactions of 20, 30 units', () => co(function *() { + yield i4.send(20, i1); + yield i4.send(30, i1); + })); + + it('should reject amount of 10', () => shouldThrow(co(function *() { + yield i4.send(10, i1); + }))); + + it('should accept a transaction from the same key as server, always', () => co(function *() { + yield i1.send(10, i4); + })); + + it('should make room as transactions get commited', () => co(function *() { + yield s1.commit(); + yield s1.commit(); + (yield s1.dal.txsDAL.getSandboxRoom()).should.equal(2); + })); + }); +}); + +function shouldThrow(promise) { + return promise.should.be.rejected(); +} diff --git a/test/integration/tests.js b/test/integration/tests.js index c133890eee9f1b6a9187ab31e3b3a334e97f5685..ab2c03cceb13b8e9e7ce1c48f3cb8eb5baa3b729 100644 --- a/test/integration/tests.js +++ b/test/integration/tests.js @@ -13,6 +13,9 @@ const jspckg = require('../../package'); const commit = require('./tools/commit'); const httpTest = require('./tools/http'); const rp = require('request-promise'); +const limiter = require('../../app/lib/system/limiter'); + +limiter.noLimit(); const expectAnswer = httpTest.expectAnswer; const MEMORY_MODE = true; diff --git a/test/integration/tools/toolbox.js b/test/integration/tools/toolbox.js index abc635e1dbc3e3bfbabd43b012b1526599e735bf..968279d380adad261a434d29b293e24b784c2125 100644 --- a/test/integration/tools/toolbox.js +++ b/test/integration/tools/toolbox.js @@ -37,8 +37,8 @@ module.exports = { sigQty: 1 }; const server = duniter({ - memory: MEMORY_MODE, - name: 'dev_unit_tests' + memory: conf.memory !== undefined ? conf.memory : MEMORY_MODE, + name: conf.homename || 'dev_unit_tests' }, _.extend(conf, commonConf)); server.port = port; diff --git a/test/integration/tools/user.js b/test/integration/tools/user.js index 08be2c3d108b5f1447b65395cf3213accf7d80da..cf07926322e10a1a1d8789c511e1f56bdbef7ad5 100644 --- a/test/integration/tools/user.js +++ b/test/integration/tools/user.js @@ -55,7 +55,7 @@ function User (uid, options, node) { } } - this.createIdentity = (useRoot) => co(function*() { + this.createIdentity = (useRoot, fromServer) => co(function*() { if (!pub) yield Q.nfcall(init); const current = yield node.server.BlockchainService.current(); @@ -67,9 +67,9 @@ function User (uid, options, node) { currency: node.server.conf.currency }); createIdentity += keyring.Key(pub, sec).signSync(createIdentity) + '\n'; - yield Q.nfcall(post, '/wot/add', { + yield doPost('/wot/add', { "identity": createIdentity - }); + }, fromServer); }); this.makeCert = (user, fromServer, overrideProps) => co(function*() { @@ -95,7 +95,7 @@ function User (uid, options, node) { this.cert = (user, fromServer) => co(function*() { const cert = yield that.makeCert(user, fromServer); - yield Q.nfcall(post, '/wot/certify', { + yield doPost('/wot/certify', { "cert": cert.getRaw() }); }); @@ -331,6 +331,21 @@ function User (uid, options, node) { postReq.form(data); } + function doPost(uri, data, fromServer) { + const ip = fromServer ? fromServer.conf.ipv4 : node.server.conf.remoteipv4; + const port = fromServer ? fromServer.conf.port : node.server.conf.remoteport; + return new Promise((resolve, reject) => { + var postReq = request.post({ + "uri": 'http://' + [ip, port].join(':') + uri, + "timeout": 1000 * 100000 + }, function (err, res, body) { + err = err || (res.statusCode != 200 && body != 'Already up-to-date' && body) || null; + err ? reject(err) : resolve(res); + }); + postReq.form(data); + }); + } + function getVucoin(fromServer) { return Q.Promise(function(resolve, reject){ let theNode = (fromServer && { server: fromServer }) || node; diff --git a/test/integration/transactions-test.js b/test/integration/transactions-test.js index acdd67af3dcc0c6ab45b1c9fa96cb44f83117443..8e638f206be407add8fb8dd15e7bc72bc3ab8537 100644 --- a/test/integration/transactions-test.js +++ b/test/integration/transactions-test.js @@ -11,6 +11,9 @@ const node = require('./tools/node'); const user = require('./tools/user'); const unit = require('./tools/unit'); const http = require('./tools/http'); +const limiter = require('../../app/lib/system/limiter'); + +limiter.noLimit(); describe("Testing transactions", function() { diff --git a/web-ui b/web-ui index bd229a872a7346262530a6b6d81e046d9c6a9f72..68f9b73ae824612ae1f39b91dfbca156eb8cafe1 160000 --- a/web-ui +++ b/web-ui @@ -1 +1 @@ -Subproject commit bd229a872a7346262530a6b6d81e046d9c6a9f72 +Subproject commit 68f9b73ae824612ae1f39b91dfbca156eb8cafe1