diff --git a/.eslintignore b/.eslintignore index 8604f057d756c38f85d0baf8995d7e350db6cbb2..a457fd77528e4296ad87ecc7e140a1d3d356f492 100644 --- a/.eslintignore +++ b/.eslintignore @@ -32,5 +32,9 @@ app/modules/prover/*.js app/modules/prover/lib/*.js app/modules/keypair/*.js app/modules/keypair/lib/*.js +app/modules/bma/*.js +app/modules/bma/lib/*.js +app/modules/bma/lib/entity/*.js +app/modules/bma/lib/controllers/*.js test/*.js test/**/*.js \ No newline at end of file diff --git a/app/lib/dal/fileDAL.ts b/app/lib/dal/fileDAL.ts index b6d910be81a0c37911ae997725c3c66aceeef68b..ab9e576f5c07fd18f1aff0978b9995a3acf283dd 100644 --- a/app/lib/dal/fileDAL.ts +++ b/app/lib/dal/fileDAL.ts @@ -328,7 +328,7 @@ export class FileDAL { return this.txsDAL.removeTX(hash) } - getTransactionsPending(versionMin:number) { + getTransactionsPending(versionMin = 0) { return this.txsDAL.getAllPending(versionMin) } @@ -457,7 +457,7 @@ export class FileDAL { return _(pending).sortBy((ms:any) => -ms.number)[0]; } - async findNewcomers(blockMedianTime:number) { + async findNewcomers(blockMedianTime = 0) { const pending = await this.msDAL.getPendingIN() const mss = await Promise.all(pending.map(async (p:any) => { const reduced = await this.mindexDAL.getReducedMS(p.issuer) diff --git a/app/lib/dto/BlockDTO.ts b/app/lib/dto/BlockDTO.ts index aab7328c3c65fc687810b95d016698a296340972..b1e4b3ebdea804af938a6d95d513eacc74ebc045 100644 --- a/app/lib/dto/BlockDTO.ts +++ b/app/lib/dto/BlockDTO.ts @@ -107,8 +107,12 @@ export class BlockDTO { return found; } + getRawUnSigned() { + return this.getRawInnerPart() + this.getSignedPart() + } + getRawSigned() { - return this.getRawInnerPart() + this.getSignedPart() + this.signature + "\n" + return this.getRawUnSigned() + this.signature + "\n" } getSignedPart() { diff --git a/app/lib/dto/ConfDTO.ts b/app/lib/dto/ConfDTO.ts index 0144f692bec403b9f8a98018af91815c18cb279e..f1049fdbf001b6c5464df2eda85ad0a5390c98e0 100644 --- a/app/lib/dto/ConfDTO.ts +++ b/app/lib/dto/ConfDTO.ts @@ -32,12 +32,25 @@ export interface CurrencyConfDTO { export interface KeypairConfDTO { pair: Keypair - oldPair: Keypair + oldPair: Keypair|null salt: string passwd: string } -export class ConfDTO implements CurrencyConfDTO { +export interface NetworkConfDTO { + remoteport: number + remotehost: string|null + remoteipv4: string|null + remoteipv6: string|null + port: number + ipv4: string + ipv6: string + dos:any + upnp:boolean + httplogs:boolean +} + +export class ConfDTO implements CurrencyConfDTO, KeypairConfDTO, NetworkConfDTO { constructor( public loglevel: string, @@ -79,19 +92,24 @@ export class ConfDTO implements CurrencyConfDTO { public sigWindow: number, public swichOnTimeAheadBy: number, public pair: Keypair, + public oldPair: Keypair|null, + public salt: string, + public passwd: string, public remoteport: number, - public remotehost: string, - public remoteipv4: string, - public remoteipv6: string, + public remotehost: string|null, + public remoteipv4: string|null, + public remoteipv6: string|null, public port: number, public ipv4: string, public ipv6: string, + public dos: any, + public upnp: boolean, public homename: string, public memory: boolean, ) {} static mock() { - return new ConfDTO("", "", [], [], 0, 0, 0.6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 0, false, 0, 0, 0, 0, 0, { pub:'', sec:'' }, 0, "", "", "", 0, "", "", "", true) + return new ConfDTO("", "", [], [], 0, 0, 0.6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 0, false, 0, 0, 0, 0, 0, { pub:'', sec:'' }, null, "", "", 0, "", "", "", 0, "", "", null, false, "", true) } static defaultConf() { diff --git a/app/modules/bma/index.ts b/app/modules/bma/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ae281165ef48d4b4d4b6ab30350bd0f861c5c3c0 --- /dev/null +++ b/app/modules/bma/index.ts @@ -0,0 +1,568 @@ +"use strict"; +import {NetworkConfDTO} from "../../lib/dto/ConfDTO" +import {Server} from "../../../server" +import * as stream from "stream" +import {BmaApi, Network} from "./lib/network" +import {UpnpApi} from "./lib/upnp" +import {BMAConstants} from "./lib/constants" +import {BMALimitation} from "./lib/limiter" + +const Q = require('q'); +const os = require('os'); +const async = require('async'); +const _ = require('underscore'); +const upnp = require('./lib/upnp').Upnp +const bma = require('./lib/bma').bma +const dtos = require('./lib/dtos') +const sanitize = require('./lib/sanitize'); +const http2raw = require('./lib/http2raw'); +const inquirer = require('inquirer'); + +let networkWizardDone = false; + +export const BmaDependency = { + duniter: { + + cliOptions: [ + { value: '--upnp', desc: 'Use UPnP to open remote port.' }, + { value: '--noupnp', desc: 'Do not use UPnP to open remote port.' }, + { value: '-p, --port <port>', desc: 'Port to listen for requests', parser: (val:string) => parseInt(val) }, + { value: '--ipv4 <address>', desc: 'IPv4 interface to listen for requests' }, + { value: '--ipv6 <address>', desc: 'IPv6 interface to listen for requests' }, + { value: '--remoteh <host>', desc: 'Remote interface others may use to contact this node' }, + { value: '--remote4 <host>', desc: 'Remote interface for IPv4 access' }, + { value: '--remote6 <host>', desc: 'Remote interface for IPv6 access' }, + { value: '--remotep <port>', desc: 'Remote port others may use to contact this node' }, + ], + + wizard: { + + 'network': async (conf:NetworkConfDTO, program:any, logger:any) => { + await Q.nbind(networkConfiguration, null, conf, logger)() + networkWizardDone = true; + }, + + 'network-reconfigure': async (conf:NetworkConfDTO, program:any, logger:any) => { + if (!networkWizardDone) { + // This step can only be launched lonely + await Q.nbind(networkReconfiguration, null)(conf, program.autoconf, logger, program.noupnp); + } + } + }, + + config: { + + onLoading: async (conf:NetworkConfDTO, program:any, logger:any) => { + + if (program.port !== undefined) conf.port = program.port; + if (program.ipv4 !== undefined) conf.ipv4 = program.ipv4; + if (program.ipv6 !== undefined) conf.ipv6 = program.ipv6; + if (program.remoteh !== undefined) conf.remotehost = program.remoteh; + if (program.remote4 !== undefined) conf.remoteipv4 = program.remote4; + if (program.remote6 !== undefined) conf.remoteipv6 = program.remote6; + if (program.remotep !== undefined) conf.remoteport = program.remotep; + + if (!conf.ipv4) delete conf.ipv4; + if (!conf.ipv6) delete conf.ipv6; + if (!conf.remoteipv4) delete conf.remoteipv4; + if (!conf.remoteipv6) delete conf.remoteipv6; + + // Default remoteipv6: same as local if defined + if (!conf.remoteipv6 && conf.ipv6) { + conf.remoteipv6 = conf.ipv6; + } + // Fix #807: default remoteipv4: same as local ipv4 if no removeipv4 is not defined AND no DNS nor IPv6 + if (conf.ipv4 && !(conf.remoteipv4 || conf.remotehost || conf.remoteipv6)) { + conf.remoteipv4 = conf.ipv4; + } + if (!conf.remoteport && conf.port) { + conf.remoteport = conf.port; + } + + // Network autoconf + const autoconfNet = program.autoconf + || !(conf.ipv4 || conf.ipv6) + || !(conf.remoteipv4 || conf.remoteipv6 || conf.remotehost) + || !(conf.port && conf.remoteport); + if (autoconfNet) { + await Q.nbind(networkReconfiguration, null)(conf, autoconfNet, logger, program.noupnp); + } + + // Default value + if (conf.upnp === undefined || conf.upnp === null) { + conf.upnp = true; // Defaults to true + } + if (!conf.dos) { + conf.dos = { whitelist: ['127.0.0.1'] }; + conf.dos.maxcount = 50; + conf.dos.burst = 20; + conf.dos.limit = conf.dos.burst * 2; + conf.dos.maxexpiry = 10; + conf.dos.checkinterval = 1; + conf.dos.trustProxy = true; + conf.dos.includeUserAgent = true; + conf.dos.errormessage = 'Error'; + conf.dos.testmode = false; + conf.dos.silent = false; + conf.dos.silentStart = false; + conf.dos.responseStatus = 429; + } + + // UPnP + if (program.noupnp === true) { + conf.upnp = false; + } + if (program.upnp === true) { + conf.upnp = true; + } + + // Configuration errors + if(!conf.ipv4 && !conf.ipv6){ + throw new Error("No interface to listen to."); + } + if(!conf.remoteipv4 && !conf.remoteipv6 && !conf.remotehost){ + throw new Error('No interface for remote contact.'); + } + if (!conf.remoteport) { + throw new Error('No port for remote contact.'); + } + }, + + beforeSave: async (conf:NetworkConfDTO, program:any) => { + if (!conf.ipv4) delete conf.ipv4; + if (!conf.ipv6) delete conf.ipv6; + if (!conf.remoteipv4) delete conf.remoteipv4; + if (!conf.remoteipv6) delete conf.remoteipv6; + conf.dos.whitelist = _.uniq(conf.dos.whitelist); + } + }, + + service: { + input: (server:Server, conf:NetworkConfDTO, logger:any) => { + server.getMainEndpoint = () => Promise.resolve(getEndpoint(conf)) + return new BMAPI(server, conf, logger) + } + }, + + methods: { + noLimit: () => BMALimitation.noLimit(), + bma, sanitize, dtos, + upnpConf: Network.upnpConf, + getRandomPort: Network.getRandomPort, + listInterfaces: Network.listInterfaces, + getEndpoint: getEndpoint, + getMainEndpoint: (conf:NetworkConfDTO) => Promise.resolve(getEndpoint(conf)), + getBestLocalIPv6: Network.getBestLocalIPv6, + getBestLocalIPv4: Network.getBestLocalIPv4, + createServersAndListen: Network.createServersAndListen, + http2raw + } + } +} + +export class BMAPI extends stream.Transform { + + // Public http interface + private bmapi:BmaApi + private upnpAPI:UpnpApi + + constructor( + private server:Server, + private conf:NetworkConfDTO, + private logger:any) { + super({ objectMode: true }) + } + + startService = async () => { + this.bmapi = await bma(this.server, null, this.conf.httplogs, this.logger); + await this.bmapi.openConnections(); + + /*************** + * UPnP + **************/ + if (this.upnpAPI) { + this.upnpAPI.stopRegular(); + } + if (this.server.conf.upnp) { + try { + this.upnpAPI = await upnp(this.server.conf.port, this.server.conf.remoteport, this.logger); + this.upnpAPI.startRegular(); + const gateway = await this.upnpAPI.findGateway(); + if (gateway) { + if (this.bmapi.getDDOS().params.whitelist.indexOf(gateway) === -1) { + this.bmapi.getDDOS().params.whitelist.push(gateway); + } + } + } catch (e) { + this.logger.warn(e); + } + } + } + + stopService = async () => { + if (this.bmapi) { + await this.bmapi.closeConnections(); + } + if (this.upnpAPI) { + this.upnpAPI.stopRegular(); + } + } +} + +function getEndpoint(theConf:NetworkConfDTO) { + let endpoint = 'BASIC_MERKLED_API'; + if (theConf.remotehost) { + endpoint += ' ' + theConf.remotehost; + } + if (theConf.remoteipv4) { + endpoint += ' ' + theConf.remoteipv4; + } + if (theConf.remoteipv6) { + endpoint += ' ' + theConf.remoteipv6; + } + if (theConf.remoteport) { + endpoint += ' ' + theConf.remoteport; + } + return endpoint; +} + +function networkReconfiguration(conf:NetworkConfDTO, autoconf:boolean, logger:any, noupnp:boolean, done:any) { + async.waterfall([ + upnpResolve.bind(null, noupnp, logger), + function(upnpSuccess:boolean, upnpConf:NetworkConfDTO, next:any) { + + // Default values + conf.port = conf.port || BMAConstants.DEFAULT_PORT; + conf.remoteport = conf.remoteport || BMAConstants.DEFAULT_PORT; + + const localOperations = getLocalNetworkOperations(conf, autoconf); + const remoteOpertions = getRemoteNetworkOperations(conf, upnpConf.remoteipv4); + const dnsOperations = getHostnameOperations(conf, logger, autoconf); + const useUPnPOperations = getUseUPnPOperations(conf, logger, autoconf); + + if (upnpSuccess) { + _.extend(conf, upnpConf); + const local = [conf.ipv4, conf.port].join(':'); + const remote = [conf.remoteipv4, conf.remoteport].join(':'); + if (autoconf) { + conf.ipv6 = conf.remoteipv6 = Network.getBestLocalIPv6(); + logger.info('IPv6: %s', conf.ipv6 || ""); + logger.info('Local IPv4: %s', local); + logger.info('Remote IPv4: %s', remote); + // Use proposed local + remote with UPnP binding + return async.waterfall(useUPnPOperations + .concat(dnsOperations), next); + } + choose("UPnP is available: duniter will be bound: \n from " + local + "\n to " + remote + "\nKeep this configuration?", true, + function () { + // Yes: not network changes + conf.ipv6 = conf.remoteipv6 = Network.getBestLocalIPv6(); + async.waterfall(useUPnPOperations + .concat(dnsOperations), next); + }, + function () { + // No: want to change + async.waterfall( + localOperations + .concat(remoteOpertions) + .concat(useUPnPOperations) + .concat(dnsOperations), next); + }); + } else { + conf.upnp = false; + if (autoconf) { + // Yes: local configuration = remote configuration + return async.waterfall( + localOperations + .concat(getHostnameOperations(conf, logger, autoconf)) + .concat([function (confDone:any) { + conf.remoteipv4 = conf.ipv4; + conf.remoteipv6 = conf.ipv6; + conf.remoteport = conf.port; + logger.info('Local & Remote IPv4: %s', [conf.ipv4, conf.port].join(':')); + logger.info('Local & Remote IPv6: %s', [conf.ipv6, conf.port].join(':')); + confDone(); + }]), next); + } + choose("UPnP is *not* available: is this a public server (like a VPS)?", true, + function () { + // Yes: local configuration = remote configuration + async.waterfall( + localOperations + .concat(getHostnameOperations(conf, logger)) + .concat([function(confDone:any) { + conf.remoteipv4 = conf.ipv4; + conf.remoteipv6 = conf.ipv6; + conf.remoteport = conf.port; + confDone(); + }]), next); + }, + function () { + // No: must give all details + async.waterfall( + localOperations + .concat(remoteOpertions) + .concat(dnsOperations), next); + }); + } + } + ], done); +} + + +async function upnpResolve(noupnp:boolean, logger:any, done:any) { + try { + let conf = await Network.upnpConf(noupnp, logger); + done(null, true, conf); + } catch (err) { + done(null, false, {}); + } +} + +function networkConfiguration(conf:NetworkConfDTO, logger:any, done:any) { + async.waterfall([ + upnpResolve.bind(null, !conf.upnp, logger), + function(upnpSuccess:boolean, upnpConf:NetworkConfDTO, next:any) { + + let operations = getLocalNetworkOperations(conf) + .concat(getRemoteNetworkOperations(conf, upnpConf.remoteipv4)); + + if (upnpSuccess) { + operations = operations.concat(getUseUPnPOperations(conf, logger)); + } + + async.waterfall(operations.concat(getHostnameOperations(conf, logger, false)), next); + } + ], done); +} + +function getLocalNetworkOperations(conf:NetworkConfDTO, autoconf:boolean = false) { + return [ + function (next:any){ + const osInterfaces = Network.listInterfaces(); + const interfaces = [{ name: "None", value: null }]; + osInterfaces.forEach(function(netInterface:any){ + const addresses = netInterface.addresses; + const filtered = _(addresses).where({family: 'IPv4'}); + filtered.forEach(function(addr:any){ + interfaces.push({ + name: [netInterface.name, addr.address].join(' '), + value: addr.address + }); + }); + }); + if (autoconf) { + conf.ipv4 = Network.getBestLocalIPv4(); + return next(); + } + inquirer.prompt([{ + type: "list", + name: "ipv4", + message: "IPv4 interface", + default: conf.ipv4, + choices: interfaces + }]).then((answers:any) => { + conf.ipv4 = answers.ipv4; + next(); + }); + }, + function (next:any){ + const osInterfaces = Network.listInterfaces(); + const interfaces:any = [{ name: "None", value: null }]; + osInterfaces.forEach(function(netInterface:any){ + const addresses = netInterface.addresses; + const filtered = _(addresses).where({ family: 'IPv6' }); + filtered.forEach(function(addr:any){ + let address = addr.address + if (addr.scopeid) + address += "%" + netInterface.name + let nameSuffix = ""; + if (addr.scopeid == 0 && !addr.internal) { + nameSuffix = " (Global)"; + } + interfaces.push({ + name: [netInterface.name, address, nameSuffix].join(' '), + internal: addr.internal, + scopeid: addr.scopeid, + value: address + }); + }); + }); + interfaces.sort((addr1:any, addr2:any) => { + if (addr1.value === null) return -1; + if (addr1.internal && !addr2.internal) return 1; + if (addr1.scopeid && !addr2.scopeid) return 1; + return 0; + }); + if (autoconf || !conf.ipv6) { + conf.ipv6 = conf.remoteipv6 = Network.getBestLocalIPv6(); + } + if (autoconf) { + return next(); + } + inquirer.prompt([{ + type: "list", + name: "ipv6", + message: "IPv6 interface", + default: conf.ipv6, + choices: interfaces + }]).then((answers:any) => { + conf.ipv6 = conf.remoteipv6 = answers.ipv6; + next(); + }); + }, + autoconf ? (done:any) => { + conf.port = Network.getRandomPort(conf); + done(); + } : async.apply(simpleInteger, "Port", "port", conf) + ]; +} + +function getRemoteNetworkOperations(conf:NetworkConfDTO, remoteipv4:string|null) { + return [ + function (next:any){ + if (!conf.ipv4) { + conf.remoteipv4 = null; + return next(null, {}); + } + const choices:any = [{ name: "None", value: null }]; + // Local interfaces + const osInterfaces = Network.listInterfaces(); + osInterfaces.forEach(function(netInterface:any){ + const addresses = netInterface.addresses; + const filtered = _(addresses).where({family: 'IPv4'}); + filtered.forEach(function(addr:any){ + choices.push({ + name: [netInterface.name, addr.address].join(' '), + value: addr.address + }); + }); + }); + if (conf.remoteipv4) { + choices.push({ name: conf.remoteipv4, value: conf.remoteipv4 }); + } + if (remoteipv4 && remoteipv4 != conf.remoteipv4) { + choices.push({ name: remoteipv4, value: remoteipv4 }); + } + choices.push({ name: "Enter new one", value: "new" }); + inquirer.prompt([{ + type: "list", + name: "remoteipv4", + message: "Remote IPv4", + default: conf.remoteipv4 || conf.ipv4 || null, + choices: choices, + validate: function (input:any) { + return !!(input && input.toString().match(BMAConstants.IPV4_REGEXP)); + } + }]).then((answers:any) => { + if (answers.remoteipv4 == "new") { + inquirer.prompt([{ + type: "input", + name: "remoteipv4", + message: "Remote IPv4", + default: conf.remoteipv4 || conf.ipv4, + validate: function (input:any) { + return !!(input && input.toString().match(BMAConstants.IPV4_REGEXP)); + } + }]).then((answers:any) => next(null, answers)); + } else { + next(null, answers); + } + }); + }, + async function (answers:any, next:any){ + conf.remoteipv4 = answers.remoteipv4; + try { + if (conf.remoteipv4 || conf.remotehost) { + await new Promise((resolve, reject) => { + const getPort = async.apply(simpleInteger, "Remote port", "remoteport", conf); + getPort((err:any) => { + if (err) return reject(err); + resolve(); + }); + }); + } else if (conf.remoteipv6) { + conf.remoteport = conf.port; + } + next(); + } catch (e) { + next(e); + } + } + ]; +} + +function getHostnameOperations(conf:NetworkConfDTO, logger:any, autoconf = false) { + return [function(next:any) { + if (!conf.ipv4) { + conf.remotehost = null; + return next(); + } + if (autoconf) { + logger.info('DNS: %s', conf.remotehost || 'No'); + return next(); + } + choose("Does this server has a DNS name?", !!conf.remotehost, + function() { + // Yes + simpleValue("DNS name:", "remotehost", "", conf, function(){ return true; }, next); + }, + function() { + conf.remotehost = null; + next(); + }); + }]; +} + +function getUseUPnPOperations(conf:NetworkConfDTO, logger:any, autoconf:boolean = false) { + return [function(next:any) { + if (!conf.ipv4) { + conf.upnp = false; + return next(); + } + if (autoconf) { + logger.info('UPnP: %s', 'Yes'); + conf.upnp = true; + return next(); + } + choose("UPnP is available: use automatic port mapping? (easier)", conf.upnp, + function() { + conf.upnp = true; + next(); + }, + function() { + conf.upnp = false; + next(); + }); + }]; +} + +function choose (question:string, defaultValue:any, ifOK:any, ifNotOK:any) { + inquirer.prompt([{ + type: "confirm", + name: "q", + message: question, + default: defaultValue + }]).then((answer:any) => { + answer.q ? ifOK() : ifNotOK(); + }); +} + +function simpleValue (question:string, property:any, defaultValue:any, conf:any, validation:any, done:any) { + inquirer.prompt([{ + type: "input", + name: property, + message: question, + default: conf[property], + validate: validation + }]).then((answers:any) => { + conf[property] = answers[property]; + done(); + }); +} + +function simpleInteger (question:string, property:any, conf:any, done:any) { + simpleValue(question, property, conf[property], conf, function (input:any) { + return input && input.toString().match(/^[0-9]+$/) ? true : false; + }, done); +} diff --git a/app/modules/bma/lib/bma.ts b/app/modules/bma/lib/bma.ts new file mode 100644 index 0000000000000000000000000000000000000000..2fcbe2cb1fe3348892228577e557142325df1775 --- /dev/null +++ b/app/modules/bma/lib/bma.ts @@ -0,0 +1,152 @@ +"use strict"; +import {Server} from "../../../../server" +import {Network, NetworkInterface} from "./network" +import * as dtos from "./dtos" +import {BMALimitation} from "./limiter" +import {BlockchainBinding} from "./controllers/blockchain" +import {NodeBinding} from "./controllers/node" +import {NetworkBinding} from "./controllers/network" +import {WOTBinding} from "./controllers/wot" +import {TransactionBinding} from "./controllers/transactions" +import {UDBinding} from "./controllers/uds" + +const co = require('co'); +const es = require('event-stream'); +const sanitize = require('./sanitize'); +const WebSocketServer = require('ws').Server; + +export const bma = function(server:Server, interfaces:NetworkInterface[], httpLogs:boolean, logger:any) { + + if (!interfaces) { + interfaces = []; + if (server.conf) { + if (server.conf.ipv4) { + interfaces = [{ + ip: server.conf.ipv4, + port: server.conf.port + }]; + } + if (server.conf.ipv6) { + interfaces.push({ + ip: server.conf.ipv6, + port: (server.conf.remoteport || server.conf.port) // We try to get the best one + }); + } + } + } + + return Network.createServersAndListen('Duniter server', server, interfaces, httpLogs, logger, null, (app:any, httpMethods:any) => { + + const node = new NodeBinding(server); + const blockchain = new BlockchainBinding(server) + const net = new NetworkBinding(server) + const wot = new WOTBinding(server) + const transactions = new TransactionBinding(server) + const dividend = new UDBinding(server) + httpMethods.httpGET( '/', node.summary, dtos.Summary, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/node/summary', node.summary, dtos.Summary, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/node/sandboxes', node.sandboxes, dtos.Sandboxes, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/parameters', blockchain.parameters, dtos.Parameters, BMALimitation.limitAsHighUsage()); + httpMethods.httpPOST( '/blockchain/membership', blockchain.parseMembership, dtos.Membership, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/memberships/:search', blockchain.memberships, dtos.Memberships, BMALimitation.limitAsHighUsage()); + httpMethods.httpPOST( '/blockchain/block', blockchain.parseBlock, dtos.Block, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/block/:number', blockchain.promoted, dtos.Block, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/blocks/:count/:from', blockchain.blocks, dtos.Blocks, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/current', blockchain.current, dtos.Block, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/hardship/:search', blockchain.hardship, dtos.Hardship, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/difficulties', blockchain.difficulties, dtos.Difficulties, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/with/newcomers', blockchain.with.newcomers, dtos.Stat, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/with/certs', blockchain.with.certs, dtos.Stat, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/with/joiners', blockchain.with.joiners, dtos.Stat, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/with/actives', blockchain.with.actives, dtos.Stat, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/with/leavers', blockchain.with.leavers, dtos.Stat, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/with/excluded', blockchain.with.excluded, dtos.Stat, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/with/revoked', blockchain.with.revoked, dtos.Stat, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/with/ud', blockchain.with.ud, dtos.Stat, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/with/tx', blockchain.with.tx, dtos.Stat, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/blockchain/branches', blockchain.branches, dtos.Branches, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/network/peering', net.peer, dtos.Peer, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/network/peering/peers', net.peersGet, dtos.MerkleOfPeers, BMALimitation.limitAsVeryHighUsage()); + httpMethods.httpPOST( '/network/peering/peers', net.peersPost, dtos.Peer, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/network/peers', net.peers, dtos.Peers, BMALimitation.limitAsHighUsage()); + httpMethods.httpPOST( '/wot/add', wot.add, dtos.Identity, BMALimitation.limitAsHighUsage()); + httpMethods.httpPOST( '/wot/certify', wot.certify, dtos.Cert, BMALimitation.limitAsHighUsage()); + httpMethods.httpPOST( '/wot/revoke', wot.revoke, dtos.Result, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/wot/lookup/:search', wot.lookup, dtos.Lookup, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/wot/members', wot.members, dtos.Members, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/wot/pending', wot.pendingMemberships, dtos.MembershipList, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/wot/requirements/:search', wot.requirements, dtos.Requirements, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/wot/requirements-of-pending/:minsig', wot.requirementsOfPending, dtos.Requirements, BMALimitation.limitAsLowUsage()); + httpMethods.httpGET( '/wot/certifiers-of/:search', wot.certifiersOf, dtos.Certifications, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/wot/certified-by/:search', wot.certifiedBy, dtos.Certifications, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/wot/identity-of/:search', wot.identityOf, dtos.SimpleIdentity, BMALimitation.limitAsHighUsage()); + httpMethods.httpPOST( '/tx/process', transactions.parseTransaction, dtos.Transaction, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/tx/hash/:hash', transactions.getByHash, dtos.Transaction, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/tx/sources/:pubkey', transactions.getSources, dtos.Sources, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/tx/history/:pubkey', transactions.getHistory, dtos.TxHistory, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/tx/history/:pubkey/blocks/:from/:to', transactions.getHistoryBetweenBlocks, dtos.TxHistory, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/tx/history/:pubkey/times/:from/:to', transactions.getHistoryBetweenTimes, dtos.TxHistory, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/tx/history/:pubkey/pending', transactions.getPendingForPubkey, dtos.TxHistory, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/tx/pending', transactions.getPending, dtos.TxPending, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/ud/history/:pubkey', dividend.getHistory, dtos.UDHistory, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/ud/history/:pubkey/blocks/:from/:to', dividend.getHistoryBetweenBlocks, dtos.UDHistory, BMALimitation.limitAsHighUsage()); + httpMethods.httpGET( '/ud/history/:pubkey/times/:from/:to', dividend.getHistoryBetweenTimes, dtos.UDHistory, BMALimitation.limitAsHighUsage()); + + }, (httpServer:any) => { + + let currentBlock = {}; + let wssBlock = new WebSocketServer({ + server: httpServer, + path: '/ws/block' + }); + let wssPeer = new WebSocketServer({ + server: httpServer, + path: '/ws/peer' + }); + + wssBlock.on('error', function (error:any) { + logger && logger.error('Error on WS Server'); + logger && logger.error(error); + }); + + wssBlock.on('connection', function connection(ws:any) { + co(function *() { + try { + currentBlock = yield server.dal.getCurrentBlockOrNull(); + if (currentBlock) { + ws.send(JSON.stringify(sanitize(currentBlock, dtos.Block))); + } + } catch (e) { + logger.error(e); + } + }); + }); + + wssBlock.broadcast = (data:any) => wssBlock.clients.forEach((client:any) => { + try { + client.send(data); + } catch (e) { + logger && logger.error('error on ws: %s', e); + } + }); + wssPeer.broadcast = (data:any) => wssPeer.clients.forEach((client:any) => client.send(data)); + + // Forward blocks & peers + server + .pipe(es.mapSync(function(data:any) { + try { + // 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))); + } + } catch (e) { + logger && logger.error('error on ws mapSync:', e); + } + })); + }); +}; diff --git a/app/modules/bma/lib/constants.ts b/app/modules/bma/lib/constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..5dd915869980a220e933533e3317ca14eb71d889 --- /dev/null +++ b/app/modules/bma/lib/constants.ts @@ -0,0 +1,47 @@ +export const BMAConstants = { + ENTITY_BLOCK: 'block', + ENTITY_IDENTITY: 'identity', + ENTITY_CERTIFICATION: 'certification', + ENTITY_MEMBERSHIP: 'membership', + ENTITY_REVOCATION: 'revocation', + ENTITY_TRANSACTION: 'transaction', + ENTITY_PEER: 'peer', + DEFAULT_PORT: 10901, + IPV4_REGEXP: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/, + IPV6_REGEXP: /^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((b((25[0-5])|(1d{2})|(2[0-4]d)|(d{1,2}))b).){3}(b((25[0-5])|(1d{2})|(2[0-4]d)|(d{1,2}))b))|(([0-9A-Fa-f]{1,4}:){0,5}:((b((25[0-5])|(1d{2})|(2[0-4]d)|(d{1,2}))b).){3}(b((25[0-5])|(1d{2})|(2[0-4]d)|(d{1,2}))b))|(::([0-9A-Fa-f]{1,4}:){0,5}((b((25[0-5])|(1d{2})|(2[0-4]d)|(d{1,2}))b).){3}(b((25[0-5])|(1d{2})|(2[0-4]d)|(d{1,2}))b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/, + PORT_START: 15000, + UPNP_INTERVAL: 300, + UPNP_TTL: 600, + PUBLIC_KEY: /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}$/, + SHA256_HASH: /^[A-F0-9]{64}$/, + + ERRORS: { + + // Technical errors + UNKNOWN: { httpCode: 500, uerr: { ucode: 1001, message: "An unknown error occured" }}, + UNHANDLED: { httpCode: 500, uerr: { ucode: 1002, message: "An unhandled error occured" }}, + HTTP_LIMITATION: { httpCode: 503, uerr: { ucode: 1006, message: "This URI has reached its maximum usage quota. Please 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" }}, + HTTP_PARAM_PEER_REQUIRED: { httpCode: 400, uerr: { ucode: 1103, message: "Requires a peer" }}, + HTTP_PARAM_BLOCK_REQUIRED: { httpCode: 400, uerr: { ucode: 1104, message: "Requires a block" }}, + HTTP_PARAM_MEMBERSHIP_REQUIRED: { httpCode: 400, uerr: { ucode: 1105, message: "Requires a membership" }}, + HTTP_PARAM_TX_REQUIRED: { httpCode: 400, uerr: { ucode: 1106, message: "Requires a transaction" }}, + HTTP_PARAM_SIG_REQUIRED: { httpCode: 400, uerr: { ucode: 1107, message: "Parameter `sig` is required" }}, + 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" }}, + SELF_PEER_NOT_FOUND: { httpCode: 404, uerr: { ucode: 2005, message: "Self peering was not found" }}, + NOT_A_MEMBER: { httpCode: 400, uerr: { ucode: 2009, message: "Not a member" }}, + NO_CURRENT_BLOCK: { httpCode: 404, uerr: { ucode: 2010, message: "No current block" }}, + PEER_NOT_FOUND: { httpCode: 404, uerr: { ucode: 2012, message: "Peer not found" }}, + NO_IDTY_MATCHING_PUB_OR_UID: { httpCode: 404, uerr: { ucode: 2021, message: "No identity matching this pubkey or uid" }}, + TX_NOT_FOUND: { httpCode: 400, uerr: { ucode: 2034, message: 'Transaction not found' }} + + // New errors: range 3000-4000 + } +} \ No newline at end of file diff --git a/app/modules/bma/lib/controllers/AbstractController.ts b/app/modules/bma/lib/controllers/AbstractController.ts new file mode 100644 index 0000000000000000000000000000000000000000..baf52825aa4075f5131288379c717da0d22ff3cf --- /dev/null +++ b/app/modules/bma/lib/controllers/AbstractController.ts @@ -0,0 +1,46 @@ +import {Server} from "../../../../../server" + +const dos2unix = require('../dos2unix') + +export abstract class AbstractController { + + constructor(protected server:Server) { + } + + get conf() { + return this.server.conf + } + + get logger() { + return this.server.logger + } + + get BlockchainService() { + return this.server.BlockchainService + } + + get IdentityService() { + return this.server.IdentityService + } + + get PeeringService() { + return this.server.PeeringService + } + + get MerkleService() { + return this.server.MerkleService + } + + async pushEntity(req:any, rawer:(req:any)=>string, type:any) { + let rawDocument = rawer(req); + rawDocument = dos2unix(rawDocument); + const written = await this.server.writeRaw(rawDocument, type); + try { + return written.json(); + } catch (e) { + this.logger.error('Written:', written); + this.logger.error(e); + throw e; + } + } +} \ No newline at end of file diff --git a/app/modules/bma/lib/controllers/blockchain.ts b/app/modules/bma/lib/controllers/blockchain.ts new file mode 100644 index 0000000000000000000000000000000000000000..4195fabb454b4a98325e8e1ddcc4a9d20c4c4a5d --- /dev/null +++ b/app/modules/bma/lib/controllers/blockchain.ts @@ -0,0 +1,141 @@ +"use strict"; +import {Server} from "../../../../../server" +import {AbstractController} from "./AbstractController" +import {ParametersService} from "../parameters" +import {BMAConstants} from "../constants" + +const co = require('co'); +const _ = require('underscore'); +const common = require('duniter-common'); +const http2raw = require('../http2raw'); +const toJson = require('../tojson'); + +const Membership = common.document.Membership + +export class BlockchainBinding extends AbstractController { + + with:any + + constructor(server:Server) { + super(server) + this.with = { + + newcomers: this.getStat('newcomers'), + certs: this.getStat('certs'), + joiners: this.getStat('joiners'), + actives: this.getStat('actives'), + leavers: this.getStat('leavers'), + revoked: this.getStat('revoked'), + excluded: this.getStat('excluded'), + ud: this.getStat('ud'), + tx: this.getStat('tx') + } + } + + parseMembership = (req:any) => this.pushEntity(req, http2raw.membership, BMAConstants.ENTITY_MEMBERSHIP); + + parseBlock = (req:any) => this.pushEntity(req, http2raw.block, BMAConstants.ENTITY_BLOCK); + + parameters = () => this.server.dal.getParameters(); + + private getStat(statName:string) { + return async () => { + let stat = await this.server.dal.getStat(statName); + return { result: toJson.stat(stat) }; + } + } + + async promoted(req:any) { + const number = await ParametersService.getNumberP(req); + const promoted = await this.BlockchainService.promoted(number); + return toJson.block(promoted); + } + + async blocks(req:any) { + const params = ParametersService.getCountAndFrom(req); + const count = parseInt(params.count); + const from = parseInt(params.from); + let blocks = await this.BlockchainService.blocksBetween(from, count); + blocks = blocks.map((b:any) => toJson.block(b)); + return blocks; + } + + async current() { + const current = await this.server.dal.getCurrentBlockOrNull(); + if (!current) throw BMAConstants.ERRORS.NO_CURRENT_BLOCK; + return toJson.block(current); + } + + async hardship(req:any) { + let nextBlockNumber = 0; + const search = await ParametersService.getSearchP(req); + const idty = await this.IdentityService.findMemberWithoutMemberships(search); + if (!idty) { + throw BMAConstants.ERRORS.NO_MATCHING_IDENTITY; + } + if (!idty.member) { + throw BMAConstants.ERRORS.NOT_A_MEMBER; + } + const current = await this.BlockchainService.current(); + if (current) { + nextBlockNumber = current ? current.number + 1 : 0; + } + const difficulty = await this.server.getBcContext().getIssuerPersonalizedDifficulty(idty.pubkey); + return { + "block": nextBlockNumber, + "level": difficulty + }; + } + + async difficulties() { + const current = await this.server.dal.getCurrentBlockOrNull(); + const number = (current && current.number) || 0; + const issuers = await this.server.dal.getUniqueIssuersBetween(number - 1 - current.issuersFrame, number - 1); + const difficulties = []; + for (const issuer of issuers) { + const member = await this.server.dal.getWrittenIdtyByPubkey(issuer); + const difficulty = await this.server.getBcContext().getIssuerPersonalizedDifficulty(member.pubkey); + difficulties.push({ + uid: member.uid, + level: difficulty + }); + } + return { + "block": number + 1, + "levels": _.sortBy(difficulties, (diff:any) => diff.level) + }; + } + + async memberships(req:any) { + const search = await ParametersService.getSearchP(req); + const idty:any = await this.IdentityService.findMember(search); + const json = { + pubkey: idty.pubkey, + uid: idty.uid, + sigDate: idty.buid, + memberships: [] + }; + json.memberships = idty.memberships.map((msObj:any) => { + const ms = Membership.fromJSON(msObj); + return { + version: ms.version, + currency: this.conf.currency, + membership: ms.membership, + blockNumber: parseInt(ms.blockNumber), + blockHash: ms.blockHash, + written: (!msObj.written_number && msObj.written_number !== 0) ? null : msObj.written_number + }; + }); + json.memberships = _.sortBy(json.memberships, 'blockNumber'); + json.memberships.reverse(); + return json; + } + + async branches() { + const branches = await this.BlockchainService.branches(); + const blocks = branches.map((b) => toJson.block(b)); + return { + blocks: blocks + }; + } +} diff --git a/app/modules/bma/lib/controllers/network.ts b/app/modules/bma/lib/controllers/network.ts new file mode 100644 index 0000000000000000000000000000000000000000..604846d006ee943b17893d4002f0c1fef29c043c --- /dev/null +++ b/app/modules/bma/lib/controllers/network.ts @@ -0,0 +1,57 @@ +import {AbstractController} from "./AbstractController" +import {BMAConstants} from "../constants" + +const _ = require('underscore'); +const http2raw = require('../http2raw'); + +export class NetworkBinding extends AbstractController { + + async peer() { + const p = await this.PeeringService.peer(); + if (!p) { + throw BMAConstants.ERRORS.SELF_PEER_NOT_FOUND; + } + return p.json(); + } + + async peersGet(req:any) { + let merkle = await this.server.dal.merkleForPeers(); + return await this.MerkleService(req, merkle, async (hashes:string[]) => { + try { + let peers = await this.server.dal.findPeersWhoseHashIsIn(hashes); + const map:any = {}; + peers.forEach((peer:any) => { + map[peer.hash] = peer; + }); + if (peers.length == 0) { + throw BMAConstants.ERRORS.PEER_NOT_FOUND; + } + return map; + } catch (e) { + throw e; + } + }) + } + + peersPost(req:any) { + return this.pushEntity(req, http2raw.peer, BMAConstants.ENTITY_PEER) + } + + async peers() { + let peers = await this.server.dal.listAllPeers(); + return { + peers: peers.map((p:any) => { + return _.pick(p, + 'version', + 'currency', + 'status', + 'first_down', + 'last_try', + 'pubkey', + 'block', + 'signature', + 'endpoints'); + }) + }; + } +} diff --git a/app/modules/bma/lib/controllers/node.ts b/app/modules/bma/lib/controllers/node.ts new file mode 100644 index 0000000000000000000000000000000000000000..420008dfb0707116190da84f763183e138e960b1 --- /dev/null +++ b/app/modules/bma/lib/controllers/node.ts @@ -0,0 +1,30 @@ +"use strict"; +import {AbstractController} from "./AbstractController" + +export class NodeBinding extends AbstractController { + + summary = () => { + return { + "duniter": { + "software": "duniter", + "version": this.server.version, + "forkWindowSize": this.server.conf.forksize + } + } + } + + async sandboxes() { + return { + identities: await sandboxIt(this.server.dal.idtyDAL.sandbox), + memberships: await sandboxIt(this.server.dal.msDAL.sandbox), + transactions: await sandboxIt(this.server.dal.txsDAL.sandbox) + } + } +} + +async function sandboxIt(sandbox:any) { + return { + size: sandbox.maxSize, + free: await sandbox.getSandboxRoom() + } +} diff --git a/app/modules/bma/lib/controllers/transactions.ts b/app/modules/bma/lib/controllers/transactions.ts new file mode 100644 index 0000000000000000000000000000000000000000..e09609e688676e187dca112cae7f550492d64b7d --- /dev/null +++ b/app/modules/bma/lib/controllers/transactions.ts @@ -0,0 +1,112 @@ +import {AbstractController} from "./AbstractController" +import {ParametersService} from "../parameters" +import {Source} from "../entity/source" + +const _ = require('underscore'); +const common = require('duniter-common'); +const http2raw = require('../http2raw'); + +const Transaction = common.document.Transaction + +export class TransactionBinding extends AbstractController { + + parseTransaction(req:any) { + return this.pushEntity(req, http2raw.transaction, constants.ENTITY_TRANSACTION) + } + + async getSources(req:any) { + const pubkey = await ParametersService.getPubkeyP(req); + const sources = await this.server.dal.getAvailableSourcesByPubkey(pubkey); + const result:any = { + "currency": this.conf.currency, + "pubkey": pubkey, + "sources": [] + }; + sources.forEach(function (src:any) { + result.sources.push(new Source(src).json()); + }); + return result; + } + + async getByHash(req:any) { + const hash = ParametersService.getHash(req); + const tx = await this.server.dal.getTxByHash(hash); + if (!tx) { + throw constants.ERRORS.TX_NOT_FOUND; + } + if (tx.block_number) { + tx.written_block = tx.block_number + } + tx.inputs = tx.inputs.map((i:any) => i.raw || i) + tx.outputs = tx.outputs.map((o:any) => o.raw || o) + return tx; + } + + async getHistory(req:any) { + const pubkey = await ParametersService.getPubkeyP(req); + return this.getFilteredHistory(pubkey, (results:any) => results); + } + + async getHistoryBetweenBlocks(req:any) { + const pubkey = await ParametersService.getPubkeyP(req); + const from = await ParametersService.getFromP(req); + const to = await ParametersService.getToP(req); + return this.getFilteredHistory(pubkey, (res:any) => { + const histo = res.history; + histo.sent = _.filter(histo.sent, function(tx:any){ return tx && tx.block_number >= from && tx.block_number <= to; }); + histo.received = _.filter(histo.received, function(tx:any){ return tx && tx.block_number >= from && tx.block_number <= to; }); + _.extend(histo, { sending: [], receiving: [] }); + return res; + }); + } + + async getHistoryBetweenTimes(req:any) { + const pubkey = await ParametersService.getPubkeyP(req); + const from = await ParametersService.getFromP(req); + const to = await ParametersService.getToP(req); + return this.getFilteredHistory(pubkey, (res:any) => { + const histo = res.history; + histo.sent = _.filter(histo.sent, function(tx:any){ return tx && tx.time >= from && tx.time <= to; }); + histo.received = _.filter(histo.received, function(tx:any){ return tx && tx.time >= from && tx.time <= to; }); + _.extend(histo, { sending: [], receiving: [] }); + return res; + }); + } + + async getPendingForPubkey(req:any) { + const pubkey = await ParametersService.getPubkeyP(req); + return this.getFilteredHistory(pubkey, function(res:any) { + const histo = res.history; + _.extend(histo, { sent: [], received: [] }); + return res; + }); + } + + async getPending() { + const pending = await this.server.dal.getTransactionsPending(); + const res = { + "currency": this.conf.currency, + "pending": pending + }; + pending.map(function(tx:any, index:number) { + pending[index] = _.omit(Transaction.fromJSON(tx).json(), 'currency', 'raw'); + }); + return res; + } + + private async getFilteredHistory(pubkey:string, filter:any) { + let history:any = await this.server.dal.getTransactionsHistory(pubkey); + let result = { + "currency": this.conf.currency, + "pubkey": pubkey, + "history": history + }; + _.keys(history).map((key:any) => { + history[key].map((tx:any, index:number) => { + history[key][index] = _.omit(Transaction.fromJSON(tx).json(), 'currency', 'raw'); + _.extend(history[key][index], {block_number: tx && tx.block_number, time: tx && tx.time}); + }); + }); + return filter(result); + } +} diff --git a/app/modules/bma/lib/controllers/uds.ts b/app/modules/bma/lib/controllers/uds.ts new file mode 100644 index 0000000000000000000000000000000000000000..566f59fcd4c2e5a2fac6d59247913c1d7dbb81cd --- /dev/null +++ b/app/modules/bma/lib/controllers/uds.ts @@ -0,0 +1,49 @@ +import {AbstractController} from "./AbstractController" +import {ParametersService} from "../parameters" +import {Source} from "../entity/source" + +const _ = require('underscore'); + +export class UDBinding extends AbstractController { + + async getHistory(req:any) { + const pubkey = await ParametersService.getPubkeyP(req); + return this.getUDSources(pubkey, (results:any) => results); + } + + async getHistoryBetweenBlocks(req:any) { + const pubkey = await ParametersService.getPubkeyP(req); + const from = await ParametersService.getFromP(req); + const to = await ParametersService.getToP(req); + return this.getUDSources(pubkey, (results:any) => { + results.history.history = _.filter(results.history.history, function(ud:any){ return ud.block_number >= from && ud.block_number <= to; }); + return results; + }) + } + + async getHistoryBetweenTimes(req:any) { + const pubkey = await ParametersService.getPubkeyP(req); + const from = await ParametersService.getFromP(req); + const to = await ParametersService.getToP(req); + return this.getUDSources(pubkey, (results:any) => { + results.history.history = _.filter(results.history.history, function(ud:any){ return ud.time >= from && ud.time <= to; }); + return results; + }); + } + + private async getUDSources(pubkey:string, filter:any) { + const history:any = await this.server.dal.getUDHistory(pubkey); + const result = { + "currency": this.conf.currency, + "pubkey": pubkey, + "history": history + }; + _.keys(history).map((key:any) => { + history[key].map((src:any, index:number) => { + history[key][index] = _.omit(new Source(src).UDjson(), 'currency', 'raw'); + _.extend(history[key][index], { block_number: src && src.block_number, time: src && src.time }); + }); + }); + return filter(result); + } +} diff --git a/app/modules/bma/lib/controllers/wot.ts b/app/modules/bma/lib/controllers/wot.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f21cfe988c3815f19e4145f15fba9c9eea92ce3 --- /dev/null +++ b/app/modules/bma/lib/controllers/wot.ts @@ -0,0 +1,250 @@ +import {AbstractController} from "./AbstractController" +import {BMAConstants} from "../constants" + +const _ = require('underscore'); +const common = require('duniter-common'); +const http2raw = require('../http2raw'); + +const Identity = common.document.Identity +const ParametersService = require('../parameters').ParametersService + +export class WOTBinding extends AbstractController { + + async lookup(req:any) { + // Get the search parameter from HTTP query + const search = await ParametersService.getSearchP(req); + // Make the research + const identities:any[] = await this.IdentityService.searchIdentities(search); + // Entitify each result + identities.forEach((idty, index) => identities[index] = Identity.fromJSON(idty)); + // Prepare some data to avoid displaying expired certifications + for (const idty of identities) { + const certs = await this.server.dal.certsToTarget(idty.pubkey, idty.getTargetHash()); + const validCerts = []; + for (const cert of certs) { + const member = await this.IdentityService.getWrittenByPubkey(cert.from); + if (member) { + cert.uids = [member.uid]; + cert.isMember = member.member; + cert.wasMember = member.wasMember; + } else { + const potentials = await this.IdentityService.getPendingFromPubkey(cert.from); + cert.uids = _(potentials).pluck('uid'); + cert.isMember = false; + cert.wasMember = false; + } + validCerts.push(cert); + } + idty.certs = validCerts; + const signed = await this.server.dal.certsFrom(idty.pubkey); + const validSigned = []; + for (let j = 0; j < signed.length; j++) { + const cert = _.clone(signed[j]); + cert.idty = await this.server.dal.getIdentityByHashOrNull(cert.target); + if (cert.idty) { + validSigned.push(cert); + } else { + this.logger.debug('A certification to an unknown identity was found (%s => %s)', cert.from, cert.to); + } + } + idty.signed = validSigned; + } + if (identities.length == 0) { + throw BMAConstants.ERRORS.NO_MATCHING_IDENTITY; + } + const resultsByPubkey:any = {}; + identities.forEach((identity) => { + const jsoned = identity.json(); + if (!resultsByPubkey[jsoned.pubkey]) { + // Create the first matching identity with this pubkey in the map + resultsByPubkey[jsoned.pubkey] = jsoned; + } else { + // Merge the identity with the existing(s) + const existing = resultsByPubkey[jsoned.pubkey]; + // We add the UID of the identity to the list of already added UIDs + existing.uids = existing.uids.concat(jsoned.uids); + // We do not merge the `signed`: every identity with the same pubkey has the same `signed` because it the *pubkey* which signs, not the identity + } + }); + return { + partial: false, + results: _.values(resultsByPubkey) + }; + } + + async members() { + const identities = await this.server.dal.getMembers(); + const json:any = { + results: [] + }; + identities.forEach((identity:any) => json.results.push({ pubkey: identity.pubkey, uid: identity.uid })); + return json; + } + + async certifiersOf(req:any) { + const search = await ParametersService.getSearchP(req); + const idty = await this.IdentityService.findMemberWithoutMemberships(search); + const certs = await this.server.dal.certsToTarget(idty.pubkey, idty.getTargetHash()); + idty.certs = []; + for (const cert of certs) { + const certifier = await this.server.dal.getWrittenIdtyByPubkey(cert.from); + if (certifier) { + cert.uid = certifier.uid; + cert.isMember = certifier.member; + cert.sigDate = certifier.buid; + cert.wasMember = true; // As we checked if(certified) + if (!cert.cert_time) { + let certBlock = await this.server.dal.getBlock(cert.block_number); + cert.cert_time = { + block: certBlock.number, + medianTime: certBlock.medianTime + }; + } + idty.certs.push(cert); + } + } + const json:any = { + pubkey: idty.pubkey, + uid: idty.uid, + sigDate: idty.buid, + isMember: idty.member, + certifications: [] + }; + idty.certs.forEach(function(cert){ + json.certifications.push({ + pubkey: cert.from, + uid: cert.uid, + isMember: cert.isMember, + wasMember: cert.wasMember, + cert_time: cert.cert_time, + sigDate: cert.sigDate, + written: cert.linked ? { + number: cert.written_block, + hash: cert.written_hash + } : null, + signature: cert.sig + }); + }); + return json; + } + + async requirements(req:any) { + const search = await ParametersService.getSearchP(req); + const identities:any = await this.IdentityService.searchIdentities(search); + const all = await this.BlockchainService.requirementsOfIdentities(identities); + if (!all || !all.length) { + throw BMAConstants.ERRORS.NO_IDTY_MATCHING_PUB_OR_UID; + } + return { + identities: all + }; + } + + async requirementsOfPending(req:any) { + const minsig = ParametersService.getMinSig(req) + const identities = await this.server.dal.idtyDAL.query('SELECT i.*, count(c.sig) as nbSig FROM idty i, cert c WHERE c.target = i.hash group by i.hash having nbSig >= ?', minsig) + const all = await this.BlockchainService.requirementsOfIdentities(identities); + if (!all || !all.length) { + throw BMAConstants.ERRORS.NO_IDTY_MATCHING_PUB_OR_UID; + } + return { + identities: all + }; + } + + async certifiedBy(req:any) { + const search = await ParametersService.getSearchP(req); + const idty = await this.IdentityService.findMemberWithoutMemberships(search); + const certs = await this.server.dal.certsFrom(idty.pubkey); + idty.certs = []; + for (const cert of certs) { + const certified = await this.server.dal.getWrittenIdtyByPubkey(cert.to); + if (certified) { + cert.uid = certified.uid; + cert.isMember = certified.member; + cert.sigDate = certified.buid; + cert.wasMember = true; // As we checked if(certified) + if (!cert.cert_time) { + let certBlock = await this.server.dal.getBlock(cert.block_number); + cert.cert_time = { + block: certBlock.number, + medianTime: certBlock.medianTime + }; + } + idty.certs.push(cert); + } + } + const json:any = { + pubkey: idty.pubkey, + uid: idty.uid, + sigDate: idty.buid, + isMember: idty.member, + certifications: [] + }; + idty.certs.forEach((cert) => json.certifications.push({ + pubkey: cert.to, + uid: cert.uid, + isMember: cert.isMember, + wasMember: cert.wasMember, + cert_time: cert.cert_time, + sigDate: cert.sigDate, + written: cert.linked ? { + number: cert.written_block, + hash: cert.written_hash + } : null, + signature: cert.sig + }) + ); + return json; + } + + async identityOf(req:any) { + let search = await ParametersService.getSearchP(req); + let idty = await this.IdentityService.findMemberWithoutMemberships(search); + if (!idty) { + throw 'Identity not found'; + } + if (!idty.member) { + throw 'Not a member'; + } + return { + pubkey: idty.pubkey, + uid: idty.uid, + sigDate: idty.buid + }; + } + + add(req:any) { + return this.pushEntity(req, http2raw.identity, BMAConstants.ENTITY_IDENTITY) + } + + certify(req:any) { + return this.pushEntity(req, http2raw.certification, BMAConstants.ENTITY_CERTIFICATION) + } + + revoke(req:any) { + return this.pushEntity(req, http2raw.revocation, BMAConstants.ENTITY_REVOCATION) + } + + async pendingMemberships() { + const memberships = await this.server.dal.findNewcomers(); + const json = { + memberships: [] + }; + json.memberships = memberships.map((ms:any) => { + return { + pubkey: ms.issuer, + uid: ms.userid, + version: ms.version, + currency: this.server.conf.currency, + membership: ms.membership, + blockNumber: parseInt(ms.blockNumber), + blockHash: ms.blockHash, + written: (!ms.written_number && ms.written_number !== 0) ? null : ms.written_number + }; + }); + json.memberships = _.sortBy(json.memberships, 'blockNumber'); + json.memberships.reverse(); + return json; + } +} diff --git a/app/modules/bma/lib/dos2unix.ts b/app/modules/bma/lib/dos2unix.ts new file mode 100644 index 0000000000000000000000000000000000000000..bbc290fc2f9c29643300f88cc1268e749c1d87cb --- /dev/null +++ b/app/modules/bma/lib/dos2unix.ts @@ -0,0 +1,3 @@ +module.exports = function dos2unix(str:string) { + return str.replace(/\r\n/g, '\n') +} diff --git a/app/modules/bma/lib/dtos.ts b/app/modules/bma/lib/dtos.ts new file mode 100644 index 0000000000000000000000000000000000000000..0949bbb9f5b9a34aa9fac0deebd73a4c19239aec --- /dev/null +++ b/app/modules/bma/lib/dtos.ts @@ -0,0 +1,469 @@ + +export const Summary = { + duniter: { + "software": String, + "version": String, + "forkWindowSize": Number + } +}; + +export const Parameters = { + currency: String, + c: Number, + dt: Number, + ud0: Number, + sigPeriod: Number, + sigStock: Number, + sigWindow: Number, + sigValidity: Number, + sigQty: Number, + idtyWindow: Number, + msWindow: Number, + xpercent: Number, + msValidity: Number, + stepMax: Number, + medianTimeBlocks: Number, + avgGenTime: Number, + dtDiffEval: Number, + percentRot: Number, + udTime0: Number, + udReevalTime0: Number, + dtReeval: Number +}; + +export const Membership = { + "signature": String, + "membership": { + "version": Number, + "currency": String, + "issuer": String, + "membership": String, + "date": Number, + "sigDate": Number, + "raw": String + } +}; + +export const Memberships = { + "pubkey": String, + "uid": String, + "sigDate": String, + "memberships": [ + { + "version": Number, + "currency": String, + "membership": String, + "blockNumber": Number, + "blockHash": String, + "written": Number + } + ] +}; + +export const MembershipList = { + "memberships": [ + { + "pubkey": String, + "uid": String, + "version": Number, + "currency": String, + "membership": String, + "blockNumber": Number, + "blockHash": String, + "written": Number + } + ] +}; + +export const TransactionOfBlock = { + "version": Number, + "currency": String, + "comment": String, + "locktime": Number, + "signatures": [String], + "outputs": [String], + "inputs": [String], + "unlocks": [String], + "block_number": Number, + "blockstamp": String, + "blockstampTime": Number, + "time": Number, + "issuers": [String] +}; + +export const Block = { + "version": Number, + "currency": String, + "number": Number, + "issuer": String, + "issuersFrame": Number, + "issuersFrameVar": Number, + "issuersCount": Number, + "parameters": String, + "membersCount": Number, + "monetaryMass": Number, + "powMin": Number, + "time": Number, + "medianTime": Number, + "dividend": Number, + "unitbase": Number, + "hash": String, + "previousHash": String, + "previousIssuer": String, + "identities": [String], + "certifications": [String], + "joiners": [String], + "actives": [String], + "leavers": [String], + "revoked": [String], + "excluded": [String], + "transactions": [TransactionOfBlock], + "nonce": Number, + "inner_hash": String, + "signature": String, + "raw": String +}; + +export const Hardship = { + "block": Number, + "level": Number +}; + +export const Difficulty = { + "uid": String, + "level": Number +}; + +export const Difficulties = { + "block": Number, + "levels": [Difficulty] +}; + +export const Blocks = [Block]; + +export const Stat = { + "result": { + "blocks": [Number] + } +}; + +export const Branches = { + "blocks": [Block] +}; + +export const Peer = { + "version": Number, + "currency": String, + "pubkey": String, + "block": String, + "endpoints": [String], + "signature": String, + "raw": String +}; + +export const DBPeer = { + "version": Number, + "currency": String, + "pubkey": String, + "block": String, + "status": String, + "first_down": Number, + "last_try": Number, + "endpoints": [String], + "signature": String, + "raw": String +}; + +export const Peers = { + "peers": [DBPeer] +}; + +export const MerkleOfPeers = { + "depth": Number, + "nodesCount": Number, + "leavesCount": Number, + "root": String, + "leaves": [String], + "leaf": { + "hash": String, + "value": DBPeer + } +}; + +export const Other = { + "pubkey": String, + "meta": { + "block_number": Number, + "block_hash": String + }, + "uids": [String], + "isMember": Boolean, + "wasMember": Boolean, + "signature": String +}; + +export const UID = { + "uid": String, + "meta": { + "timestamp": String + }, + "self": String, + "revocation_sig": String, + "revoked": Boolean, + "revoked_on": Number, + "others": [Other] +}; + +export const Signed = { + "uid": String, + "pubkey": String, + "meta": { + "timestamp": String + }, + "cert_time": { + "block": Number, + "block_hash": String + }, + "isMember": Boolean, + "wasMember": Boolean, + "signature": String +}; + +export const CertIdentity = { + "issuer": String, + "uid": String, + "timestamp": String, + "sig": String +}; + +export const Cert = { + "issuer": String, + "timestamp": String, + "sig": String, + "target": CertIdentity +}; + +export const Identity = { + "pubkey": String, + "uids": [UID], + "signed": [Signed] +}; + +export const Result = { + "result": Boolean +}; + +export const Lookup = { + "partial": Boolean, + "results": [Identity] +}; + +export const Members = { + "results": [{ + pubkey: String, + uid: String + }] +}; + +export const RequirementsCert = { + from: String, + to: String, + expiresIn: Number, + sig: String +}; + +export const RequirementsPendingCert = { + from: String, + to: String, + blockstamp: String, + sig: String +}; + +export const RequirementsPendingMembership = { + type: String, + blockstamp: String, + sig: String +}; + +export const Requirements = { + "identities": [{ + pubkey: String, + uid: String, + meta: { + timestamp: String + }, + sig: String, + revocation_sig: String, + revoked: Boolean, + revoked_on: Number, + expired: Boolean, + outdistanced: Boolean, + isSentry: Boolean, + wasMember: Boolean, + certifications: [RequirementsCert], + pendingCerts: [RequirementsPendingCert], + pendingMemberships: [RequirementsPendingMembership], + membershipPendingExpiresIn: Number, + membershipExpiresIn: Number + }] +}; + +export const Certification = { + "pubkey": String, + "uid": String, + "isMember": Boolean, + "wasMember": Boolean, + "cert_time": { + "block": Number, + "medianTime": Number + }, + "sigDate": String, + "written": { + "number": Number, + "hash": String + }, + "signature": String +}; + +export const Certifications = { + "pubkey": String, + "uid": String, + "sigDate": String, + "isMember": Boolean, + "certifications": [Certification] +}; + +export const SimpleIdentity = { + "pubkey": String, + "uid": String, + "sigDate": String +}; + +export const Transaction = { + "version": Number, + "currency": String, + "issuers": [String], + "inputs": [String], + "unlocks": [String], + "outputs": [String], + "comment": String, + "locktime": Number, + "signatures": [String], + "raw": String, + "written_block": Number, + "hash": String +}; + +export const Source = { + "type": String, + "noffset": Number, + "identifier": String, + "amount": Number, + "base": Number, + "conditions": String +}; + +export const Sources = { + "currency": String, + "pubkey": String, + "sources": [Source] +}; + +export const TxOfHistory = { + "version": Number, + "issuers": [String], + "inputs": [String], + "unlocks": [String], + "outputs": [String], + "comment": String, + "locktime": Number, + "received": Number, + "signatures": [String], + "hash": String, + "block_number": Number, + "time": Number, + "blockstamp": String, + "blockstampTime": Number +}; + +export const TxHistory = { + "currency": String, + "pubkey": String, + "history": { + "sent": [TxOfHistory], + "received": [TxOfHistory], + "sending": [TxOfHistory], + "receiving": [TxOfHistory], + "pending": [TxOfHistory] + } +}; + +export const TxPending = { + "currency": String, + "pending": [Transaction] +}; + +export const UD = { + "block_number": Number, + "consumed": Boolean, + "time": Number, + "amount": Number, + "base": Number +}; + +export const UDHistory = { + "currency": String, + "pubkey": String, + "history": { + "history": [UD] + } +}; + +export const BooleanDTO = { + "success": Boolean +}; + +export const SummaryConf = { + "cpu": Number +}; + +export const AdminSummary = { + "version": String, + "host": String, + "current": Block, + "rootBlock": Block, + "pubkey": String, + "seckey": String, + "conf": SummaryConf, + "parameters": Parameters, + "lastUDBlock": Block +}; + +export const PoWSummary = { + "total": Number, + "mirror": Boolean, + "waiting": Boolean +}; + +export const PreviewPubkey = { + "pubkey": String +}; + +export const Sandbox = { + size: Number, + free: Number +}; + +export const IdentitySandbox = Sandbox; +export const MembershipSandbox = Sandbox; +export const TransactionSandbox = Sandbox; + +export const Sandboxes = { + identities: IdentitySandbox, + memberships: MembershipSandbox, + transactions: TransactionSandbox +}; + +export const LogLink = { + link: String +}; diff --git a/app/modules/bma/lib/entity/source.ts b/app/modules/bma/lib/entity/source.ts new file mode 100644 index 0000000000000000000000000000000000000000..1bc134728948bf89cc51c245677cbdc5eb093176 --- /dev/null +++ b/app/modules/bma/lib/entity/source.ts @@ -0,0 +1,41 @@ +"use strict"; +const _ = require('underscore'); + +export class Source { + + [k:string]: any + + constructor(json:any) { + _(json || {}).keys().forEach((key:string) => { + let value = json[key]; + if (key == "number") { + value = parseInt(value); + } + else if (key == "consumed") { + value = !!value; + } + this[key] = value; + }) + } + + json() { + return { + "type": this.type, + "noffset": this.pos, + "identifier": this.identifier, + "amount": this.amount, + "conditions": this.conditions, + "base": this.base + }; + }; + + UDjson() { + return { + "block_number": this.number, + "consumed": this.consumed, + "time": this.time, + "amount": this.amount, + "base": this.base + }; + }; +} diff --git a/app/modules/bma/lib/http2raw.ts b/app/modules/bma/lib/http2raw.ts new file mode 100644 index 0000000000000000000000000000000000000000..0fe2d48c5645850eec2fa01365ec76b860a06512 --- /dev/null +++ b/app/modules/bma/lib/http2raw.ts @@ -0,0 +1,36 @@ +import {BMAConstants} from "./constants" + +module.exports = { + identity: requiresParameter('identity', BMAConstants.ERRORS.HTTP_PARAM_IDENTITY_REQUIRED), + certification: requiresParameter('cert', BMAConstants.ERRORS.HTTP_PARAM_CERT_REQUIRED), + revocation: requiresParameter('revocation', BMAConstants.ERRORS.HTTP_PARAM_REVOCATION_REQUIRED), + transaction: requiresParameter('transaction', BMAConstants.ERRORS.HTTP_PARAM_TX_REQUIRED), + peer: requiresParameter('peer', BMAConstants.ERRORS.HTTP_PARAM_PEER_REQUIRED), + membership: Http2RawMembership, + block: requiresParameter('block', BMAConstants.ERRORS.HTTP_PARAM_BLOCK_REQUIRED), + conf: requiresParameter('conf', BMAConstants.ERRORS.HTTP_PARAM_CONF_REQUIRED), + cpu: requiresParameter('cpu', BMAConstants.ERRORS.HTTP_PARAM_CPU_REQUIRED) +}; + +function requiresParameter(parameter:string, err:any) { + return (req:any) => { + if(!req.body || req.body[parameter] === undefined){ + throw err; + } + return req.body[parameter]; + }; +} + +function Http2RawMembership (req:any) { + if(!(req.body && req.body.membership)){ + throw BMAConstants.ERRORS.HTTP_PARAM_MEMBERSHIP_REQUIRED; + } + let ms = req.body.membership; + if(req.body && req.body.signature){ + ms = [ms, req.body.signature].join(''); + if (!ms.match(/\n$/)) { + ms += '\n'; + } + } + return ms; +} diff --git a/app/modules/bma/lib/limiter.ts b/app/modules/bma/lib/limiter.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a0c70f9abf0c41a0097ae3910e4b909205733ae --- /dev/null +++ b/app/modules/bma/lib/limiter.ts @@ -0,0 +1,129 @@ +"use strict"; + +const A_MINUTE = 60 * 1000; +const A_SECOND = 1000; + +export class Limiter { + + private limitPerSecond:number + private limitPerMinute:number + + // Stock of request times + private reqsSec:number[] = [] + + // The length of reqs. + // It is better to have it instead of calling reqs.length + private reqsSecLen:number + + // Minute specific + private reqsMin:number[] = [] + private reqsMinLen:number + + constructor(strategy: { limitPerSecond:number, limitPerMinute:number }) { + this.limitPerSecond = strategy.limitPerSecond + this.limitPerMinute = strategy.limitPerMinute + } + + /** + * 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 LOW_USAGE_STRATEGY = { + limitPerSecond: 1, + limitPerMinute: 30 +} + +let HIGH_USAGE_STRATEGY = { + limitPerSecond: 10, + limitPerMinute: 300 +} + +let VERY_HIGH_USAGE_STRATEGY = { + limitPerSecond: 30, + limitPerMinute: 30 * 60 // Limit is only per secon +} + +let TEST_STRATEGY = { + limitPerSecond: 5, + limitPerMinute: 6 +} + +let NO_LIMIT_STRATEGY = { + limitPerSecond: 1000000, + limitPerMinute: 1000000 * 60 +} + +let disableLimits = false; + +export const BMALimitation = { + + limitAsLowUsage() { + return disableLimits ? new Limiter(NO_LIMIT_STRATEGY) : new Limiter(LOW_USAGE_STRATEGY); + }, + + limitAsHighUsage() { + return disableLimits ? new Limiter(NO_LIMIT_STRATEGY) : new Limiter(HIGH_USAGE_STRATEGY); + }, + + limitAsVeryHighUsage() { + return disableLimits ? new Limiter(NO_LIMIT_STRATEGY) : new Limiter(VERY_HIGH_USAGE_STRATEGY); + }, + + limitAsUnlimited() { + return new Limiter(NO_LIMIT_STRATEGY); + }, + + limitAsTest() { + return disableLimits ? new Limiter(NO_LIMIT_STRATEGY) : new Limiter(TEST_STRATEGY); + }, + + noLimit() { + disableLimits = true; + }, + + withLimit() { + disableLimits = false; + } +}; diff --git a/app/modules/bma/lib/network.ts b/app/modules/bma/lib/network.ts new file mode 100644 index 0000000000000000000000000000000000000000..61e74ec38bdba9d8f32addcb403bd7d481591b4d --- /dev/null +++ b/app/modules/bma/lib/network.ts @@ -0,0 +1,381 @@ +"use strict"; +import {NetworkConfDTO} from "../../../lib/dto/ConfDTO" +import {Server} from "../../../../server" +import {BMAConstants} from "./constants" +import {BMALimitation} from "./limiter" + +const os = require('os'); +const Q = require('q'); +const _ = require('underscore'); +const ddos = require('ddos'); +const http = require('http'); +const express = require('express'); +const morgan = require('morgan'); +const errorhandler = require('errorhandler'); +const bodyParser = require('body-parser'); +const cors = require('cors'); +const fileUpload = require('express-fileupload'); +const sanitize = require('./sanitize'); + +export interface NetworkInterface { + ip:string|null + port:number|null +} + +export const Network = { + + getBestLocalIPv4: getBestLocalIPv4, + getBestLocalIPv6: getBestLocalIPv6, + + listInterfaces: listInterfaces, + + upnpConf: (noupnp:boolean, logger:any) => upnpConf(noupnp, logger), + + getRandomPort: getRandomPort, + + createServersAndListen: async (name:string, server:Server, interfaces:NetworkInterface[], httpLogs:boolean, logger:any, staticPath:string|null, routingCallback:any, listenWebSocket:any, enableFileUpload:boolean = false) => { + + const app = express(); + + // all environments + if (httpLogs) { + app.use(morgan('\x1b[90m:remote-addr - :method :url HTTP/:http-version :status :res[content-length] - :response-time ms\x1b[0m', { + stream: { + write: function(message:string){ + message && logger && logger.trace(message.replace(/\n$/,'')); + } + } + })); + } + + // DDOS protection + const whitelist = interfaces.map(i => i.ip); + if (whitelist.indexOf('127.0.0.1') === -1) { + whitelist.push('127.0.0.1'); + } + const ddosConf = server.conf.dos || { silentStart: true }; + ddosConf.whitelist = _.uniq((ddosConf.whitelist || []).concat(whitelist)); + const ddosInstance = new ddos(ddosConf); + app.use(ddosInstance.express); + + // CORS for **any** HTTP request + app.use(cors()); + + if (enableFileUpload) { + // File upload for backup API + app.use(fileUpload()); + } + + app.use(bodyParser.urlencoded({ + extended: true + })); + app.use(bodyParser.json({ limit: '10mb' })); + + // development only + if (app.get('env') == 'development') { + app.use(errorhandler()); + } + + const handleRequest = (method:any, uri:string, promiseFunc:(...args:any[])=>Promise<any>, dtoContract:any, theLimiter:any) => { + const limiter = theLimiter || BMALimitation.limitAsUnlimited(); + method(uri, async function(req:any, res:any) { + res.set('Access-Control-Allow-Origin', '*'); + res.type('application/json'); + try { + if (!limiter.canAnswerNow()) { + throw BMAConstants.ERRORS.HTTP_LIMITATION; + } + limiter.processRequest(); + let result = await promiseFunc(req); + // Ensure of the answer format + result = sanitize(result, dtoContract); + // HTTP answer + res.status(200).send(JSON.stringify(result, null, " ")); + } catch (e) { + let error = getResultingError(e, logger); + // HTTP error + res.status(error.httpCode).send(JSON.stringify(error.uerr, null, " ")); + } + }); + }; + + const handleFileRequest = (method:any, uri:string, promiseFunc:(...args:any[])=>Promise<any>, theLimiter:any) => { + const limiter = theLimiter || BMALimitation.limitAsUnlimited(); + method(uri, async function(req:any, res:any) { + res.set('Access-Control-Allow-Origin', '*'); + try { + if (!limiter.canAnswerNow()) { + throw BMAConstants.ERRORS.HTTP_LIMITATION; + } + limiter.processRequest(); + let fileStream:any = await promiseFunc(req); + // HTTP answer + fileStream.pipe(res); + } catch (e) { + let error = getResultingError(e, logger); + // HTTP error + res.status(error.httpCode).send(JSON.stringify(error.uerr, null, " ")); + throw e + } + }); + }; + + routingCallback(app, { + httpGET: (uri:string, promiseFunc:(...args:any[])=>Promise<any>, dtoContract:any, limiter:any) => handleRequest(app.get.bind(app), uri, promiseFunc, dtoContract, limiter), + httpPOST: (uri:string, promiseFunc:(...args:any[])=>Promise<any>, dtoContract:any, limiter:any) => handleRequest(app.post.bind(app), uri, promiseFunc, dtoContract, limiter), + httpGETFile: (uri:string, promiseFunc:(...args:any[])=>Promise<any>, dtoContract:any, limiter:any) => handleFileRequest(app.get.bind(app), uri, promiseFunc, limiter) + }); + + if (staticPath) { + app.use(express.static(staticPath)); + } + + const httpServers = interfaces.map(() => { + const httpServer = http.createServer(app); + const sockets:any = {}; + let nextSocketId = 0; + httpServer.on('connection', (socket:any) => { + const socketId = nextSocketId++; + sockets[socketId] = socket; + //logger && logger.debug('socket %s opened', socketId); + + socket.on('close', () => { + //logger && logger.debug('socket %s closed', socketId); + delete sockets[socketId]; + }); + }); + httpServer.on('error', (err:any) => { + httpServer.errorPropagates(err); + }); + listenWebSocket && listenWebSocket(httpServer); + return { + http: httpServer, + closeSockets: () => { + _.keys(sockets).map((socketId:number) => { + sockets[socketId].destroy(); + }); + } + }; + }); + + if (httpServers.length == 0){ + throw 'Duniter does not have any interface to listen to.'; + } + + // Return API + return new BmaApi(interfaces, ddosInstance, httpServers, logger) + } +} + +export class BmaApi { + + private listenings:boolean[] + + constructor( + private interfaces:any, + private ddosInstance:any, + private httpServers:any, + private logger:any + ) { + + // May be removed when using Node 5.x where httpServer.listening boolean exists + this.listenings = interfaces.map(() => false) + } + + getDDOS() { + return this.ddosInstance + } + + async closeConnections() { + for (let i = 0, len = this.httpServers.length; i < len; i++) { + const httpServer = this.httpServers[i].http; + const isListening = this.listenings[i]; + if (isListening) { + this.listenings[i] = false; + this.logger && this.logger.info(name + ' stop listening'); + await Q.Promise((resolve:any, reject:any) => { + httpServer.errorPropagates((err:any) => { + reject(err); + }); + this.httpServers[i].closeSockets(); + httpServer.close((err:any) => { + err && this.logger && this.logger.error(err.stack || err); + resolve(); + }); + }); + } + } + return []; + } + + async openConnections() { + for (let i = 0, len = this.httpServers.length; i < len; i++) { + const httpServer = this.httpServers[i].http; + const isListening = this.listenings[i]; + if (!isListening) { + const netInterface = this.interfaces[i].ip; + const port = this.interfaces[i].port; + try { + await Q.Promise((resolve:any, reject:any) => { + // Weird the need of such a hack to catch an exception... + httpServer.errorPropagates = function(err:any) { + reject(err); + }; + //httpServer.on('listening', resolve.bind(this, httpServer)); + httpServer.listen(port, netInterface, (err:any) => { + if (err) return reject(err); + this.listenings[i] = true; + resolve(httpServer); + }); + }); + this.logger && this.logger.info(name + ' listening on http://' + (netInterface.match(/:/) ? '[' + netInterface + ']' : netInterface) + ':' + port); + } catch (e) { + this.logger && this.logger.warn('Could NOT listen to http://' + netInterface + ':' + port); + this.logger && this.logger.warn(e); + } + } + } + return []; + } +} + +function getResultingError(e:any, logger:any) { + // Default is 500 unknown error + let error = BMAConstants.ERRORS.UNKNOWN; + if (e) { + // Print eventual stack trace + typeof e == 'string' && logger && logger.error(e); + e.stack && logger && logger.error(e.stack); + e.message && logger && logger.warn(e.message); + // BusinessException + if (e.uerr) { + error = e; + } else { + const cp = BMAConstants.ERRORS.UNHANDLED; + error = { + httpCode: cp.httpCode, + uerr: { + ucode: cp.uerr.ucode, + message: e.message || e || error.uerr.message + } + }; + } + } + return error; +} + +function getBestLocalIPv4() { + return getBestLocal('IPv4'); +} + +function getBestLocalIPv6() { + const osInterfaces = listInterfaces(); + for (let netInterface of osInterfaces) { + const addresses = netInterface.addresses; + const filtered = _(addresses).where({family: 'IPv6', scopeid: 0, internal: false }); + const filtered2 = _.filter(filtered, (address:any) => !address.address.match(/^fe80/) && !address.address.match(/^::1/)); + if (filtered2[0]) { + return filtered2[0].address; + } + } + return null; +} + +function getBestLocal(family:string) { + let netInterfaces = os.networkInterfaces(); + let keys = _.keys(netInterfaces); + let res = []; + for (const name of keys) { + let addresses = netInterfaces[name]; + for (const addr of addresses) { + if (!family || addr.family == family) { + res.push({ + name: name, + value: addr.address + }); + } + } + } + const interfacePriorityRegCatcher = [ + /^tun\d/, + /^enp\ds\d/, + /^enp\ds\df\d/, + /^eth\d/, + /^Ethernet/, + /^wlp\ds\d/, + /^wlan\d/, + /^Wi-Fi/, + /^lo/, + /^Loopback/, + /^None/ + ]; + const best = _.sortBy(res, function(entry:any) { + for (let i = 0; i < interfacePriorityRegCatcher.length; i++) { + // `i` is the priority (0 is the better, 1 is the second, ...) + if (entry.name.match(interfacePriorityRegCatcher[i])) return i; + } + return interfacePriorityRegCatcher.length; + })[0]; + return (best && best.value) || ""; +} + +function listInterfaces() { + const netInterfaces = os.networkInterfaces(); + const keys = _.keys(netInterfaces); + const res = []; + for (const name of keys) { + res.push({ + name: name, + addresses: netInterfaces[name] + }); + } + return res; +} + +async function upnpConf (noupnp:boolean, logger:any) { + const conf:NetworkConfDTO = { + port: 10901, + ipv4: '127.0.0.1', + ipv6: '::1', + dos: null, + upnp: false, + httplogs: false, + remoteport: 10901, + remotehost: null, + remoteipv4: null, + remoteipv6: null + } + const client = require('nnupnp').createClient(); + // Look for 2 random ports + const privatePort = getRandomPort(conf); + const publicPort = privatePort; + logger && logger.info('Checking UPnP features...'); + if (noupnp) { + throw Error('No UPnP'); + } + const publicIP = await Q.nbind(client.externalIp, client)(); + await Q.nbind(client.portMapping, client)({ + public: publicPort, + private: privatePort, + ttl: 120 + }); + const privateIP = await Q.Promise((resolve:any, reject:any) => { + client.findGateway((err:any, res:any, localIP:any) => { + if (err) return reject(err); + resolve(localIP); + }); + }); + conf.remoteipv4 = publicIP.match(BMAConstants.IPV4_REGEXP) ? publicIP : null; + conf.remoteport = publicPort; + conf.port = privatePort; + conf.ipv4 = privateIP.match(BMAConstants.IPV4_REGEXP) ? privateIP : null; + return conf; +} + +function getRandomPort(conf:NetworkConfDTO) { + if (conf && conf.remoteport) { + return conf.remoteport; + } else { + return ~~(Math.random() * (65536 - BMAConstants.PORT_START)) + BMAConstants.PORT_START; + } +} diff --git a/app/modules/bma/lib/parameters.ts b/app/modules/bma/lib/parameters.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a27c9db386b50058aada7eaeb1dbeb1ff955f48 --- /dev/null +++ b/app/modules/bma/lib/parameters.ts @@ -0,0 +1,130 @@ +"use strict"; +import {BMAConstants} from "./constants" + +const Q = require('q'); + +export class ParametersService { + + static getSearch(req:any, callback:any) { + if(!req.params || !req.params.search){ + callback("No search criteria given"); + return; + } + callback(null, req.params.search); + } + + static getSearchP(req:any) { + return Q.nbind(ParametersService.getSearch, this)(req) + } + + static getCountAndFrom(req:any) { + if(!req.params.from){ + throw "From is required"; + } + if(!req.params.count){ + throw "Count is required"; + } + const matches = req.params.from.match(/^(\d+)$/); + if(!matches){ + throw "From format is incorrect, must be a positive integer"; + } + const matches2 = req.params.count.match(/^(\d+)$/); + if(!matches2){ + throw "Count format is incorrect, must be a positive integer"; + } + return { + count: matches2[1], + from: matches[1] + }; + } + + static getHash(req:any) { + if(!req.params.hash){ + throw Error("`hash` is required"); + } + const matches = req.params.hash.match(BMAConstants.SHA256_HASH); + if(!matches){ + throw Error("`hash` format is incorrect, must be a SHA256 hash"); + } + return req.params.hash; + }; + + static getMinSig(req:any){ + if(!req.params.minsig){ + return 4 // Default value + } + const matches = req.params.minsig.match(/\d+/) + if(!matches){ + throw Error("`minsig` format is incorrect, must be an integer") + } + return parseInt(req.params.minsig) + } + + static getPubkey = function (req:any, callback:any){ + if(!req.params.pubkey){ + callback('Parameter `pubkey` is required'); + return; + } + const matches = req.params.pubkey.match(BMAConstants.PUBLIC_KEY); + if(!matches){ + callback("Pubkey format is incorrect, must be a Base58 string"); + return; + } + callback(null, matches[0]); + } + + static getPubkeyP(req:any) { + return Q.nbind(ParametersService.getPubkey, this)(req) + } + + static getFrom(req:any, callback:any){ + if(!req.params.from){ + callback('Parameter `from` is required'); + return; + } + const matches = req.params.from.match(/^(\d+)$/); + if(!matches){ + callback("From format is incorrect, must be a positive or zero integer"); + return; + } + callback(null, matches[0]); + } + + static getFromP(req:any) { + return Q.nbind(ParametersService.getFrom, this)(req) + } + + static getTo(req:any, callback:any){ + if(!req.params.to){ + callback('Parameter `to` is required'); + return; + } + const matches = req.params.to.match(/^(\d+)$/); + if(!matches){ + callback("To format is incorrect, must be a positive or zero integer"); + return; + } + callback(null, matches[0]); + } + + static getToP(req:any) { + return Q.nbind(ParametersService.getTo, this)(req) + } + + static getNumber(req:any, callback:any){ + if(!req.params.number){ + callback("Number is required"); + return; + } + const matches = req.params.number.match(/^(\d+)$/); + if(!matches){ + callback("Number format is incorrect, must be a positive integer"); + return; + } + callback(null, parseInt(matches[1])); + } + + static getNumberP(req:any) { + return Q.nbind(ParametersService.getNumber, this)(req) + } +} diff --git a/app/modules/bma/lib/sanitize.ts b/app/modules/bma/lib/sanitize.ts new file mode 100644 index 0000000000000000000000000000000000000000..01d6cec2c3764c6e1e58d68af1ec2bf4466e9481 --- /dev/null +++ b/app/modules/bma/lib/sanitize.ts @@ -0,0 +1,118 @@ +"use strict"; + +let _ = require('underscore'); + +module.exports = function sanitize (json:any, contract:any) { + + // Tries to sanitize only if contract is given + if (contract) { + + if (Object.prototype.toString.call(contract) === "[object Array]") { + // Contract is an array + + if (Object.prototype.toString.call(json) !== "[object Array]") { + json = []; + } + + for (let i = 0, len = json.length; i < len; i++) { + json[i] = sanitize(json[i], contract[0]); + } + } else { + // Contract is an object or native type + + // Return type is either a string, a number or an object + if (typeof json != typeof contract) { + try { + // Cast value + json = contract(json); + } catch (e) { + // Cannot be casted: create empty value + json = contract(); + } + } + + let contractFields = _(contract).keys(); + let objectFields = _(json).keys(); + let toDeleteFromObj = _.difference(objectFields, contractFields); + + // Remove unwanted fields + for (let i = 0, len = toDeleteFromObj.length; i < len; i++) { + let field = toDeleteFromObj[i]; + delete json[field]; + } + + // Format wanted fields + for (let i = 0, len = contractFields.length; i < len; i++) { + let prop = contractFields[i]; + let propType = contract[prop]; + let t = ""; + if (propType.name) { + t = propType.name; + } else if (propType.length != undefined) { + t = 'Array'; + } else { + t = 'Object'; + } + // Test json member type + let tjson:any = typeof json[prop]; + if (~['Array', 'Object'].indexOf(t)) { + if (tjson == 'object' && json[prop] !== null) { + tjson = json[prop].length == undefined ? 'Object' : 'Array'; + } + } + // Check coherence & alter member if needed + if (!_(json[prop]).isNull() && t.toLowerCase() != tjson.toLowerCase()) { + try { + if (t == "String") { + let s = json[prop] == undefined ? '' : json[prop]; + json[prop] = String(s).valueOf(); + } + else if (t == "Number") { + let s = json[prop] == undefined ? '' : json[prop]; + json[prop] = Number(s).valueOf(); + } + else if (t == "Array") { + json[prop] = []; + } + else if (t == "Object") { + json[prop] = {}; + } + else { + json[prop] = Boolean(); + } + } catch (ex) { + if (t == "String") { + json[prop] = String(); + } + else if (t == "Number") { + json[prop] = Number(); + } + else if (t == "Array") { + json[prop] = []; + } + else if (t == "Object") { + json[prop] = {}; + } + else { + json[prop] = Boolean(); + } + } + } + // Arrays + if (t == 'Array') { + let subt = propType[0]; + for (let j = 0, len2 = json[prop].length; j < len2; j++) { + if (!(subt == "String" || subt == "Number")) { + json[prop][j] = sanitize(json[prop][j], subt); + } + } + } + // Recursivity + if (t == 'Object' && json[prop] !== null) { + json[prop] = sanitize(json[prop], contract[prop]); + } + } + } + } + return json; +}; diff --git a/app/modules/bma/lib/tojson.ts b/app/modules/bma/lib/tojson.ts new file mode 100644 index 0000000000000000000000000000000000000000..4d7e4268b0d40bba85099e17682473e158b5167f --- /dev/null +++ b/app/modules/bma/lib/tojson.ts @@ -0,0 +1,52 @@ +"use strict"; +import {BlockDTO} from "../../../lib/dto/BlockDTO" + +const _ = require('underscore') + +export const stat = (stat:any) => { + return { "blocks": stat.blocks } +} + +export const block = (block:any) => { + const json:any = {}; + json.version = parseInt(block.version) + json.nonce = parseInt(block.nonce) + json.number = parseInt(block.number) + json.powMin = parseInt(block.powMin) + json.time = parseInt(block.time) + json.medianTime = parseInt(block.medianTime) + json.membersCount = parseInt(block.membersCount) + json.monetaryMass = parseInt(block.monetaryMass) + json.unitbase = parseInt(block.unitbase) + json.issuersCount = parseInt(block.issuersCount) + json.issuersFrame = parseInt(block.issuersFrame) + json.issuersFrameVar = parseInt(block.issuersFrameVar) + json.len = parseInt(block.len) + json.currency = block.currency || "" + json.issuer = block.issuer || "" + json.signature = block.signature || "" + json.hash = block.hash || "" + json.parameters = block.parameters || "" + json.previousHash = block.previousHash || null + json.previousIssuer = block.previousIssuer || null + json.inner_hash = block.inner_hash || null + json.dividend = parseInt(block.dividend) || null + json.identities = (block.identities ||Â []) + json.joiners = (block.joiners ||Â []) + json.actives = (block.actives ||Â []) + json.leavers = (block.leavers ||Â []) + json.revoked = (block.revoked ||Â []) + json.excluded = (block.excluded ||Â []) + json.certifications = (block.certifications ||Â []) + json.transactions = []; + block.transactions.forEach((obj:any) => { + json.transactions.push(_(obj).omit('raw', 'certifiers', 'hash')); + }); + json.transactions = block.transactions.map((tx:any) => { + tx.inputs = tx.inputs.map((i:any) => i.raw || i) + tx.outputs = tx.outputs.map((o:any) => o.raw || o) + return tx + }) + json.raw = BlockDTO.fromJSONObject(block).getRawUnSigned() + return json; +} \ No newline at end of file diff --git a/app/modules/bma/lib/upnp.ts b/app/modules/bma/lib/upnp.ts new file mode 100644 index 0000000000000000000000000000000000000000..45b451ff05a81698a1d5052e551103e5db1ef043 --- /dev/null +++ b/app/modules/bma/lib/upnp.ts @@ -0,0 +1,86 @@ +import {BMAConstants} from "./constants" +const upnp = require('nnupnp'); +const Q = require('q'); + +export const Upnp = async function (localPort:number, remotePort:number, logger:any) { + "use strict"; + + logger.info('UPnP: configuring...'); + const api = new UpnpApi(localPort, remotePort, logger) + try { + await api.openPort() + } catch (e) { + const client = upnp.createClient(); + try { + await Q.nbind(client.externalIp, client)(); + } catch (err) { + if (err && err.message == 'timeout') { + throw 'No UPnP gateway found: your node won\'t be reachable from the Internet. Use --noupnp option to avoid this message.' + } + throw err; + } finally { + client.close(); + } + } + return api +}; + +export class UpnpApi { + + private interval:NodeJS.Timer|null + + constructor( + private localPort:number, + private remotePort:number, + private logger:any + )Â {} + + openPort() { + "use strict"; + return Q.Promise((resolve:any, reject:any) => { + this.logger.trace('UPnP: mapping external port %s to local %s...', this.remotePort, this.localPort); + const client = upnp.createClient(); + client.portMapping({ + 'public': this.remotePort, + 'private': this.localPort, + 'ttl': BMAConstants.UPNP_TTL + }, (err:any) => { + client.close(); + if (err) { + this.logger.warn(err); + return reject(err); + } + resolve(); + }); + }); + } + + async findGateway() { + try { + const client = upnp.createClient(); + const res = await Q.nbind(client.findGateway, client)(); + const desc = res && res[0] && res[0].description; + if (desc) { + const match = desc.match(/(\d+.\d+.\d+.\d+):/); + if (match) { + return match[1]; + } + } + return null; + } catch (e) { + return null; + } + } + + startRegular() { + this.stopRegular(); + // Update UPnP IGD every INTERVAL seconds + this.interval = setInterval(() => this.openPort(), 1000 * BMAConstants.UPNP_INTERVAL) + } + + stopRegular() { + if (this.interval) { + clearInterval(this.interval) + } + } +} \ No newline at end of file diff --git a/app/service/PeeringService.ts b/app/service/PeeringService.ts index 32d5851f02f0f6d6b8b9aa7daae4929702891368..9355de98445f82b4370f4772111061bbce7fe40e 100644 --- a/app/service/PeeringService.ts +++ b/app/service/PeeringService.ts @@ -261,7 +261,7 @@ export class PeeringService { return endpoints.filter((ep) => { return !ep.match(constants.BMA_REGEXP) || ( !(ep.includes(' ' + theConf.remoteport) && ( - ep.includes(theConf.remotehost) || ep.includes(theConf.remoteipv6) || ep.includes(theConf.remoteipv4)))); + ep.includes(theConf.remotehost || '') || ep.includes(theConf.remoteipv6 || '') || ep.includes(theConf.remoteipv4 || '')))); }); } } diff --git a/doc/API.md b/doc/API.md new file mode 100644 index 0000000000000000000000000000000000000000..8a4b818bd0196282ca13c4c5957ba03401db7297 --- /dev/null +++ b/doc/API.md @@ -0,0 +1,1845 @@ +# Duniter HTTP API + +## Contents + +* [Contents](#contents) +* [Overview](#overview) +* [Merkle URLs](#merkle-urls) +* [API](#api) + * [node/](#node) + * [summary](#nodesummary) + * [sandboxes](#sandboxes) + * [wot/](#wot) + * [add](#wotadd) + * [certify](#wotcertify) + * [revoke](#wotrevoke) + * [lookup/[search]](#wotlookupsearch) + * [requirements/[PUBKEY]](#wotrequirementspubkey) + * [certifiers-of/[search]](#wotcertifiers-ofsearch) + * [certified-by/[search]](#wotcertified-bysearch) + * [blockchain/](#blockchain) + * [parameters](#blockchainparameters) + * [membership](#blockchainmembership) + * [memberships/[search]](#blockchainmembershipssearch) + * [block](#blockchainblock) + * [block/[number]](#blockchainblocknumber) + * [blocks/[count]/[from]](#blockchainblockscountfrom) + * [current](#blockchaincurrent) + * [hardship/[PUBKEY]](#blockchainhardshippubkey) + * [difficulties](#blockchaindifficulties) + * [with/](#blockchainwith) + * [newcomers](#blockchainwithnewcomers) + * [certs](#blockchainwithcerts) + * [actives](#blockchainwithactives) + * [leavers](#blockchainwithleavers) + * [excluded](#blockchainwithexcluded) + * [ud](#blockchainwithud) + * [tx](#blockchainwithtx) + * [branches](#blockchainbranches) + * [network/](#network) + * [peers](#networkpeers) + * [peering](#networkpeering) + * [peering/peers (GET)](#networkpeeringpeers-get) + * [peering/peers (POST)](#networkpeeringpeers-post) + * [tx/](#tx) + * [process](#txprocess) + * [sources/[pubkey]](#txsourcespubkey) + * [history/[pubkey]](#txhistorypubkey) + * [history/[pubkey]/blocks/[from]/[to]](#txhistorypubkeyblocksfromto) + * [history/[pubkey]/times/[from]/[to]](#txhistorypubkeytimesfromto) + * [ud/](#ud) + * [history/[pubkey]](#udhistorypubkey) + * [ws/](#ws) + * [block](#wsblock) + * [peer](#wspeer) + +## Overview + +Data is made accessible through an HTTP API mainly inspired from [OpenUDC_exchange_formats draft](https://github.com/Open-UDC/open-udc/blob/master/docs/OpenUDC_exchange_formats.draft.txt), and has been adapted to fit Duniter specificities. + + http[s]://Node[:port]/... + |-- node/ + | |-- summary + | |-- sandboxes + |-- wot/ + | |-- add + | |-- certify + | |-- revoke + | |-- requirements/[pubkey] + | |-- certifiers-of/[uid|pubkey] + | |-- certified-by/[uid|pubkey] + | |-- members + | `-- lookup + |-- blockchain/ + | |-- parameters + | |-- membership + | |-- with/ + | |-- newcomers + | |-- certs + | |-- joiners + | |-- actives + | |-- leavers + | |-- excluded + | |-- ud + | `-- tx + | |-- hardship + | | `-- [PUBKEY] + | |-- block + | | `-- [NUMBER] + | |-- difficulties + | `-- current + |-- network/ + | |-- peers + | `-- peering + | `-- peers + |-- tx/ + | |-- process + | |-- sources + | `-- history + |-- ud/ + | `-- history + `-- ws/ + |-- block + `-- peer + +## Merkle URLs + +Merkle URL is a special kind of URL applicable for resources: + +* `network/peering/peers (GET)` + +Such kind of URL returns Merkle tree hashes informations. In Duniter, Merkle trees are an easy way to detect unsynced data and where the differences come from. For example, `network/peering/peers` is a Merkle tree whose leaves are peers' key fingerprint sorted ascending way. Thus, if any new peer is added, a branch of the tree will see its hash modified and propagated to the root hash. Change is then easy to detect. + +For commodity issues, this URL uses query parameters to retrieve partial data of the tree, as most of the time all the data is not required. Duniter Merkle tree has a determined number of parent nodes (given a number of leaves), which allows to ask only for interval of them. + +Here is an example of members Merkle tree with 5 members (taken from [Tree Hash EXchange format (THEX)](http://web.archive.org/web/20080316033726/http://www.open-content.net/specs/draft-jchapweske-thex-02.html)): + + ROOT=H(H+E) + / \ + / \ + H=H(F+G) E + / \ \ + / \ \ + F=H(A+B) G=H(C+D) E + / \ / \ \ + / \ / \ \ + A B C D E + + + Note: H() is some hash function + +Where A,B,C,D,E are already hashed data. + +With such a tree structure, Duniter consider the tree has exactly 6 nodes: `[ROOT,H,E,F,G,E]`. Nodes are just an array, and for a Lambda Server LS1, it is easy to ask for the values of another server LS2 for level 1 (`H` and `E`, the second level): it requires nodes interval `[1;2]`. + +Hence it is quite easy for anyone who wants to check if a `Z` member joined the Duniter community as it would alter the `E` branch of the tree: + + ROOT'=H(H+E') + / \ + / \ + H=H(F+G) E' + / \ \ + / \ \ + F=H(A+B) G=H(C+D) E'=H(E+Z) + / \ / \ / \ + / \ / \ / \ + A B C D E Z + +`ROOT` changed to `ROOT'`, `E` to `E'`, but `H` did not. The whole `E'` branch should be updated with the proper new data. + +For that purpose, Merkle URL defines different parameters and results: + +**Parameters** + +Parameter | Description +--------- | ----------- +`leaves` | Defines wether or not leaves hashes should be returned too. Defaults to `false`. +`leaf` | Hash of a leaf whose content should be returned. Ignore `leaves` parameter. + +**Returns** + +Merkle URL result with `leaves=false`. +```json +{ + "depth": 3, + "nodesCount": 6, + "leavesCount": 5, + "root": "6513D6A1582DAE614D8A3B364BF3C64C513D236B" +} +``` + +Merkle URL result with `leaves=true`. +```json +{ + "depth": 3, + "nodesCount": 6, + "leavesCount": 5, + "root": "6513D6A1582DAE614D8A3B364BF3C64C513D236B", + "leaves": [ + "32096C2E0EFF33D844EE6D675407ACE18289357D", + "50C9E8D5FC98727B4BBC93CF5D64A68DB647F04F", + "6DCD4CE23D88E2EE9568BA546C007C63D9131C1B", + "AE4F281DF5A5D0FF3CAD6371F76D5C29B6D953EC", + "E0184ADEDF913B076626646D3F52C3B49C39AD6D" + ] +} +``` + +Merkle URL result with `leaf=AE4F281DF5A5D0FF3CAD6371F76D5C29B6D953EC`. +```json +{ + "depth": 3, + "nodesCount": 6, + "leavesCount": 5, + "root": "6513D6A1582DAE614D8A3B364BF3C64C513D236B", + "leaf": { + "hash": "AE4F281DF5A5D0FF3CAD6371F76D5C29B6D953EC", + "value": // JSON value (object, string, int, ...) + } +} +``` + +### Duniter Merkle trees leaves + +Each tree manages different data, and has a different goal. Hence, each tree has its own rules on how are generated and sorted tree leaves. +Here is a summup of such rules: + + +Merkle URL | Leaf | Sort +---------------------- | --------------------------| ------------- +`network/peers (GET)` | Hash of the peers' pubkey | By hash string sort, ascending. + +#### Unicity + +It has to be noted that **possible conflict exists** for leaves, as every leaf is hash, but is rather unlikely. + +## API + +### node/* + +#### `node/summary` +**Goal** + +GET technical informations about this peer. + +**Parameters** + +*None*. + +**Returns** + +Technical informations about the node. +```json +{ + "duniter": { + "software": "duniter", + "version": "0.10.3", + "forkWindowSize": 10 + } +} +``` + +#### `node/sandboxes` +**Goal** + +GET filling and capacity of indentities, membership and transactions sandboxes of the requested peer. + +**Parameters** + +*None*. + +**Returns** + +Technical informations about identities, membership and transactions sandboxes. +```json +{ + "identities": { + "size": 5000, + "free": 4626 + }, + "memberships": { + "size": 5000, + "free": 4750 + }, + "transactions": { + "size": 200, + "free": 190 + } +} +``` + +### wot/* + +#### `wot/add` + + +**Goal** + +POST [Identity](./Protocol.md#identity) data. + +**Parameters** + +Name | Value | Method +---- | ----- | ------ +`identity` | The raw identity. | POST + +**Returns** + +The available validated data for this public key. +```json +{ + "pubkey": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "uids": [ + { + "uid": "udid2;c;TOCQUEVILLE;FRANCOIS-XAVIER-ROBE;1989-07-14;e+48.84+002.30;0;", + "meta": { + "timestamp": "44-76522E321B3380B058DB6D9E66121705EEA63610869A7C5B3E701CF6AF2D55A8" + }, + "self": "J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUbGpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBfCznzyci", + "others": [ + { + "pubkey": "9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB", + "meta": { + "timestamp": "22-2E910FCCCEE008C4B978040CA68211C2395C84C3E6BFB432A267384ED8CD22E5" + }, + "signature": "42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r" + } + ] + } + ] +} +``` + +#### `wot/certify` + + +**Goal** + +POST [Certification](./Protocol.md#certification) data. + +**Parameters** + +Name | Value | Method +---- | ----- | ------ +`cert` | The raw certification. | POST + +**Returns** + +The available validated data for this public key. +```json +{ + "issuer": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "timestamp": "44-76522E321B3380B058DB6D9E66121705EEA63610869A7C5B3E701CF6AF2D55A8", + "sig": "42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r", + "target": { + "issuer": "CqwuWfMsPqtUkWdUK6FxV6hPWeHaUfEcz7dFZZJA49BS", + "uid": "johnsnow", + "timestamp": "44-76522E321B3380B058DB6D9E66121705EEA63610869A7C5B3E701CF6AF2D55A8", + "sig": "42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r", + } +} +``` + +#### `wot/revoke` + + +**Goal** + +Remove an identity from Identity pool. + +> N.B.: An identity **written in the blockchain cannot be removed**. + +**Parameters** + +Name | Value | Method +---- | ----- | ------ +`revocation` | The raw revocation. | POST + +**Returns** + +True if operation went well. An HTTP error otherwise with body as error message. +```json +{ + "result": true +} +``` + +#### `wot/lookup/[search]` + + +**Goal** + +GET [Public key](./Protocol.md#publickey) data. + +**Parameters** + +Name | Value | Method +---- | ----- | ------ +`search` | A string of data to look for (public key, uid). | URL + +**Returns** + +A list of public key data matching search string (may not return all results, check `partial` flag which is set to `false` if all results are here, ` true` otherwise). +```json +{ + "partial": false, + "results": [ + { + "pubkey": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "uids": [ + { + "uid": "udid2;c;TOCQUEVILLE;FRANCOIS-XAVIER-ROBE;1989-07-14;e+48.84+002.30;0;", + "meta": { + "timestamp": "56-97A56CCE04A1B7A03264ADE09545B262CBE65E62DDA481B7D7C89EB4669F5435" + }, + "self": "J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUbGpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBfCznzyci", + "revocation_sig": "CTmlh3tO4B8f8IbL8iDy5ZEr3jZDcxkPmDmRPQY74C39MRLXi0CKUP+oFzTZPYmyUC7fZrUXrb3LwRKWw1jEBQ==", + "revoked": false, + "others": [ + { + "pubkey": "9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB", + "meta": { + "timestamp": "32-DB30D958EE5CB75186972286ED3F4686B8A1C2CD" + }, + "signature": "42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r" + } + ] + } + ], + "signed": [ + { + "uid": "snow", + "pubkey": "2P7y2UDiCcvsgSSt8sgHF3BPKS4m9waqKw4yXHCuP6CN", + "meta": { + "timestamp": "33-AB30D958EE5CB75186972286ED3F4686B8A1C2CD" + }, + "revocation_sig": "CK6UDDJM3d0weE1RVtzFJnw/+J507lPAtspleHc59T4+N1tzQj1RRGWrzPiTknCjnCO6SxBSJX0B+MIUWrpNAw==", + "revoked": false, + "signature": "Xbr7qhyGNCmLoVuuKnKIbrdmtCvb9VBIEY19izUNwA5nufsjNm8iEsBTwKWOo0lq5O1+AAPMnht8cm2JjMq8AQ==" + }, + { + "uid": "snow", + "pubkey": "2P7y2UDiCcvsgSSt8sgHF3BPKS4m9waqKw4yXHCuP6CN", + "meta": { + "timestamp": "978-B38F54242807DFA1A12F17E012D355D8DB92CA6E947FC5D147F131B30C639163" + }, + "revocation_sig": "a7SFapoVaXq27NU+wZj4afmxp0SbwLGqLJih8pfX6TRKPvNp/V93fbKixbqg10cwa1CadNenztxq3ZgOivqADw==", + "revoked": false, + "signature": "HU9VPwC4EqPJwATPuyUJM7HLjfig5Ke1CKonL9Q78n5/uNSL2hkgE9Pxsor8CCJfkwCxh66NjGyqnGYqZnQMAg==" + }, + { + "uid": "snow", + "pubkey": "7xapQvvxQ6367bs8DsskEf3nvQAgJv97Yu11aPbkCLQj", + "meta": { + "timestamp": "76-0DC977717C49E69A78A67C6A1526EC17ED380BC68F0C38D290A954471A1349B7" + }, + "revocation_sig": "h8D/dx/z5K2dx06ktp7fnmLRdxkdV5wRkJgnmEvKy2k55mM2RyREpHfD7t/1CC5Ew+UD0V9N27PfaoLxZc1KCQ==", + "revoked": true, + "signature": "6S3x3NwiHB2QqYEY79x4wCUYHcDctbazfxIyxejs38V1uRAl4DuC8R3HJUfD6wMSiWKPqbO+td+8ZMuIn0L8AA==" + }, + { + "uid": "cat", + "pubkey": "CK2hBp25sYQhdBf9oFMGHyokkmYFfzSCmwio27jYWAN7", + "meta": { + "timestamp": "63-999677597FC04E6148860AE888A2E1942DF0E1E732C31500BA8EFF07F06FEC0C" + }, + "revocation_sig": "bJyoM2Tz4hltVXkLvYHOOmLP4qqh2fx7aMLkS5q0cMoEg5AFER3iETj13uoFyhz8yiAKESyAZSDjjQwp8A1QDw==", + "revoked": false, + "signature": "AhgblSOdxUkLwpUN9Ec46St3JGaw2jPyDn/mLcR4j3EjKxUOwHBYqqkxcQdRz/6K4Qo/xMa941MgUp6NjNbKBA==" + } + ] + } + ] +} +``` + +#### `wot/members` + + +**Goal** + +GET the list of current Web of Trust members. + +**Parameters** + +*None*. + +**Returns** + +A list of public key + uid. +```json +{ + "results": [ + { "pubkey": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", "uid": "cat" }, + { "pubkey": "9kNEiyseUNoPn3pmNUhWpvCCwPRgavsLu7YFKZuzzd1L", "uid": "tac" }, + { "pubkey": "9HJ9VXa9wc6EKC6NkCi8b5TKWBot68VhYDg7kDk5T8Cz", "uid": "toc" } + ] +} +``` + +#### `wot/requirements/[pubkey]` + + +**Goal** + +GET requirements to be filled by pubkey to become a member. + +**Parameters** + +Name | Value | Method +---- | ----- | ------ +`pbkey` | Public key to check. | URL + +**Returns** + +A list of identities matching this pubkey and the requirements of each identities to become a member. + +> If the pubkey is matching a member, only one identity may be displayed: the one which is a member. + +```json +{ + "pubkey": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "identities": [{ + "uid": "tobi", + "meta": { + "timestamp": "12-20504546F37853625C1E695B757D93CFCC6E494069D53F73748E428947933E45" + }, + "outdistanced": true, + "certifications": 2, + "membershipMissing": true + } + ... + ] +} +``` + +#### `wot/certifiers-of/[search]` + + +**Goal** + +GET [Certification](./Protocol.md#certification) data over a member. + +**Parameters** + +Name | Value | Method +---- | ----- | ------ +`search` | Public key or uid of a *member* (or someone who *was a member*) we want see the certifications. | URL + +**Returns** + +A list of certifications issued to the member by other members (or who used to be members), with `written` data indicating wether the certification is written in the blockchain or not. + +Each certification also has: + +* a `isMember` field to indicate wether the issuer of the certification is still a member or not. +* a `written` field to indicate the block where the certification was written (or null if not written yet). + +```json +{ + "pubkey": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "uid": "user identifier", + "isMember": true, + sigDate: 1421787461, + "certifications": [ + { + "pubkey": "9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB", + "uid": "certifier uid", + "cert_time": { + "block": 88, + "medianTime": 1509991044 + }, + sigDate: 1421787461, + "written": { + "number": 872768, + "hash": "D30978C9D6C5A348A8188603F039423D90E50DC5" + }, + "isMember": true, + "signature": "42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r" + }, + ... + ] +} +``` + +#### `wot/certified-by/[search]` + + +**Goal** + +GET [Certification](./Protocol.md#certification) data over a member. + +**Parameters** + +Name | Value | Method +---- | ----- | ------ +`search` | Public key or uid of a *member* (or someone who *was a member*) we want see the certifications. | URL + +**Returns** + +A list of certifications issued by the member to other members (or who used to be members), with `written` data indicating wether the certification is written in the blockchain or not. + +Each certification also has: + +* a `isMember` field to indicate wether the issuer of the certification is still a member or not. +* a `written` field to indicate the block where the certification was written (or null if not written yet). +```json +{ + "pubkey": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "uid": "user identifier", + "isMember": true, + sigDate: 1421787461, + "certifications": [ + { + "pubkey": "9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB", + "uid": "certifier uid", + "cert_time": { + "block": 88, + "medianTime": 1509991044 + }, + sigDate: 1421787461, + "written": { + "number": 872768, + "hash": "D30978C9D6C5A348A8188603F039423D90E50DC5" + }, + "isMember": true, + "signature": "42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r" + }, + ... + ] +} +``` + +#### `wot/identity-of/[search]` + + +**Goal** + +GET identity data written for a member. + +**Parameters** + +Name | Value | Method +---- | ----- | ------ +`search` | Public key or uid of a *member* we want see the attached identity. | URL + +**Returns** + +Identity data written in the blockchain. +```json +{ + "pubkey": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "uid": "user identifier", + "sigDate": "21-EB18A8D89256EA80195990C91AD399B798F92EE8187F775DF7F4823C46E61F00" +} +``` + + +### blockchain/* + +#### `blockchain/parameters` + +**Goal** + +GET the blockchain parameters used by this node. + +**Parameters** + +*None*. + +**Returns** + +The synchronization parameters. +```json +{ + currency: "beta_brouzouf", + c: 0.01, + dt: 302400, + ud0: 100, + sigPeriod: 7200, + sigStock: 45, + sigWindow: 604800, + sigValidity: 2629800, + sigQty: 3, + idtyWindow: 604800, + msWindow: 604800, + xpercent: 5, + msValidity: 2629800, + stepMax: 3, + medianTimeBlocks: 11, + avgGenTime: 600, + dtDiffEval: 10, + percentRot: 0.67 +} +``` + +Parameters meaning is described under [Protocol parameters](./Protocol.md#protocol-parameters). + +#### `blockchain/membership` + + +**Goal** + +POST a [Membership](./Protocol.md#membership) document. + +**Parameters** + +Name | Value | Method +---- | ----- | ------ +`membership` | The membership document (with signature). | POST + +**Returns** + +The posted membership request. +```json +{ + "signature": "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", + "membership": { + "version": "2", + "currency": "beta_brouzouf", + "issuer": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "membership": "IN", + "sigDate": 1390739944, + "uid": "superman63" + } +} +``` + +#### `blockchain/memberships/[search]` + + +**Goal** + +GET [Membership](./Protocol.md#membership) data written for a member. + +**Parameters** + +Name | Value | Method +---- | ----- | ------ +`search` | Public key or uid of a *member* we want see the memberships. | URL + +**Returns** + +A list of memberships issued by the *member* and written in the blockchain. +```json +{ + "pubkey": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "uid": "user identifier", + "sigDate": 1390739944, + "memberships": [ + { + "version": "2", + "currency": "beta_brouzouf", + "membership": "IN", + "blockNumber": 678, + "blockHash": "000007936DF3CC32BFCC1023D1258EC9E485D474", + "written_number": null + }, + ... + ] +} +``` + +#### `blockchain/block` + +**Goal** + +POST a new block to add to the blockchain. + +**Parameters** + +Name | Value | Method +------------------ | ------------------------------ | ------ +`block` | The raw block to be added | POST +`signature` | Signature of the raw block | POST + +**Returns** + +The promoted block if successfully added to the blockchain (see [block/[number]](#blockchainblocknumber) return object). + +#### `blockchain/block/[NUMBER]` + +**Goal** + +GET the promoted block whose number `NUMBER`. + +**Parameters** + +Name | Value | Method +------------------ | ------------------------------------------------------------- | ------ +`NUMBER` | The promoted block number (integer value) we want to see. | URL + +**Returns** + +The promoted block if it exists (otherwise return HTTP 404). +```json +{ + "version": 2, + "currency": "beta_brouzouf", + "nonce": 28, + "inner_hash": "FD09B0F7CEC5A575CA6E528DC4C854B612AE77B7283F48E0D28677F5C9C9D0DD", + "number": 1, + "time": 1408996317, + "medianTime": 1408992543, + "dividend": 254, + "monetaryMass": 18948, + "issuer": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "previousHash": "0009A7A62703F976F683BBA500FC0CB832B8220D", + "previousIssuer": "CYYjHsNyg3HMRMpTHqCJAN9McjH5BwFLmDKGV3PmCuKp", + "membersCount": 4, + "hash": "0000F40BDC0399F2E84000468628F50A122B5F16", + "identities": [ + "9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB:2D96KZwNUvVtcapQPq2mm7J9isFcDCfykwJpVEZwBc7tCgL4qPyu17BT5ePozAE9HS6Yvj51f62Mp4n9d9dkzJoX:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:crowlin" + ], + "joiners": [ + "9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB:2XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:crowlin" + ], + "leavers": [ + "9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB:2XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:crowlin" + ], + "revoked": [ + "9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB:2D96KZwNUvVtcapQPq2mm7J9isFcDCfykwJpVEZwBc7tCgL4qPyu17BT5ePozAE9HS6Yvj51f62Mp4n9d9dkzJoX" + ], + "excluded": [ + "9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB" + ], + "certifications": [ + "CYYjHsNyg3HMRMpTHqCJAN9McjH5BwFLmDKGV3PmCuKp:9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB:1505900000:2XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk" + ], + "transactions": [ + { + "signatures": [ + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==" + "2D96KZwNUvVtcapQPq2mm7J9isFcDCfykwJpVEZwBc7tCgL4qPyu17BT5ePozAE9HS6Yvj51f62Mp4n9d9dkzJoX", + "2XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk" + ], + "version": 2, + "currency": "beta_brouzouf", + "issuers": [ + "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "CYYjHsNyg3HMRMpTHqCJAN9McjH5BwFLmDKGV3PmCuKp", + "9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB" + ], + "inputs": [ + "T:6991C993631BED4733972ED7538E41CCC33660F554E3C51963E2A0AC4D6453D3:0", + "T:3A09A20E9014110FD224889F13357BAB4EC78A72F95CA03394D8CCA2936A7435:0", + "D:4745EEBA84D4E3C2BDAE4768D4E0F5A671531EE1B0B9F5206744B4551C664FDF:243", + "T:3A09A20E9014110FD224889F13357BAB4EC78A72F95CA03394D8CCA2936A7435:1", + "T:67F2045B5318777CC52CD38B424F3E40DDA823FA0364625F124BABE0030E7B5B:0", + "D:521A760049DF4FAA602FEF86B7A8E306654502FA3A345F6169B8468B81E71AD3:187" + ], + "unlocks": [ + "0:SIG(0)", + "1:SIG(2)", + "2:SIG(1)", + "3:SIG(1)", + "4:SIG(0)", + "5:SIG(0)" + ], + "outputs": [ + "30:SIG(BYfWYFrsyjpvpFysgu19rGK3VHBkz4MqmQbNyEuVU64g)", + "156:SIG(DSz4rgncXCytsUMW2JU2yhLquZECD2XpEkpP9gG5HyAx)", + "49:SIG(6DyGr5LFtFmbaJYRvcs9WmBsr4cbJbJ1EV9zBbqG7A6i)" + ] + } + ], + "signature": "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==", +} +``` + +#### `blockchain/blocks/[COUNT]/[FROM]` + +**Goal** + +GET the `[COUNT]` promoted blocks from `[FROM]` number, inclusive. + +**Parameters** + +Name | Value | Method +------------------ | ------------------------------------------------------------- | ------ +`COUNT` | The number of blocks we want to see. | URL +`FROM` | The starting block. | URL + +**Returns** + +The promoted blocks if it exists block `[FROM]` (otherwise return HTTP 404). Result is an array whose values are the same structure as [/blockchain/block/[number]](#blockchainblocknumber). +```json +{ + "blocks": [ + { number: 2, ... }, + { number: 3, ... } + ] +} +``` + +#### `blockchain/current` + +Same as [block/[number]](#blockchainblocknumber), but return last accepted block. + +#### `blockchain/hardship/[PUBKEY]` + +**Goal** + +GET hardship level for given member's pubkey for writing next block. + +**Parameters** + +Name | Value | Method +---- | ----- | ------ +`PUBKEY` | Member's pubkey. | URL + +**Returns** + +The hardship value (`level`) + `block` number. +```json +{ + "block": 598, + "level": 3 +} + +``` + +#### `blockchain/difficulties` + +**Goal** + +GET hardship level for all member's uid for writing next block. + +**Parameters** + +None. + +**Returns** + +The respective difficulty of each member in the last `IssuersFrame` blocks for current block. +```json +{ + "block": 598, + "levels": [{ + "uid": "jack", + "level": 8 + },{ + "uid": "cat", + "level": 4 + }] +} + +``` + +#### `blockchain/with/newcomers` +**Goal** + +GET the block numbers containing newcomers (new identities). + +**Parameters** + +*None*. + +**Returns** + +Block numbers. +```json +{ + "result": { + "blocks": [223,813] + } +} +``` + +#### `blockchain/with/certs` +**Goal** + +GET the block numbers containing certifications. + +**Parameters** + +*None*. + +**Returns** + +Block numbers. +```json +{ + "result": { + "blocks": [223,813] + } +} +``` + +#### `blockchain/with/joiners` +**Goal** + +GET the block numbers containing joiners (newcomers or people coming back after exclusion). + +**Parameters** + +*None*. + +**Returns** + +Block numbers. +```json +{ + "result": { + "blocks": [223,813] + } +} +``` + +#### `blockchain/with/actives` +**Goal** + +GET the block numbers containing actives (members updating their membership). + +**Parameters** + +*None*. + +**Returns** + +Block numbers. +```json +{ + "result": { + "blocks": [223,813] + } +} +``` + +#### `blockchain/with/leavers` +**Goal** + +GET the block numbers containing leavers (members leaving definitely the currency). + +**Parameters** + +*None*. + +**Returns** + +Block numbers. +```json +{ + "result": { + "blocks": [223,813] + } +} +``` + +#### `blockchain/with/revoked` +**Goal** + +GET the block numbers containing revoked members. + +**Parameters** + +*None*. + +**Returns** + +Block numbers. +```json +{ + "result": { + "blocks": [223,813] + } +} +``` + +#### `blockchain/with/excluded` +**Goal** + +GET the block numbers containing excluded members. + +**Parameters** + +*None*. + +**Returns** + +Block numbers. +```json +{ + "result": { + "blocks": [223,813] + } +} +``` + +#### `blockchain/with/ud` +**Goal** + +GET the block numbers containing Universal Dividend. + +**Parameters** + +*None*. + +**Returns** + +Block numbers. +```json +{ + "result": { + "blocks": [223,813] + } +} +``` + +#### `blockchain/with/tx` +**Goal** + +GET the block numbers containing transactions. + +**Parameters** + +*None*. + +**Returns** + +Block numbers. +```json +{ + "result": { + "blocks": [223,813] + } +} +``` + +#### `blockchain/branches` + +**Goal** + +GET current branches of the node. + +**Parameters** + +*None*. + +**Returns** + +Top block of each branch, i.e. the last received block of each branch. An array of 4 blocks would mean the node has 4 branches, +3 would mean 3 branches, and so on. + +```json +{ + "blocks": [ + { number: 2, ... }, + { number: 3, ... } + ] +} +``` + +### network/* + +This URL is used for Duniter Gossip protocol (exchanging UCG messages). + +#### `network/peers` +**Goal** + +GET the exhaustive list of peers known by the node. + +**Parameters** + +*None*. + +**Returns** + +List of peering entries. +```json +{ + "peers": [ + { + "version": "2", + "currency": "meta_brouzouf", + "status": "UP", + "first_down": null, + "last_try": null, + "pubkey": "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", + "block": "45180-00000E577DD4B308B98D0ED3E43926CE4D22E9A8", + "signature": "GKTrlUc4um9lQuj9UI8fyA/n/JKieYqBYcl9keIWfAVOnvHamLHaqGzijsdX1kNt64cadcle/zkd7xOgMTdQAQ==", + "endpoints": [ + "BASIC_MERKLED_API metab.ucoin.io 88.174.120.187 9201" + ] + }, + { + "version": "2", + "currency": "meta_brouzouf", + "status": "UP", + "first_down": null, + "last_try": null, + "pubkey": "2aeLmae5d466y8D42wLK5MknwUBCR6MWWeixRzdTQ4Hu", + "block": "45182-0000064EEF412C1CDD1B370CC45A3BC3B9743464", + "signature": "kbdTay1OirDqG/E3jyCaDlL7HVVHb9/BXvNHAg+xO9sSA+NgmBo/4mEqL9b7hH0UnbXHss6TfuvxAHZLmBqsCw==", + "endpoints": [ + "BASIC_MERKLED_API twiced.fr 88.174.120.187 9223" + ] + }, + ... + ] +} +``` + +#### `network/peering` +**Goal** + +GET the peering informations of this node. + +**Parameters** + +*None*. + +**Returns** + +Peering entry of the node. +```json +{ + "version": "2", + "currency": "beta_brouzouf", + "pubkey": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "endpoints": [ + "BASIC_MERKLED_API some.dns.name 88.77.66.55 2001:0db8:0000:85a3:0000:0000:ac1f 9001", + "BASIC_MERKLED_API some.dns.name 88.77.66.55 2001:0db8:0000:85a3:0000:0000:ac1f 9002", + "OTHER_PROTOCOL 88.77.66.55 9001", + ], + "signature": "42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r" +} +``` + +#### `network/peering/peers (GET)` +**Goal** + +Merkle URL refering to peering entries of every node inside the currency network. + +**Parameters** + +*None*. + +**Returns** + +Merkle URL result. +```json +{ + "depth": 3, + "nodesCount": 6, + "leavesCount": 5, + "root": "114B6E61CB5BB93D862CA3C1DFA8B99E313E66E9" +} +``` + +Merkle URL leaf: peering entry +```json +{ + "hash": "2E69197FAB029D8669EF85E82457A1587CA0ED9C", + "value": { + "version": "2", + "currency": "beta_brouzouf", + "pubkey": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "endpoints": [ + "BASIC_MERKLED_API some.dns.name 88.77.66.55 2001:0db8:0000:85a3:0000:0000:ac1f 9001", + "BASIC_MERKLED_API some.dns.name 88.77.66.55 2001:0db8:0000:85a3:0000:0000:ac1f 9002", + "OTHER_PROTOCOL 88.77.66.55 9001", + ], + "signature": "42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r" + } +} +``` + +#### `network/peering/peers (POST)` +**Goal** + +POST a peering entry document. + +**Parameters** + +Name | Value | Method +----------- | ----------------------------------- | ------ +`peer` | The peering entry document. | POST + +**Returns** + +The posted entry. +```json +{ + "version": "2", + "currency": "beta_brouzouf", + "pubkey": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "endpoints": [ + "BASIC_MERKLED_API some.dns.name 88.77.66.55 2001:0db8:0000:85a3:0000:0000:ac1f 9001", + "BASIC_MERKLED_API some.dns.name 88.77.66.55 2001:0db8:0000:85a3:0000:0000:ac1f 9002", + "OTHER_PROTOCOL 88.77.66.55 9001", + ], + "signature": "42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r" +} +``` + +### tx/* + +#### `tx/process` +**Goal** + +POST a transaction. + +**Parameters** + +Name | Value | Method +----------------- | ------------------------------------------------------------- | ------ +`transaction` | The raw transaction. | POST + +**Returns** + +The recorded transaction. +```json +{ + "raw": "Version: 2\r\n...\r\n", + "transaction": + { + "signatures": [ + "H41/8OGV2W4CLKbE35kk5t1HJQsb3jEM0/QGLUf80CwJvGZf3HvVCcNtHPUFoUBKEDQO9mPK3KJkqOoxHpqHCw==" + "2D96KZwNUvVtcapQPq2mm7J9isFcDCfykwJpVEZwBc7tCgL4qPyu17BT5ePozAE9HS6Yvj51f62Mp4n9d9dkzJoX", + "2XiBDpuUdu6zCPWGzHXXy8c4ATSscfFQG9DjmqMZUxDZVt1Dp4m2N5oHYVUfoPdrU9SLk4qxi65RNrfCVnvQtQJk" + ], + "version": 2, + "currency": "beta_brouzouf", + "issuers": [ + "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "CYYjHsNyg3HMRMpTHqCJAN9McjH5BwFLmDKGV3PmCuKp", + "9WYHTavL1pmhunFCzUwiiq4pXwvgGG5ysjZnjz9H8yB" + ], + "inputs": [ + "T:6991C993631BED4733972ED7538E41CCC33660F554E3C51963E2A0AC4D6453D3:0", + "T:3A09A20E9014110FD224889F13357BAB4EC78A72F95CA03394D8CCA2936A7435:0", + "D:4745EEBA84D4E3C2BDAE4768D4E0F5A671531EE1B0B9F5206744B4551C664FDF:243", + "T:3A09A20E9014110FD224889F13357BAB4EC78A72F95CA03394D8CCA2936A7435:1", + "T:67F2045B5318777CC52CD38B424F3E40DDA823FA0364625F124BABE0030E7B5B:0", + "D:521A760049DF4FAA602FEF86B7A8E306654502FA3A345F6169B8468B81E71AD3:187" + ], + "unlocks": [ + "0:SIG(0)", + "1:SIG(2)", + "2:SIG(1)", + "3:SIG(1)", + "4:SIG(0)", + "5:SIG(0)" + ], + "outputs": [ + "30:SIG(BYfWYFrsyjpvpFysgu19rGK3VHBkz4MqmQbNyEuVU64g)", + "156:SIG(DSz4rgncXCytsUMW2JU2yhLquZECD2XpEkpP9gG5HyAx)", + "49:SIG(6DyGr5LFtFmbaJYRvcs9WmBsr4cbJbJ1EV9zBbqG7A6i)" + ] + } +} +``` + + +#### `tx/sources/[pubkey]` + +**Goal** + +GET a list of available sources. + +**Parameters** + +Name | Value | Method +---- | ----- | ------ +`pubkey` | Owner of the coins' pubkey. | URL + +**Returns** + +A list of available sources for the given `pubkey`. +```json +{ + "currency": "beta_brouzouf", + "pubkey": "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY", + "sources": [ + { + "type: "D", + "noffset": 5, + "identifier": "6C20752F6AD06AEA8D0BB46BB8C4F68641A34C79", + "amount": 100 + }, + { + "type: "D", + "noffset": 18, + "identifier": "DB7D88E795E42CF8CFBFAAFC77379E97847F9B42", + "amount": 110 + }, + { + "type: "T", + "noffset": 55, + "identifier": "E614E814179F313B1113475E6319EF4A3D470AD0", + "amount": 30 + } + ] +} +``` + + +#### `tx/history/[pubkey]` + +**Goal** + +Get the wallet transaction history + +**parameters** + +Name | Value | Method +---- | ----- | ------ +`pubkey` | Wallet public key. | URL + +**Returns** + +The full transaction history for the given `pubkey` +```json +{ + "currency": "meta_brouzouf", + "pubkey": "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", + "history": { + "sent": [ + { + "version": 2, + "received": null, + "issuers": [ + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" + ], + "inputs": [ + "D:000A8362AE0C1B8045569CE07735DE4C18E81586:125" + ], + "outputs": [ + "5:SIG(8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU)", + "95:SIG(HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk)" + ], + "comment": "Essai", + "signatures": [ + "8zzWSU+GNSNURnH1NKPT/TBoxngEc/0wcpPSbs7FqknGxud+94knvT+dpe99k6NwyB5RXvOVnKAr4p9/KEluCw==" + ], + "hash": "FC7BAC2D94AC9C16AFC5C0150C2C9E7FBB2E2A09", + "block_number": 173, + "time": 1421932545 + } + ], + "received": [ + { + "version": 2, + "received": null, + "issuers": [ + "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU" + ], + "inputs": [ + "D:000A8362AE0C1B8045569CE07735DE4C18E81586:125" + ], + "outputs": [ + "7:SIG(HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk)", + "93:SIG(8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU)" + ], + "comment": "", + "signatures": [ + "1Mn8q3K7N+R4GZEpAUm+XSyty1Uu+BuOy5t7BIRqgZcKqiaxfhAUfDBOcuk2i4TJy1oA5Rntby8hDN+cUCpvDg==" + ], + "hash": "5FB3CB80A982E2BDFBB3EA94673A74763F58CB2A", + "block_number": 207, + "time": 1421955525 + }, + { + "version": 2, + "received": null, + "issuers": [ + "J78bPUvLjxmjaEkdjxWLeENQtcfXm7iobqB49uT1Bgp3" + ], + "inputs": [ + "T:6A50FF82410387B239489CE38B34E0FDDE1697FE:0" + ], + "outputs": [ + "42:SIG(HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk)", + "9958:SIG(J78bPUvLjxmjaEkdjxWLeENQtcfXm7iobqB49uT1Bgp3)" + ], + "comment": "", + "signatures": [ + "XhBcCPizPiWdKeXWg1DX/FTQst6DppEjsYEtoAZNA0P11reXtgc9IduiIxNWzNjt/KvTw8APkSI8/Uf31QQVDA==" + ], + "hash": "ADE7D1C4002D6BC10013C34CE22733A55173BAD4", + "block_number": 15778, + "time": 1432314584 + } + ], + "sending": [ + { + "version": 2, + "received": 1459691641, + "issuers": [ + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" + ], + "inputs": [ + "0:D:8196:000022AD426FE727C707D847EC2168A64C577706:5872" + ], + "outputs": [ + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:5871" + "2sq8bBDQGK74f1eD3mAPQVgHCmFdijZr9nbv16FwbokX:1", + ], + "comment": "some comment", + "signatures": [ + "kLOAAy7/UldQk7zz4I7Jhv9ICuGYRx7upl8wH8RYL43MMF6+7MbPh3QRN1qNFGpAfa3XMWIQmbUWtjZKP6OfDA==" + ], + "hash": "BA41013F2CD38EDFFA9D38A275F8532DD906A2DE" + } + ], + "receiving": [ + { + "version": 2, + "received": 1459691641, + "issuers": [ + "2sq8bBDQGK74f1eD3mAPQVgHCmFdijZr9nbv16FwbokX" + ], + "inputs": [ + "0:D:8196:000022AD426FE727C707D847EC2168A64C577706:4334" + ], + "outputs": [ + "2sq8bBDQGK74f1eD3mAPQVgHCmFdijZr9nbv16FwbokX:1", + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:4333" + ], + "comment": "some comment", + "signatures": [ + "DRiZinUEKrrLiJNogtydzwEbmETrvWiLNYXCiJsRekxTLyU5g4LjnwiLp/XlvmIekjJK5n/gullLWrHUBvFSAw== + ], + "hash": "A0A511131CD0E837204A9441B3354918AC4CE671" + } + ] + } +} +``` + +#### `tx/history/[PUBKEY]/blocks/[from]/[to]` + +**Goal** + +Get the wallet transaction history + +**parameters** + +Name | Value | Method +---- | ----- | ------ +`pubkey` | Wallet public key. | URL +`from` | The starting block. | URL +`to` | the ending block. | URL + +**Returns** + +The transaction history for the given `pubkey` and between the given `from` and `to` blocks. +```json +{ + "currency": "meta_brouzouf", + "pubkey": "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", + "history": { + "sent": [ + { + "version": 2, + "issuers": [ + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" + ], + "inputs": [ + "0:D:125:000A8362AE0C1B8045569CE07735DE4C18E81586:100" + ], + "outputs": [ + "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:5", + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:95" + ], + "comment": "Essai", + "signatures": [ + "8zzWSU+GNSNURnH1NKPT/TBoxngEc/0wcpPSbs7FqknGxud+94knvT+dpe99k6NwyB5RXvOVnKAr4p9/KEluCw==" + ], + "hash": "FC7BAC2D94AC9C16AFC5C0150C2C9E7FBB2E2A09", + "block_number": 173, + "time": 1421932545 + } + ], + "received": [ + { + "version": 2, + "issuers": [ + "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU" + ], + "inputs": [ + "0:D:125:000A8362AE0C1B8045569CE07735DE4C18E81586:100" + ], + "outputs": [ + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:7", + "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:93" + ], + "comment": "", + "signatures": [ + "1Mn8q3K7N+R4GZEpAUm+XSyty1Uu+BuOy5t7BIRqgZcKqiaxfhAUfDBOcuk2i4TJy1oA5Rntby8hDN+cUCpvDg==" + ], + "hash": "5FB3CB80A982E2BDFBB3EA94673A74763F58CB2A", + "block_number": 207, + "time": 1421955525 + }, + { + "version": 2, + "issuers": [ + "J78bPUvLjxmjaEkdjxWLeENQtcfXm7iobqB49uT1Bgp3" + ], + "inputs": [ + "0:T:15128:6A50FF82410387B239489CE38B34E0FDDE1697FE:10000" + ], + "outputs": [ + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:42", + "J78bPUvLjxmjaEkdjxWLeENQtcfXm7iobqB49uT1Bgp3:9958" + ], + "comment": "", + "signatures": [ + "XhBcCPizPiWdKeXWg1DX/FTQst6DppEjsYEtoAZNA0P11reXtgc9IduiIxNWzNjt/KvTw8APkSI8/Uf31QQVDA==" + ], + "hash": "ADE7D1C4002D6BC10013C34CE22733A55173BAD4", + "block_number": 15778, + "time": 1432314584 + } + ], + "sending": [ + { + "version": 2, + "issuers": [ + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" + ], + "inputs": [ + "0:D:8196:000022AD426FE727C707D847EC2168A64C577706:5872" + ], + "outputs": [ + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:5871" + "2sq8bBDQGK74f1eD3mAPQVgHCmFdijZr9nbv16FwbokX:1", + ], + "comment": "some comment", + "signatures": [ + "kLOAAy7/UldQk7zz4I7Jhv9ICuGYRx7upl8wH8RYL43MMF6+7MbPh3QRN1qNFGpAfa3XMWIQmbUWtjZKP6OfDA==" + ], + "hash": "BA41013F2CD38EDFFA9D38A275F8532DD906A2DE" + } + ], + "receiving": [ + { + "version": 2, + "issuers": [ + "2sq8bBDQGK74f1eD3mAPQVgHCmFdijZr9nbv16FwbokX" + ], + "inputs": [ + "0:D:8196:000022AD426FE727C707D847EC2168A64C577706:4334" + ], + "outputs": [ + "2sq8bBDQGK74f1eD3mAPQVgHCmFdijZr9nbv16FwbokX:1", + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:4333" + ], + "comment": "some comment", + "signatures": [ + "DRiZinUEKrrLiJNogtydzwEbmETrvWiLNYXCiJsRekxTLyU5g4LjnwiLp/XlvmIekjJK5n/gullLWrHUBvFSAw== + ], + "hash": "A0A511131CD0E837204A9441B3354918AC4CE671" + } + ] + } +} +``` + +#### `tx/history/[pubkey]/times/[from]/[to]` + +**Goal** + +Get the wallet transaction history + +**parameters** + +Name | Value | Method +---- | ----- | ------ +`pubkey` | Wallet public key. | URL +`from` | The starting timestamp limit. (optionnal) | URL +`to` | The ending timestamp. (optionnal) | URL + +**Returns** + +The transaction history for the given `pubkey` and between the given `from` and `to` dates. +```json +{ + "currency": "meta_brouzouf", + "pubkey": "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", + "history": { + "sent": [ + { + "version": 2, + "issuers": [ + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" + ], + "inputs": [ + "0:D:125:000A8362AE0C1B8045569CE07735DE4C18E81586:100" + ], + "outputs": [ + "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:5", + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:95" + ], + "comment": "Essai", + "signatures": [ + "8zzWSU+GNSNURnH1NKPT/TBoxngEc/0wcpPSbs7FqknGxud+94knvT+dpe99k6NwyB5RXvOVnKAr4p9/KEluCw==" + ], + "hash": "FC7BAC2D94AC9C16AFC5C0150C2C9E7FBB2E2A09", + "block_number": 173, + "time": 1421932545 + } + ], + "received": [ + { + "version": 2, + "issuers": [ + "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU" + ], + "inputs": [ + "0:D:125:000A8362AE0C1B8045569CE07735DE4C18E81586:100" + ], + "outputs": [ + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:7", + "8Fi1VSTbjkXguwThF4v2ZxC5whK7pwG2vcGTkPUPjPGU:93" + ], + "comment": "", + "signatures": [ + "1Mn8q3K7N+R4GZEpAUm+XSyty1Uu+BuOy5t7BIRqgZcKqiaxfhAUfDBOcuk2i4TJy1oA5Rntby8hDN+cUCpvDg==" + ], + "hash": "5FB3CB80A982E2BDFBB3EA94673A74763F58CB2A", + "block_number": 207, + "time": 1421955525 + }, + { + "version": 2, + "issuers": [ + "J78bPUvLjxmjaEkdjxWLeENQtcfXm7iobqB49uT1Bgp3" + ], + "inputs": [ + "0:T:15128:6A50FF82410387B239489CE38B34E0FDDE1697FE:10000" + ], + "outputs": [ + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:42", + "J78bPUvLjxmjaEkdjxWLeENQtcfXm7iobqB49uT1Bgp3:9958" + ], + "comment": "", + "signatures": [ + "XhBcCPizPiWdKeXWg1DX/FTQst6DppEjsYEtoAZNA0P11reXtgc9IduiIxNWzNjt/KvTw8APkSI8/Uf31QQVDA==" + ], + "hash": "ADE7D1C4002D6BC10013C34CE22733A55173BAD4", + "block_number": 15778, + "time": 1432314584 + } + ], + "sending": [ + { + "version": 2, + "issuers": [ + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" + ], + "inputs": [ + "0:D:8196:000022AD426FE727C707D847EC2168A64C577706:5872" + ], + "outputs": [ + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:5871" + "2sq8bBDQGK74f1eD3mAPQVgHCmFdijZr9nbv16FwbokX:1", + ], + "comment": "some comment", + "signatures": [ + "kLOAAy7/UldQk7zz4I7Jhv9ICuGYRx7upl8wH8RYL43MMF6+7MbPh3QRN1qNFGpAfa3XMWIQmbUWtjZKP6OfDA==" + ], + "hash": "BA41013F2CD38EDFFA9D38A275F8532DD906A2DE" + } + ], + "receiving": [ + { + "version": 2, + "issuers": [ + "2sq8bBDQGK74f1eD3mAPQVgHCmFdijZr9nbv16FwbokX" + ], + "inputs": [ + "0:D:8196:000022AD426FE727C707D847EC2168A64C577706:4334" + ], + "outputs": [ + "2sq8bBDQGK74f1eD3mAPQVgHCmFdijZr9nbv16FwbokX:1", + "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk:4333" + ], + "comment": "some comment", + "signatures": [ + "DRiZinUEKrrLiJNogtydzwEbmETrvWiLNYXCiJsRekxTLyU5g4LjnwiLp/XlvmIekjJK5n/gullLWrHUBvFSAw== + ], + "hash": "A0A511131CD0E837204A9441B3354918AC4CE671" + } + ] + } +} +``` +### ud/* + +#### `ud/history/[pubkey]` + +**Goal** + +Get the wallet universal dividend history + +**parameters** + +Name | Value | Method +---- | ----- | ------ +`pubkey` | Wallet public key. | URL + +**Returns** + +The universal dividend history for the given `pubkey`. +```json +{ + "currency": "meta_brouzouf", + "pubkey": "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", + "history": { + "history": [ + { + "block_number": 125, + "consumed": true, + "time": 1421927007, + "amount": 100 + }, + { + "block_number": 410, + "consumed": false, + "time": 1422012828, + "amount": 100 + }, + { + "block_number": 585, + "consumed": true, + "time": 1422098800, + "amount": 100 + } + ] + } +} +``` + + +### ws/* + +#### `ws/block` + +**Goal** + +A websocket entry point for receiving blocks. + +**Parameters** + +*None*. + +**Returns** + +Websocket connection. + +#### `ws/peer` + +**Goal** + +A websocket entry point for receiving peers. + +**Parameters** + +*None*. + +**Returns** + +Websocket connection. diff --git a/test/fast/modules/bma/ddos-test.js b/test/fast/modules/bma/ddos-test.js new file mode 100644 index 0000000000000000000000000000000000000000..c4127c62d6754fb0fc8f1a64260d235590a68dc9 --- /dev/null +++ b/test/fast/modules/bma/ddos-test.js @@ -0,0 +1,39 @@ +"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('../lib/bma'); +// +// limiter.noLimit(); +// +// const s1 = toolbox.server({ +// pair: { +// pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd', +// sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP' +// } +// }); +// +// describe('DDOS', () => { +// +// before(() => co(function*() { +// limiter.noLimit(); +// yield s1.initWithDAL().then(bma).then((bmapi) => { +// s1.bma = bmapi; +// bmapi.openConnections(); +// }); +// })); +// +// it('should not be able to send more than 4 reqs/s', () => co(function*() { +// try { +// s1.bma.getDDOS().params.limit = 3; +// s1.bma.getDDOS().params.burst = 3; +// s1.bma.getDDOS().params.whitelist = []; +// yield Array.from({ length: 4 }).map(() => s1.get('/blockchain/current')); +// throw 'Wrong error thrown'; +// } catch (e) { +// e.should.have.property('statusCode').equal(429); +// } +// })); +// }); diff --git a/test/fast/modules/bma/limiter-test.js b/test/fast/modules/bma/limiter-test.js new file mode 100644 index 0000000000000000000000000000000000000000..0ba0c0254a0b8c6597f4628b01d09e3245a52e65 --- /dev/null +++ b/test/fast/modules/bma/limiter-test.js @@ -0,0 +1,60 @@ +"use strict"; +// const should = require('should'); +// const co = require('co'); +// const limiter = require('../lib/limiter'); +// const toolbox = require('../integration/tools/toolbox'); +// const user = require('../integration/tools/user'); +// const bma = require('duniter-bma').duniter.methods.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/fast/modules/bma/module-test.js b/test/fast/modules/bma/module-test.js new file mode 100644 index 0000000000000000000000000000000000000000..9d9bb5063276b80aff738f16be61b54b32004dc4 --- /dev/null +++ b/test/fast/modules/bma/module-test.js @@ -0,0 +1,140 @@ +"use strict"; +const assert = require('assert'); +const should = require('should'); +const co = require('co'); +const duniterBMA = require('../../../../app/modules/bma/index').BmaDependency +const duniterKeypair = require('../../../../app/modules/keypair').KeypairDependency +const network = require('../../../../app/modules/bma/lib/network').Network +const duniter = require('../../../../index') +const logger = require('../../../../app/lib/logger').NewLogger() +const rp = require('request-promise'); + +// Do not pollute the tests with logs +logger.mute(); + +const stack = duniter.statics.minimalStack(); +stack.registerDependency(duniterKeypair, 'duniter-keypair'); +stack.registerDependency(duniterBMA, 'duniter-bma'); + +describe('Module usage', () => { + + it('/node/summary should answer', () => co(function*() { + stack.registerDependency({ + duniter: { + cli: [{ + name: 'test1', + desc: 'Unit Test execution', + onDatabaseExecute: (server, conf, program, params, startServices) => co(function*() { + yield startServices(); + }) + }] + } + }, 'duniter-automated-test'); + yield stack.executeStack(['node', 'index.js', 'test1', + '--memory', + '--ipv4', '127.0.0.1', + '--port', '10400' + ]); + const json = yield rp.get({ + url: 'http://127.0.0.1:10400/node/summary', + json: true, + timeout: 1000 + }); + should.exist(json); + json.should.have.property('duniter').property('software').equal('duniter'); + })); + + it('remoteipv4 should NOT be filled if remote Host is declared', () => co(function*() { + stack.registerDependency({ + duniter: { + cli: [{ + name: 'test2', + desc: 'Unit Test execution', + onConfiguredExecute: (server, conf, program, params, startServices) => co(function*() { + conf.should.not.have.property('remoteipv4'); + conf.should.have.property('remoteipv6').equal(undefined); + conf.should.have.property('remotehost').equal('localhost'); + }) + }] + } + }, 'duniter-automated-test'); + yield stack.executeStack(['node', 'index.js', 'test2', + '--memory', + '--ipv4', '127.0.0.1', + '--remoteh', 'localhost', + '--port', '10400' + ]); + })); + + it('remoteipv4 should NOT be filled if remote IPv6 is declared', () => co(function*() { + stack.registerDependency({ + duniter: { + cli: [{ + name: 'test3', + desc: 'Unit Test execution', + onConfiguredExecute: (server, conf, program, params, startServices) => co(function*() { + conf.should.not.have.property('remoteipv4'); + conf.should.not.have.property('remotehost'); + conf.should.have.property('remoteipv6').equal('::1'); + }) + }] + } + }, 'duniter-automated-test'); + yield stack.executeStack(['node', 'index.js', 'test3', + '--memory', + '--ipv4', '127.0.0.1', + '--ipv6', '::1', + '--port', '10400' + ]); + })); + + it('remoteipv4 should be NOT be auto-filled if manual remoteipv4 is declared', () => co(function*() { + stack.registerDependency({ + duniter: { + cli: [{ + name: 'test4', + desc: 'Unit Test execution', + onConfiguredExecute: (server, conf, program, params, startServices) => co(function*() { + conf.should.not.have.property('remotehost'); + conf.should.have.property('remoteipv6').equal(undefined); + conf.should.have.property('remoteipv4').equal('192.168.0.1'); + }) + }] + } + }, 'duniter-automated-test'); + yield stack.executeStack(['node', 'index.js', 'test4', + '--memory', + '--remote4', '192.168.0.1', + '--ipv4', '127.0.0.1', + '--port', '10400' + ]); + })); + + it('remoteipv4 should be filled if no remote is declared, but local IPv4 is', () => co(function*() { + stack.registerDependency({ + duniter: { + cli: [{ + name: 'test5', + desc: 'Unit Test execution', + onConfiguredExecute: (server, conf, program, params, startServices) => co(function*() { + conf.should.not.have.property('remotehost'); + conf.should.have.property('remoteipv6').equal(undefined); + conf.should.have.property('remoteipv4').equal('127.0.0.1'); + }) + }] + } + }, 'duniter-automated-test'); + yield stack.executeStack(['node', 'index.js', 'test5', + '--memory', + '--ipv4', '127.0.0.1', + '--port', '10400' + ]); + })); + + it('default IPv6 should not be a local one', () => co(function*() { + const ipv6 = network.getBestLocalIPv6(); + if (ipv6) { + ipv6.should.not.match(/fe80/); + } + })); +});