ucoind 18.7 KB
Newer Older
1
#!/usr/bin/env node
Cédric Moreau's avatar
Cédric Moreau committed
2 3 4 5 6 7 8 9 10 11 12 13 14 15
var fs          = require('fs');
var os          = require('os');
var async       = require('async');
var _           = require('underscore');
var program     = require('commander');
var mongoose    = require('mongoose');
var moment      = require('moment');
var inquirer    = require('inquirer');
var openpgp     = require('openpgp');
var jpgp        = require('../app/lib/jpgp');
var wizard      = require('../app/lib/wizard');
var router      = require('../app/lib/streams/router');
var multicaster = require('../app/lib/streams/multicaster');
var logger      = require('../app/lib/logger')('ucoind');
16
var signature   = require('../app/lib/signature');
Cédric Moreau's avatar
Cédric Moreau committed
17
var ucoin       = require('./..');
18

Cédric Moreau's avatar
Cédric Moreau committed
19 20 21 22
function keys (val) {
  return val.split(',');
}

23 24 25
// Constants
var DO_NOT_LISTEN_HTTP = false;

26
program
Cédric Moreau's avatar
v0.7.0  
Cédric Moreau committed
27
  .version('0.7.0')
28
  .usage('<command> [options]')
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
  .option('-p, --port <port>',         'Port to listen for requests', parseInt)
  .option('-c, --currency <name>',     'Name of the currency managed by this node.')
  .option('--mhost <host>',            'MongoDB host.')
  .option('--mport <port>',            'MongoDB port.')
  .option('-d, --mdb <name>',          'MongoDB database name (defaults to "ucoin_default").')
  .option('--pgpkey <keyPath>',        'Path to the private key used for signing HTTP responses.')
  .option('--pgppasswd <password>',    'Password for the key provided with --httpgp-key option.')
  .option('--ipv4 <address>',          'IPV4 interface to listen for requests')
  .option('--ipv6 <address>',          'IPV6 interface to listen for requests')
  .option('--remoteh <host>',          'Remote interface others may use to contact this node')
  .option('--remote4 <host>',          'Remote interface for IPv4 access')
  .option('--remote6 <host>',          'Remote interface for IPv6 access')
  .option('--remotep <port>',          'Remote port others may use to contact this node')
  .option('--kmanagement <ALL|KEYS>',  'Define key management policy')
  .option('--kaccept <ALL|KEYS>',      'Define key acceptance policy')
  .option('--sigDelay <timestamp>',    'Minimum delay between 2 certifications of a same issuer for a same recipient, in seconds.')
  .option('--sigValidity <timestamp>', 'Validity duration of a certification, in seconds.')
  .option('--sigQty <number>',         'Minimum number of required certifications to be a member/stay as a member')
  .option('--powZeroMin <number>',     'Minimum number of leading zeros for a proof-of-work')
  .option('--powPeriod <number>',      'Number of blocks to wait to decrease proof-of-work difficulty by one')
49
  .option('--powDelay <number>',       'Number of seconds to wait before starting the computation of next block')
50 51 52
  .option('--particpate <Y|N>',        'Participate to writing the keychain')
  .option('--tsInterval <number>',     'Number of seconds as acceptable offset when receiving a keyblock')
  .option('--openpgpjs',               'Prefer using embedded Openpgpjs implementation for signing requests')
53
  ;
Cédric Moreau's avatar
Cédric Moreau committed
54

55 56 57 58 59 60 61 62 63 64 65
program
  .command('send-pubkey [host] [port]')
  .description('Send self pubkey to given node')
  .action(function (host, port) {
    // Only show message "Saved"
    require('log4js').configure({
      "appenders": [{
        category: "ucoind",
        type: "console"
      }]
    });
66
    service(ucoin.createPeerServer, function (server, conf) {
67 68 69 70 71 72 73 74
      // Only show message "Saved"
      require('log4js').configure({
        "appenders": [{
          category: "multicaster",
          type: "console"
        }]
      });
      async.waterfall([
75 76 77
        function (next){
          server.start(DO_NOT_LISTEN_HTTP, next);
        },
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
        function (next){
          server.on('peerInited', next);
        },
        function (next){
          server.PublicKeyService.getTheOne(server.PeeringService.cert.fingerprint, next);
        },
        function (pubkey, next){
          var Peer = server.conn.model('Peer');
          var peer = new Peer({
            endpoints: [['BASIC_MERKLED_API', host, port].join(' ')]
          });
          multicaster().sendPubkey(peer, pubkey, next);
        },
      ], function (err, result) {
        if (err) logger.error(err);
        server.disconnect();
        process.exit();
      });
    })(null);
  });

99 100 101
program
  .command('wizard [step]')
  .description('Launch the configuration Wizard')
102 103 104 105 106 107 108
  .action(function (step) {
    // Only show message "Saved"
    require('log4js').configure({
      "appenders": [{
        category: "ucoind",
        type: "console"
      }]
109
    });
110
    connect(function (step, server, conf) {
111 112 113 114 115
      var wiz = wizard(server);
      var task = {
        'currency': wiz.configCurrency,
        'openpgp': wiz.configOpenpgp,
        'network': wiz.configNetwork,
116 117
        'key': wiz.configKey,
        'ucp': wiz.configUCP
118 119 120 121 122 123 124 125 126 127 128 129 130
      };
      var wizDo = task[step] || wiz.configAll;
      async.waterfall([
        function (next){
          wizDo(conf, next);
        },
        function (next){
          conf.save(function (err) {
            !err && logger.debug("Configuration saved.");
            next(err);
          });
        },
        function (next) {
131
          // Check config
132
          service(ucoin.createPeerServer, function (key, server, conf) {
133 134
            next();
          })(null, null);
135 136 137 138 139 140 141 142
        }
      ], function (err, result) {
        err && logger.error(err);
        server.disconnect();
        process.exit();
      });
    })(step, null);
  });
143

144 145 146
program
  .command('sync [host] [port]')
  .description('Tries to synchronise data with remote uCoin node')
147
  .action(service(ucoin.createPeerServer, function (host, port, server, conf) {
148

149 150 151 152
    async.series([
      function (next){
        // Synchronize
        var Synchroniser = require('../app/lib/sync');
153
        var remote = new Synchroniser(server, host, port, false, conf);
154 155 156 157 158 159
        remote.sync(next);
      },
    ], function (err) {
      if(err){
        logger.error('Error during sync:', err);
      }
160
      server.disconnect();
161
      process.exit();
162 163 164
    });
  }));

165 166 167
program
  .command('allow-key [key]')
  .description('Add given key to authorized keys of this node')
168
  .action(service(ucoin.createWOTServer, function (key, server, conf) {
169 170 171 172 173 174 175 176
    key = key || "";
    key = key.toUpperCase();
    async.waterfall([
      function (next) {
        if (!key.isSha1()) {
          next("Key must match a SHA-1 hash");
          return;
        }
177
        next();
178
      },
179
      function (next){
180
        server.KeyService.setKnown(key, next);
181 182 183
      }
    ], function (err, result) {
      if(err){
184
        logger.error('Error: %s', err);
185 186
        server.disconnect();
        process.exit();
187 188
        return;
      }
189
      logger.debug("Key %s is now allowed to be stored" , key);
190
      server.disconnect();
191
      process.exit();
192 193 194
    });
  }));

195 196 197
program
  .command('manage-key [key]')
  .description('Add given key to stack of managed keys of this node')
198
  .action(service(ucoin.createWOTServer, function (key, server, conf) {
199
    handleKey(server, key, true, 'Key %s is now managed');
200 201 202 203 204
  }));

program
  .command('forget-key [key]')
  .description('Remove given key of the managed keys\' stack of this node')
205
  .action(service(ucoin.createWOTServer, function (key, server, conf) {
206
    handleKey(server, key, false, 'Key %s no more managed from now');
207 208
  }));

209
function handleKey (server, key, isManaged, message) {
210 211 212
  key = key || "";
  key = key.toUpperCase();
  async.waterfall([
213
    function (next){
214 215 216 217
      server.initServices(next);
    },
    function (next){
      server.KeyService.handleKey(key, isManaged, next);
218 219 220
    }
  ], function (err, result) {
    if(err){
221
      logger.error('Error: %s', err);
222
      server.disconnect();
223 224
      return;
    }
225
    logger.debug(message , key);
226
    server.disconnect();
227
    process.exit();
228 229 230
  });
}

231 232 233
program
  .command('gen-next [host] [port] [difficulty]')
  .description('Tries to generate the next keyblock of the keychain, with both updates & newcomers')
234
  .action(service(ucoin.createWOTServer, generateAndSend("generateNext")));
235

236 237
program
  .command('gen-empty-next [host] [port] [difficulty]')
238
  .description('Tries to generate the next keyblock of the keychain without any changes')
239
  .action(service(ucoin.createWOTServer, generateAndSend("generateEmptyNext")));
Cédric Moreau's avatar
Cédric Moreau committed
240 241 242

program
  .command('gen-updates [host] [port] [difficulty]')
Cédric Moreau's avatar
Cédric Moreau committed
243
  .description('Tries to generate an update (#1+) keyblock, containing only update changes')
244
  .action(service(ucoin.createWOTServer, generateAndSend("generateUpdates")));
245

246 247
program
  .command('gen-newcomers [host] [port] [difficulty]')
Cédric Moreau's avatar
Cédric Moreau committed
248
  .description('Tries to generate a newcomers (#0+) keyblock, containing only newcomers changes')
249
  .action(service(ucoin.createWOTServer, generateAndSend("generateNewcomers")));
250

251 252
program
  .command('gen-newcomers-auto [host] [port] [difficulty]')
Cédric Moreau's avatar
Cédric Moreau committed
253
  .description('Tries to generate a newcomers (#0+) keyblock, containing only newcomers changes')
254
  .action(service(ucoin.createWOTServer, generateAndSend("generateNewcomersAuto")));
255

Cédric Moreau's avatar
Cédric Moreau committed
256 257 258
program
  .command('compute-distances')
  .description('Recompute distance between each member and the whole WoT and mark outdistanced ones as kicked')
259
  .action(service(ucoin.createWOTServer, function (server, conf) {
Cédric Moreau's avatar
Cédric Moreau committed
260 261 262 263 264 265 266
    server.KeychainService.computeDistances(function (err) {
      if (err) logger.error(err);
      server.disconnect();
      process.exit();
    });
  }));

267 268 269
program
  .command('show-keychain')
  .description('Recompute distance between each member and the whole WoT and mark outdistanced ones as kicked')
270
  .action(service(ucoin.createWOTServer, function (server, conf) {
271 272 273 274 275 276 277
    server.KeychainService.showKeychain(function (err) {
      if (err) logger.error(err);
      server.disconnect();
      process.exit();
    });
  }));

278 279 280
program
  .command('compute-certifications')
  .description('Recompute available certifications for next blocks')
281
  .action(service(ucoin.createWOTServer, function (server, conf) {
282 283 284 285 286 287 288
    server.KeychainService.updateCertifications(function (err) {
      if (err) logger.error(err);
      server.disconnect();
      process.exit();
    });
  }));

289 290
function generateAndSend (generationMethod) {
  return function (host, port, difficulty, server, conf) {
291 292
    async.waterfall([
      function (next){
293 294
        var method = eval('server.KeychainService.' + generationMethod);
        method(next);
295 296
      },
      function (block, next){
297
        logger.debug('Keyblock to be sent: %s', block.quickDescription());
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
        var wiz = wizard(server);
        async.waterfall([
          function (next){
            wiz.configOpenpgp(conf, next);
          },
          function (next){
            wiz.configKey(conf, next);
          },
          function (next){
            signature(conf.pgpkey, conf.pgppasswd, conf.openpgpjs, next);
          },
          function (sigFunc, next){
            proveAndSend(server, block, sigFunc, difficulty, host, port, next);
          },
        ], next);
      },
    ], function (err) {
      if (err) {
        logger.error(err);
      }
      server.disconnect();
      process.exit();
    });
321 322
  };
}
323

324 325 326 327 328 329 330 331 332 333 334
function proveAndSend (server, block, sigFunc, difficulty, host, port, done) {
  var KeychainService = server.KeychainService;
  async.waterfall([
    function (next){
      KeychainService.prove(block, sigFunc, difficulty, next);
    },
    function (block, next){
      var Peer = server.conn.model('Peer');
      var peer = new Peer({
        endpoints: [['BASIC_MERKLED_API', host, port].join(' ')]
      });
335
      logger.info('Posted block ' + block.quickDescription());
336 337 338 339 340
      multicaster().sendKeyblock(peer, block, next);
    },
  ], done);
}

341 342 343
program
  .command('check-config')
  .description('Checks the node\'s configuration')
344
  .action(service(ucoin.createPeerServer, function (server, conf) {
345 346 347 348 349 350 351
    server.checkConfig(function (err) {
      if (err)
        logger.warn(err);
      else
        logger.warn('Configuration seems correct.');
      server.disconnect();
      process.exit();
352 353 354 355
    });
    return;
  }));

356 357 358
program
  .command('config')
  .description('Register configuration in database')
359
  .action(service(function (server, conf) {
360
    conf.save(function (err) {
361
      if(err){
362
        logger.error("Configuration could not be saved: " + err);
363 364
      }
      else{
365
        logger.debug("Configuration saved.");
366
      }
367
      server.disconnect();
368
      process.exit();
369 370 371
      return;
    });
  }));
372

373 374 375
program
  .command('reset [config|data]')
  .description('Reset configuration or data in database')
376
  .action(service(function (type, server, conf) {
377
    if(!~['config', 'data'].indexOf(type)){
378
      logger.error('Bad command: usage `reset config` or `reset data`');
379
      server.disconnect();
380 381 382
      return;
    }
    if(type == 'data'){
383
      server.reset(function (err) {
384
        if(err)
385
          logger.error(err);
386 387 388
        else
          logger.warn('Data successfuly reseted.');
        server.disconnect();
389
        process.exit();
390 391 392
      });
    }
    if(type == 'config'){
393
      server.resetConf(function (err) {
394
        if(err)
395
          logger.error(err);
396 397 398
        else
          logger.warn('Configuration successfuly reseted.');
        server.disconnect();
399
        process.exit();
400 401 402 403
      });
    }
  }));

404 405 406
program
  .command('start')
  .description('Start uCoin server using given --currency')
407
  .action(service(ucoin.createPeerServer, function (server, conf) {
408

Cédric Moreau's avatar
Cédric Moreau committed
409 410 411
    server
      .pipe(router(server.PeeringService.cert.fingerprint, server.conn))
      .pipe(multicaster());
Cédric Moreau's avatar
Cédric Moreau committed
412

413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
    async.waterfall([
      function (next){
        server.checkConfig(function (err) {
          if (err) {
            // If required, launch config
            var wiz = wizard(server);
            async.waterfall([
              function (next){
                wiz.doTasks(['currency', 'openpgp', 'key', 'network'], conf, next);
              },
              function (next){
                conf.save(next);
              },
            ], function (err) {
              next(err);
            });
          }
          else next();
        });
      },
      function (next){
        // Launching server
        server.start(function (err, app) {
          next(err);
        });
      },
    ], function (err) {
440
      if(err){
441 442
        logger.error(err);
        server.disconnect();
443 444 445
        process.exit();
        return;
      }
446
      logger.debug('Server ready!');
447 448 449
    });
  }));

450
function overrideConf(conf) {
451

452 453
  conf.sync = conf.sync || {};
  var cli = {
454
    currency: program.currency,
455 456 457 458 459 460 461 462 463 464
    server: {
      port: program.port,
      ipv4address: program.ipv4,
      ipv6address: program.ipv6,
      pgp: {
        key: program.pgpkey,
        password: program.pgppasswd
      },
      remote: {
        host: program.remoteh,
465 466
        ipv4: program.remote4,
        ipv6: program.remote6,
467
        port: program.remotep
468 469
      },
      openpgpjs: program.openpgpjs
470 471 472 473 474
    },
    db: {
      host: program.mhost,
      port: program.mport,
      database: program.mdb,
475
    },
476 477
    policy: {
      keys: program.kmanagement,
Cédric Moreau's avatar
Cédric Moreau committed
478
      pubkeys: program.kaccept
479 480 481 482 483 484 485
    },
    ucp: {
      sigDelay:    program.sigDelay,
      sigValidity: program.sigValidity,
      sigQty:      program.sigQty,
      powZeroMin:  program.powZeroMin,
      powPeriod:   program.powPeriod,
486
      powDelay:   program.powDelay,
487 488
      particpate:  program.particpate,
      tsInterval:  program.tsInterval
489 490
    }
  };
491

492
  if(cli.server.pgp.key) cli.server.pgp.key = fs.readFileSync(cli.server.pgp.key, 'utf8');
493 494 495 496 497 498 499 500 501 502 503 504 505 506 507

  // Update conf
  if (cli.currency)                         conf.currency       = cli.currency;
  if (cli.server.ipv4address)               conf.ipv4           = cli.server.ipv4address;
  if (cli.server.ipv6address)               conf.ipv6           = cli.server.ipv6address;
  if (cli.server.port)                      conf.port           = cli.server.port;
  if (cli.server.openpgpjs != undefined)    conf.openpgpjs      = cli.server.openpgpjs;
  if (cli.server.pgp.key)                   conf.pgpkey         = cli.server.pgp.key;
  if (cli.server.pgp.password != undefined) conf.pgppasswd      = cli.server.pgp.password;
  if (cli.server.remote.host != undefined)  conf.remotehost     = cli.server.remote.host;
  if (cli.server.remote.ipv4 != undefined)  conf.remoteipv4     = cli.server.remote.ipv4;
  if (cli.server.remote.ipv6 != undefined)  conf.remoteipv6     = cli.server.remote.ipv6;
  if (cli.server.remote.port != undefined)  conf.remoteport     = cli.server.remote.port;
  if (cli.policy.keys)                      conf.kmanagement    = cli.policy.keys;
  if (cli.policy.pubkeys)                   conf.kaccept        = cli.policy.pubkeys;
508 509 510 511 512
  if (cli.ucp.sigDelay)                     conf.sigDelay       = cli.ucp.sigDelay;
  if (cli.ucp.sigValidity)                  conf.sigValidity    = cli.ucp.sigValidity;
  if (cli.ucp.sigQty)                       conf.sigQty         = cli.ucp.sigQty;
  if (cli.ucp.powZeroMin)                   conf.powZeroMin     = cli.ucp.powZeroMin;
  if (cli.ucp.powPeriod)                    conf.powPeriod      = cli.ucp.powPeriod;
513
  if (cli.ucp.powDelay)                     conf.powDelay       = cli.ucp.powDelay;
514 515
  if (cli.ucp.participate)                  conf.participate    = cli.ucp.participate == 'Y';
  if (cli.ucp.tsInterval)                   conf.tsInterval     = cli.ucp.tsInterval;
516 517 518

  // Specific internal settings
  conf.createNext = true;
519 520
  return conf;
}
521

522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
function connect(callback) {
  return function () {
    var cbArgs = arguments;
    var dbName = program.mdb || "ucoin_default";

    var server = ucoin.connect({ name: dbName, host: program.mhost, port: program.mport }, overrideConf({}));

    // Connecting to DB
    server.on('connected', function (err) {

      if(err){
        logger.warn(err);
        server.disconnect();
        process.exit(1);
        return;
      }

      cbArgs.length--;
      cbArgs[cbArgs.length++] = server;
      cbArgs[cbArgs.length++] = server.conf;
      callback.apply(this, cbArgs);
    });
  };
}

547
function service(serverFactory, callback) {
548 549
  if (arguments.length == 1) {
    callback = serverFactory;
550
    serverFactory = ucoin.createWOTServer;
551
  }
552 553
  return function () {
    var cbArgs = arguments;
554
    var dbName = program.mdb || "ucoin_default";
555

556
    var server = serverFactory({ name: dbName, host: program.mhost, port: program.mport }, overrideConf({}));
557

558
    // Connecting to DB
559
    server.on('services', function (err) {
560 561

      if(err){
562 563 564
        logger.warn(err);
        server.disconnect();
        process.exit(1);
565 566
        return;
      }
567

568
      cbArgs.length--;
569 570
      cbArgs[cbArgs.length++] = server;
      cbArgs[cbArgs.length++] = server.conf;
571 572 573 574
      callback.apply(this, cbArgs);
    });
  };
}
575

576 577 578
function parseMoment (d) {
  if (d.toLowerCase() == 'now') {
    return parseInt(moment().format("X"));
579
  } else if (d.match(/^\d{2}-\d{2}-\d{4}$/)) {
580
    return parseInt(moment(d, "DD-MM-YYYY").format("X"));
581 582
  } else if (d.match(/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}$/)) {
    return parseInt(moment(d, "DD-MM-YYYY hh:mm:ss").format("X"));
583 584 585 586 587
  } else {
    return parseInt(d);
  }
}

588 589 590 591
function splitColon (str) {
  return str.split(':');
}

592
program.parse(process.argv);