├── .github └── stale.yml ├── test ├── .gitignore ├── defaults.js ├── multiple-sbots.js ├── util.js ├── caps.js ├── bin.js └── data │ └── gossip.json ├── .gitignore ├── .travis.yml ├── lib ├── cli-cmd-aliases.js ├── progress.js └── validators.js ├── index.js ├── README.md ├── package.json ├── bin.js └── api.md /.github/stale.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | .privatekey 2 | .db 3 | .privatekey2 4 | .db2 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .db 2 | .privatekey 3 | node_modules 4 | .ssbrc 5 | *.log 6 | coverage 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 8 5 | - 6 6 | - 10 7 | -------------------------------------------------------------------------------- /lib/cli-cmd-aliases.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | feed: 'createFeedStream', 3 | history: 'createHistoryStream', 4 | hist: 'createHistoryStream', 5 | public: 'getPublicKey', 6 | pub: 'getPublicKey', 7 | log: 'createLogStream', 8 | logt: 'messagesByType', 9 | conf: 'config' 10 | } -------------------------------------------------------------------------------- /test/defaults.js: -------------------------------------------------------------------------------- 1 | //defaults only used in the tests! 2 | 3 | var crypto = require('crypto') 4 | function hash(s) { 5 | return crypto.createHash('sha256').update(s, 'utf8').digest() 6 | } 7 | 8 | module.exports = { 9 | caps: { 10 | shs: hash('test default shs'), 11 | sign: hash('test default sign'), 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /test/multiple-sbots.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | 3 | tape('createSsbServer method allows creating multiple servers with the same plugins', function (t) { 4 | var createSsbServer = require('../').createSsbServer 5 | 6 | var ssbServer1 = createSsbServer() 7 | .use(require('ssb-replicate')) 8 | 9 | var ssbServer2 = createSsbServer() 10 | .use(require('ssb-replicate')) 11 | .use(require('ssb-legacy-conn')) 12 | 13 | t.pass() 14 | t.end() 15 | }) 16 | 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var SecretStack = require('secret-stack') 2 | var caps = require('ssb-caps') 3 | var SSB = require('ssb-db') 4 | 5 | //create a sbot with default caps. these can be overridden again when you call create. 6 | function createSsbServer () { 7 | return SecretStack({ caps: { shs: Buffer.from(caps.shs, 'base64') } }).use(SSB) 8 | } 9 | module.exports = createSsbServer() 10 | 11 | //this isn't really needed anymore. 12 | module.exports.createSsbServer = createSsbServer 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | var ref = require('ssb-ref') 2 | 3 | exports.follow = function (id) { 4 | return { 5 | type: 'contact', contact: id, following: true 6 | } 7 | } 8 | exports.unfollow = function (id) { 9 | return { 10 | type: 'contact', contact: id, following: false 11 | } 12 | } 13 | exports.block = function unfollow(id) { 14 | return { 15 | type: 'contact', contact: id, flagged: true 16 | } 17 | } 18 | 19 | exports.pub = function (address) { 20 | return { 21 | type: 'pub', 22 | address: ref.parseAddress(address) 23 | } 24 | } 25 | 26 | exports.file = function (hash) { 27 | return { 28 | type: 'file', 29 | file: hash 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /lib/progress.js: -------------------------------------------------------------------------------- 1 | //poll the progress() function and report how much waiting there is... 2 | //just whipped this up, obviously room for improvement here. 3 | module.exports = function (progress) { 4 | function bar (r) { 5 | var s = '\r', M = 50 6 | for(var i = 0; i < M; i++) 7 | s += i < M*r ? '*' : '.' 8 | 9 | return s 10 | } 11 | 12 | function round (n, p) { 13 | return Math.round(n * p) / p 14 | } 15 | 16 | function percent (n) { 17 | return (round(n, 1000)*100).toString().substring(0, 4)+'%' 18 | } 19 | 20 | function rate (prog) { 21 | if(prog.target == prog.current) return 1 22 | return (prog.current - prog.start) / (prog.target - prog.start) 23 | } 24 | 25 | var prog = -1 26 | var int = setInterval(function () { 27 | var p = progress() 28 | var r = 1, c = 0 29 | var tasks = [] 30 | for(var k in p) { 31 | var _r = rate(p[k]) 32 | if(_r < 1) 33 | tasks.push(k+':'+percent(_r)) 34 | r = Math.min(_r, r) 35 | c++ 36 | } 37 | if(r != prog) { 38 | prog = r 39 | var msg = tasks.join(', ') 40 | process.stdout.write('\r'+bar(prog) + ' ('+msg+')\x1b[K\r') 41 | } 42 | }, 333) 43 | int.unref && int.unref() 44 | } 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssb-minimal-pub-server 2 | 3 | This is a friendly fork of 4 | [ssb-server](https://github.com/ssbc/ssb-server) aimed at a minimal 5 | core useful for pubs. 6 | 7 | This repo has the following changes compared to ssb-server: 8 | - [ssb-links](https://github.com/ssbc/ssb-links) has been 9 | removed. This module not used in the core, instead mostly used by 10 | external applications usch as patchfoo, git-ssb and ssb-npm. If 11 | needed can be installed as a plugin. 12 | - Use [ssb-legacy-conn](https://github.com/staltz/ssb-legacy-conn) 13 | instead of [ssb-gossip](https://github.com/ssbc/ssb-gossip). 14 | - Includes all the modules needed for 15 | [peer-invites](https://github.com/ssbc/ssb-peer-invites). 16 | 17 | In all other respects it behaves the same as ssb-server, so it should 18 | be an easy replacement. 19 | 20 | Furthermore this repo tries to stay as close to ssb-server as possible 21 | by merging in changes when new releases appear. This means that 15.0.1 22 | is the same as ssb-server 15.0.1 except the changes mentioned above. 23 | 24 | ## Install 25 | 26 | ``` 27 | npm install -g ssb-minimal-pub-server 28 | ``` 29 | 30 | Upon installation it is recommended that you supply a [device 31 | address](https://github.com/ssbc/ssb-device-address#usage) which 32 | enables nodes to use your pub for peer-invites. 33 | 34 | ## Usage 35 | 36 | Please refer to the [ssb-server documentation](https://github.com/ssbc/ssb-server#example-usage-bash). 37 | 38 | # License 39 | 40 | MIT 41 | -------------------------------------------------------------------------------- /test/caps.js: -------------------------------------------------------------------------------- 1 | var cont = require('cont') 2 | var deepEqual = require('deep-equal') 3 | var tape = require('tape') 4 | var pull = require('pull-stream') 5 | var ssbKeys = require('ssb-keys') 6 | 7 | var u = require('./util') 8 | 9 | // create 3 servers 10 | // give them all pub servers (on localhost) 11 | // and get them to follow each other... 12 | 13 | var createSsbServer = 14 | require('secret-stack')(require('./defaults')) 15 | .use(require('ssb-db')) 16 | .use(require('ssb-replicate')) 17 | .use(require('ssb-friends')) 18 | .use(require('ssb-legacy-conn')) 19 | .use(require('ssb-logging')) 20 | 21 | var createHash = require('crypto').createHash 22 | 23 | function hash (data) { 24 | return createHash('sha256').update(data, 'utf8').digest() 25 | } 26 | 27 | var sign_cap1 = hash('test-sign-cap1') 28 | var shs_cap1 = hash('test-shs-cap1') 29 | 30 | var alice, bob, carol 31 | var dbA = createSsbServer({ 32 | temp: 'server-alice', 33 | port: 45451, timeout: 1400, 34 | keys: alice = ssbKeys.generate(), 35 | caps: { 36 | shs: shs_cap1, 37 | sign: sign_cap1 38 | }, 39 | level: 'info' 40 | }) 41 | 42 | //uses default caps, incompatible with above 43 | var dbB = createSsbServer({ 44 | temp: 'server-bob', 45 | port: 45452, timeout: 1400, 46 | keys: bob = ssbKeys.generate(), 47 | seeds: [dbA.getAddress()], 48 | level: 'info' 49 | }) 50 | 51 | //can connect to A 52 | var dbC = createSsbServer({ 53 | temp: 'server-carol', 54 | port: 45453, timeout: 1400, 55 | keys: alice = ssbKeys.generate(), 56 | caps: { 57 | shs: shs_cap1, 58 | sign: sign_cap1 59 | }, 60 | level: 'info' 61 | }) 62 | 63 | 64 | tape('signatures not accepted if made from different caps', function (t) { 65 | 66 | 67 | dbA.publish({type: 'test', foo: true}, function (err, msg) { 68 | if(err) throw err 69 | console.log(msg) 70 | dbB.add(msg.value, function (err) { 71 | t.ok(err) //should not be valid in this universe 72 | t.ok(/invalid/.test(err.message)) 73 | console.log(err.stack) 74 | t.end() 75 | 76 | }) 77 | }) 78 | }) 79 | 80 | tape('cannot connect if different shs caps, custom -> default', function (t) { 81 | dbA.connect(dbB.getAddress(), function (err) { 82 | t.ok(err) 83 | console.log(err.stack) 84 | 85 | t.end() 86 | 87 | }) 88 | 89 | }) 90 | 91 | tape('cannot connect if different shs caps, default -> custom', function (t) { 92 | dbB.connect(dbA.getAddress(), function (err) { 93 | t.ok(err) 94 | 95 | console.log(err.stack) 96 | t.end() 97 | }) 98 | }) 99 | 100 | tape('cannot connect if different shs caps, default -> custom', function (t) { 101 | dbC.connect(dbA.getAddress(), function (err) { 102 | if(err) throw err 103 | t.end() 104 | }) 105 | }) 106 | 107 | 108 | tape('cleanup', function (t) { 109 | dbA.close() 110 | dbB.close() 111 | dbC.close() 112 | t.end() 113 | }) 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssb-minimal-pub-server", 3 | "description": "network protocol layer for secure-scuttlebutt", 4 | "version": "15.1.0", 5 | "homepage": "https://github.com/ssbc/ssb-minimal-pub-server", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/ssbc/ssb-minimal-pub-server.git" 9 | }, 10 | "bin": { 11 | "sbot": "./bin.js", 12 | "ssb-server": "./bin.js" 13 | }, 14 | "scripts": { 15 | "start": "node bin start", 16 | "prepublishOnly": "npm test", 17 | "test": "set -e; for t in test/*.js; do echo TEST $t; node $t; done && compatibility", 18 | "reinstall": "rm -rf node_modules package-lock.json npm-shrinkwrap.json; export npm_config_package_lock=true; npm install && npm dedupe && npm shrinkwrap", 19 | "coverage": "nyc --reporter=lcov npm test" 20 | }, 21 | "dependencies": { 22 | "bash-color": "~0.0.3", 23 | "broadcast-stream": "^0.2.1", 24 | "cont": "~1.0.3", 25 | "cross-spawn": "^6.0.5", 26 | "debug": "^4.1.1", 27 | "deep-equal": "^1.0.1", 28 | "explain-error": "^1.0.3", 29 | "has-network": "0.0.1", 30 | "ip": "^1.1.5", 31 | "minimist": "^1.1.3", 32 | "mkdirp": "~0.5.0", 33 | "multiblob": "^1.13.0", 34 | "multiserver": "^3.5.0", 35 | "multiserver-address": "^1.0.1", 36 | "muxrpc-validation": "^3.0.2", 37 | "muxrpcli": "^3.1.1", 38 | "mv": "^2.1.1", 39 | "osenv": "^0.1.5", 40 | "pull-cat": "~1.1.5", 41 | "pull-file": "^1.0.0", 42 | "pull-many": "~1.0.6", 43 | "pull-pushable": "^2.2.0", 44 | "pull-stream": "^3.6.13", 45 | "rimraf": "^2.4.2", 46 | "secret-stack": "^6.2.1", 47 | "ssb-blobs": "1.2.2", 48 | "ssb-caps": "1.0.1", 49 | "ssb-client": "^4.7.7", 50 | "ssb-config": "^3.3.1", 51 | "ssb-db": "19.2.0", 52 | "ssb-device-address": "^1.1.6", 53 | "ssb-ebt": "^5.6.7", 54 | "ssb-friends": "^4.1.4", 55 | "ssb-legacy-conn": "^1.0.25", 56 | "ssb-identities": "^2.1.0", 57 | "ssb-invite": "^2.1.2", 58 | "ssb-keys": "^7.1.7", 59 | "ssb-local": "^1.0.0", 60 | "ssb-logging": "^1.0.0", 61 | "ssb-master": "^1.0.3", 62 | "ssb-no-auth": "^1.0.0", 63 | "ssb-onion": "^1.0.0", 64 | "ssb-ooo": "^1.3.0", 65 | "ssb-peer-invites": "^2.0.0", 66 | "ssb-plugins": "^1.0.2", 67 | "ssb-query": "^2.4.2", 68 | "ssb-ref": "^2.13.9", 69 | "ssb-replicate": "^1.3.0", 70 | "ssb-unix-socket": "^1.0.0", 71 | "ssb-ws": "^6.2.3", 72 | "stream-to-pull-stream": "^1.6.10", 73 | "zerr": "^1.0.0" 74 | }, 75 | "devDependencies": { 76 | "@types/node": "^11.13.4", 77 | "cat-names": "^3.0.0", 78 | "compatibility": "^1.0.1", 79 | "dog-names": "^2.0.0", 80 | "hexpp": "^2.0.0", 81 | "interleavings": "^1.0.0", 82 | "npm-install-package": "^2.1.0", 83 | "nyc": "^13.2.0", 84 | "pull-abortable": "^4.1.1", 85 | "pull-bitflipper": "^0.1.1", 86 | "pull-paramap": "^1.2.2", 87 | "rng": "^0.2.2", 88 | "run-series": "^1.1.8", 89 | "semver": "^5.6.0", 90 | "ssb-feed": "^2.3.0", 91 | "ssb-generate": "^1.0.1", 92 | "ssb-gossip": "^1.1.0", 93 | "ssb-validate": "^4", 94 | "tape": "^4.9.1", 95 | "typescript": "^3.3.4000", 96 | "typewiselite": "^1.0.0" 97 | }, 98 | "nyc": { 99 | "exclude": [ 100 | "!**/node_modules/", 101 | "**/node_modules/**/{test,node_modules}/" 102 | ], 103 | "include": [ 104 | "*.js", 105 | "lib/*.js", 106 | "plugins/*.js", 107 | "node_modules/{ssb-*,flume*,secret-stack,muxrpc,multiserver}/*.js", 108 | "node_modules/{ssb-*,flume*,secret-stack,muxrpc,multiserver}/{util,lib,frame,indexes}/*.js" 109 | ] 110 | }, 111 | "compatibility": [ 112 | "ssb-friends", 113 | "ssb-blobs", 114 | "ssb-ebt", 115 | "ssb-db", 116 | "ssb-ooo", 117 | "ssb-plugins" 118 | ], 119 | "author": "Paul Frazee ", 120 | "license": "MIT" 121 | } 122 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var fs = require('fs') 4 | var path = require('path') 5 | var pull = require('pull-stream') 6 | var toPull = require('stream-to-pull-stream') 7 | var File = require('pull-file') 8 | var explain = require('explain-error') 9 | var Config = require('ssb-config/inject') 10 | var Client = require('ssb-client') 11 | var createHash = require('multiblob/util').createHash 12 | var minimist = require('minimist') 13 | var muxrpcli = require('muxrpcli') 14 | var cmdAliases = require('./lib/cli-cmd-aliases') 15 | var ProgressBar = require('./lib/progress') 16 | var packageJson = require('./package.json') 17 | 18 | //get config as cli options after --, options before that are 19 | //options to the command. 20 | var argv = process.argv.slice(2) 21 | var i = argv.indexOf('--') 22 | var conf = argv.slice(i+1) 23 | argv = ~i ? argv.slice(0, i) : argv 24 | 25 | var config = Config(process.env.ssb_appname, minimist(conf)) 26 | 27 | if (config.keys.curve === 'k256') 28 | throw new Error('k256 curves are no longer supported,'+ 29 | 'please delete' + path.join(config.path, 'secret')) 30 | 31 | var manifestFile = path.join(config.path, 'manifest.json') 32 | 33 | if (argv[0] == 'server') { 34 | console.log('WARNING-DEPRECATION: `sbot server` has been renamed to `ssb-server start`') 35 | argv[0] = 'start' 36 | } 37 | 38 | if (argv[0] == 'start') { 39 | console.log(packageJson.name, packageJson.version, config.path, 'logging.level:'+config.logging.level) 40 | console.log('my key ID:', config.keys.public) 41 | 42 | // special start command: 43 | // import ssbServer and start the server 44 | 45 | var createSsbServer = require('./') 46 | .use(require('ssb-onion')) 47 | .use(require('ssb-unix-socket')) 48 | .use(require('ssb-no-auth')) 49 | .use(require('ssb-plugins')) 50 | .use(require('ssb-master')) 51 | .use(require('ssb-legacy-conn')) 52 | .use(require('ssb-replicate')) 53 | .use(require('ssb-friends')) 54 | .use(require('ssb-blobs')) 55 | .use(require('ssb-invite')) 56 | .use(require('ssb-local')) 57 | .use(require('ssb-logging')) 58 | .use(require('ssb-query')) 59 | .use(require('ssb-ws')) 60 | .use(require('ssb-ebt')) 61 | .use(require('ssb-ooo')) 62 | .use(require('ssb-device-address')) 63 | .use(require('ssb-identities')) 64 | .use(require('ssb-peer-invites')) 65 | // add third-party plugins 66 | 67 | require('ssb-plugins').loadUserPlugins(createSsbServer, config) 68 | 69 | // start server 70 | var server = createSsbServer(config) 71 | 72 | // write RPC manifest to ~/.ssb/manifest.json 73 | fs.writeFileSync(manifestFile, JSON.stringify(server.getManifest(), null, 2)) 74 | 75 | if(process.stdout.isTTY && (config.logging.level != 'info')) 76 | ProgressBar(server.progress) 77 | } else { 78 | // normal command: 79 | // create a client connection to the server 80 | 81 | // read manifest.json 82 | var manifest 83 | try { 84 | manifest = JSON.parse(fs.readFileSync(manifestFile)) 85 | } catch (err) { 86 | throw explain(err, 87 | 'no manifest file' 88 | + '- should be generated first time server is run' 89 | ) 90 | } 91 | 92 | var opts = { 93 | manifest: manifest, 94 | port: config.port, 95 | host: config.host || 'localhost', 96 | caps: config.caps, 97 | key: config.key || config.keys.id 98 | } 99 | 100 | // connect 101 | Client(config.keys, opts, function (err, rpc) { 102 | if(err) { 103 | if (/could not connect/.test(err.message)) { 104 | console.error('Error: Could not connect to ssb-server ' + opts.host + ':' + opts.port) 105 | console.error('Use the "start" command to start it.') 106 | console.error('Use --verbose option to see full error') 107 | if(config.verbose) throw err 108 | process.exit(1) 109 | } 110 | throw err 111 | } 112 | 113 | // add aliases 114 | for (var k in cmdAliases) { 115 | rpc[k] = rpc[cmdAliases[k]] 116 | manifest[k] = manifest[cmdAliases[k]] 117 | } 118 | 119 | // add some extra commands 120 | // manifest.version = 'async' 121 | manifest.config = 'sync' 122 | // rpc.version = function (cb) { 123 | // console.log(packageJson.version) 124 | // cb() 125 | // } 126 | rpc.config = function (cb) { 127 | console.log(JSON.stringify(config, null, 2)) 128 | cb() 129 | } 130 | 131 | // HACK 132 | // we need to output the hash of blobs that are added via blobs.add 133 | // because muxrpc doesnt support the `sink` callback yet, we need this manual override 134 | // -prf 135 | if (process.argv[2] === 'blobs.add') { 136 | var filename = process.argv[3] 137 | var source = 138 | filename ? File(process.argv[3]) 139 | : !process.stdin.isTTY ? toPull.source(process.stdin) 140 | : (function () { 141 | console.error('USAGE:') 142 | console.error(' blobs.add # add a file') 143 | console.error(' source | blobs.add # read from stdin') 144 | process.exit(1) 145 | })() 146 | var hasher = createHash('sha256') 147 | pull( 148 | source, 149 | hasher, 150 | rpc.blobs.add(function (err) { 151 | if (err) 152 | throw err 153 | console.log('&'+hasher.digest) 154 | process.exit() 155 | }) 156 | ) 157 | return 158 | } 159 | 160 | // run commandline flow 161 | muxrpcli(argv, manifest, rpc, config.verbose) 162 | }) 163 | } 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /lib/validators.js: -------------------------------------------------------------------------------- 1 | var valid = require('muxrpc-validation') 2 | var zerr = require('zerr') 3 | var ref = require('ssb-ref') 4 | 5 | // errors 6 | var MissingAttr = zerr('Usage', 'Param % must have a .% of type "%"') 7 | var AttrType = zerr('Usage', '.% of param % must be of type "%"') 8 | 9 | function isFilter (v) { 10 | return (v == '@' || v == '%' || v == '&') 11 | } 12 | 13 | module.exports = valid({ 14 | msgId: function (v) { 15 | if (!ref.isMsg(v)) 16 | return 'type' 17 | }, 18 | msgLink: function (v) { 19 | if (!ref.isMsgLink(v)) 20 | return 'type' 21 | }, 22 | feedId: function (v) { 23 | if (!ref.isFeed(v)) 24 | return 'type' 25 | }, 26 | blobId: function (v) { 27 | if (!ref.isBlob(v)) 28 | return 'type' 29 | }, 30 | 31 | msgContent: function (v, n) { 32 | var err = this.get('object')(v, n) 33 | if (err) return err 34 | if (!v.type || typeof v.type != 'string') 35 | return MissingAttr(n, 'type', 'string') 36 | }, 37 | 38 | msg: function (v, n) { 39 | var err = this.get('object')(v, n) 40 | if (err) 41 | return err 42 | 43 | //allow content to be string. (i.e. for encrypted messages) 44 | //or object with type string 45 | if(!v.content) 46 | return MissingAttr(n, 'content', 'object|string') 47 | else if(typeof v.content === 'string') 48 | ; //check if it's base64? 49 | else if('object' === typeof v.content) { 50 | if(!v.content.type || typeof v.content.type != 'string') 51 | return MissingAttr(n, 'content.type', 'string') 52 | } 53 | else 54 | return MissingAttr(n, 'content', 'object|string') 55 | 56 | // .author 57 | if (!ref.isFeed(v.author)) 58 | return MissingAttr(n, 'author', 'feedId') 59 | 60 | // .sequence 61 | if (typeof v.sequence != 'number') 62 | return MissingAttr(n, 'sequence', 'number') 63 | 64 | // .previous 65 | if (v.sequence > 1 && !ref.isMsg(v.previous)) 66 | return MissingAttr(n, 'previous', 'msgId') 67 | else if(v.sequence == 1 && v.previous != null) 68 | return MissingAttr(n, 'previous', 'null') 69 | 70 | // .timestamp 71 | if (typeof v.timestamp != 'number') 72 | return MissingAttr(n, 'timestamp', 'number') 73 | 74 | // .hash 75 | if (v.hash != 'sha256') 76 | return zerr('Usage', 'Param % must have .hash set to "sha256"')(n) 77 | 78 | // .signature 79 | if (typeof v.signature != 'string') 80 | return MissingAttr(n, 'signature', 'string') 81 | }, 82 | 83 | readStreamOpts: function (v, n) { 84 | var err = this.get('object')(v, n) 85 | if (err) 86 | return err 87 | 88 | // .live 89 | if (v.live && typeof v.live != 'boolean' && typeof v.live != 'number') 90 | return AttrType(n, 'live', 'boolean') 91 | 92 | // .reverse 93 | if (v.reverse && typeof v.reverse != 'boolean' && typeof v.reverse != 'number') 94 | return AttrType(n, 'reverse', 'boolean') 95 | 96 | // .keys 97 | if (v.keys && typeof v.keys != 'boolean' && typeof v.keys != 'number') 98 | return AttrType(n, 'keys', 'boolean') 99 | 100 | // .values 101 | if (v.values && typeof v.values != 'boolean' && typeof v.values != 'number') 102 | return AttrType(n, 'values', 'boolean') 103 | 104 | // .limit 105 | if (v.limit && typeof v.limit != 'number') 106 | return AttrType(n, 'limit', 'number') 107 | 108 | // .fillCache 109 | if (v.fillCache && typeof v.fillCache != 'boolean' && typeof v.fillCache != 'number') 110 | return AttrType(n, 'fillCache', 'boolean') 111 | }, 112 | 113 | createHistoryStreamOpts: function (v, n) { 114 | // .id 115 | if (!ref.isFeed(v.id)) 116 | return MissingAttr(n, 'id', 'feedId') 117 | 118 | // .seq 119 | if (v.seq && typeof v.seq != 'number') 120 | return AttrType(n, 'seq', 'number') 121 | 122 | // .live 123 | if (v.live && typeof v.live != 'boolean' && typeof v.live != 'number') 124 | return AttrType(n, 'live', 'boolean') 125 | 126 | // .limit 127 | if (v.limit && typeof v.limit != 'number') 128 | return AttrType(n, 'limit', 'number') 129 | 130 | // .keys 131 | if (v.keys && typeof v.keys != 'boolean' && typeof v.keys != 'number') 132 | return AttrType(n, 'keys', 'boolean') 133 | 134 | // .values 135 | if (v.values && typeof v.values != 'boolean' && typeof v.values != 'number') 136 | return AttrType(n, 'values', 'boolean') 137 | }, 138 | 139 | createUserStreamOpts: function (v, n) { 140 | var err = this.get('readStreamOpts')(v, n) 141 | if (err) 142 | return err 143 | 144 | // .id 145 | if (!ref.isFeed(v.id)) 146 | return MissingAttr(n, 'id', 'feedId') 147 | }, 148 | 149 | messagesByTypeOpts: function (v, n) { 150 | var err = this.get('readStreamOpts')(v, n) 151 | if (err) 152 | return err 153 | 154 | // .type 155 | if (typeof v.type != 'string') 156 | return MissingAttr(n, 'type', 'string') 157 | }, 158 | 159 | linksOpts: function (v, n) { 160 | var err = this.get('object')(v, n) 161 | if (err) 162 | return err 163 | 164 | // .source 165 | if (v.source && !ref.isLink(v.source) && !isFilter(v.source)) 166 | return AttrType(n, 'source', 'id|filter') 167 | 168 | // .dest 169 | if (v.dest && !ref.isLink(v.dest) && !isFilter(v.dest)) 170 | return AttrType(n, 'dest', 'id|filter') 171 | 172 | // .rel 173 | if (v.rel && typeof v.rel != 'string') 174 | return AttrType(n, 'rel', 'string') 175 | 176 | // .live 177 | if (v.live && typeof v.live != 'boolean' && typeof v.live != 'number') 178 | return AttrType(n, 'live', 'boolean') 179 | 180 | // .reverse 181 | if (v.reverse && typeof v.reverse != 'boolean' && typeof v.reverse != 'number') 182 | return AttrType(n, 'reverse', 'boolean') 183 | 184 | // .keys 185 | if (v.keys && typeof v.keys != 'boolean' && typeof v.keys != 'number') 186 | return AttrType(n, 'keys', 'boolean') 187 | 188 | // .values 189 | if (v.values && typeof v.values != 'boolean' && typeof v.values != 'number') 190 | return AttrType(n, 'values', 'boolean') 191 | }, 192 | 193 | isBlockedOpts: function (v, n) { 194 | var err = this.get('object')(v, n) 195 | if (err) 196 | return err 197 | 198 | // .source 199 | if (v.source && !ref.isFeed(v.source)) 200 | return AttrType(n, 'source', 'feedId') 201 | 202 | // .dest 203 | if (v.dest && !ref.isFeed(v.dest)) 204 | return AttrType(n, 'dest', 'feedId') 205 | }, 206 | 207 | createFriendStreamOpts: function (v, n) { 208 | var err = this.get('object')(v, n) 209 | if (err) 210 | return err 211 | 212 | // .start 213 | if (v.start && !ref.isFeed(v.start)) 214 | return AttrType(n, 'start', 'feedId') 215 | 216 | // .graph 217 | if (v.graph && typeof v.graph != 'string') 218 | return AttrType(n, 'graph', 'string') 219 | 220 | // .dunbar 221 | if (v.dunbar && typeof v.dunbar != 'number') 222 | return AttrType(n, 'dunbar', 'number') 223 | 224 | // .hops 225 | if (v.hops && typeof v.hops != 'number') 226 | return AttrType(n, 'hops', 'number') 227 | } 228 | }) 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /test/bin.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var test = require('tape') 3 | var spawn = require('child_process').spawn 4 | var exec = require('child_process').exec 5 | var crypto = require('crypto') 6 | var net = require('net') 7 | var mkdirp = require('mkdirp') 8 | var join = require('path').join 9 | var ma = require('multiserver-address') 10 | 11 | // travis currently does not support ipv6, becaue GCE does not. 12 | var has_ipv6 = process.env.TRAVIS === undefined 13 | var children = [] 14 | 15 | process.on('exit', function () { 16 | children.forEach(function (e) { 17 | e.kill('SIGKILL') 18 | }) 19 | }) 20 | process.on('SIGINT', function () { 21 | children.forEach(function (e) { 22 | e.kill('SIGKILL') 23 | }) 24 | process.exit(1) 25 | }) 26 | 27 | var exited = false 28 | var count = 0 29 | function ssbServer(t, argv, opts) { 30 | count ++ 31 | exited = false 32 | opts = opts || {} 33 | 34 | var sh = spawn( 35 | process.execPath, 36 | [join(__dirname, '../bin.js')] 37 | .concat(argv), 38 | Object.assign({ 39 | env: Object.assign({}, process.env, {ssb_appname: 'test'}), 40 | }, opts) 41 | ) 42 | 43 | sh.once('exit', function (code, name) { 44 | exited = true 45 | t.equal(name,'SIGKILL') 46 | if(--count) return 47 | t.end() 48 | }) 49 | 50 | sh.stdout.pipe(process.stdout) 51 | sh.stderr.pipe(process.stderr) 52 | 53 | children.push(sh) 54 | 55 | return function end () { 56 | while(children.length) children.shift().kill('SIGKILL') 57 | } 58 | } 59 | 60 | function try_often(times, opts, work, done) { 61 | if (typeof opts == 'function') { 62 | done = work 63 | work = opts 64 | opts = {} 65 | } 66 | const delay = 2000 67 | setTimeout(function() { // delay first try 68 | console.log('try more:', times) 69 | work(function(err, result) { 70 | if (!err) return done(null, result) 71 | if (opts.ignore && err.message && !err.message.match(opts.ignore)) { 72 | console.error('Fatal error:', err) 73 | return done(err) 74 | } 75 | if (!times) return done(err) 76 | if(exited) return done(new Error('already exited')) 77 | console.warn('retry run', times) 78 | console.error('work(err):', err) 79 | try_often(times-1, work, done) 80 | }) 81 | }, delay) 82 | } 83 | 84 | function connect(port, host, cb) { 85 | var done = false 86 | var socket = net.connect(port, host) 87 | socket.on('error', function(err) { 88 | if (done) return 89 | done = true 90 | cb(err) 91 | }) 92 | socket.on('connect', function() { 93 | if (done) return 94 | done = true 95 | cb(null) 96 | }) 97 | } 98 | 99 | function testSsbServer(t, opts, asConfig, port, cb) { 100 | var dir = '/tmp/ssb-server_binjstest_' + Date.now() 101 | if('function' === typeof port) 102 | cb = port, port = opts.port 103 | mkdirp.sync(dir) 104 | var args = [ 105 | 'start', 106 | '--path '+dir 107 | ] 108 | 109 | if(asConfig) { 110 | fs.writeFileSync(join(dir, '.testrc'), JSON.stringify(opts)) 111 | } else { 112 | ;(function toArgs (prefix, opts) { 113 | for(var k in opts) { 114 | if(opts[k] && 'object' == typeof opts[k]) 115 | toArgs(prefix+k+'.', opts[k]) 116 | else 117 | args.push(prefix+k+'='+opts[k]) 118 | } 119 | })('--', opts) 120 | } 121 | 122 | var end = ssbServer(t, args, { 123 | cwd: dir 124 | }) 125 | 126 | try_often(10, { 127 | ignore: /ECONNREFUSED/ 128 | }, function work(cb) { 129 | connect(port, opts.host, cb) 130 | }, function (err) { 131 | cb(err) 132 | end() 133 | }) 134 | } 135 | 136 | var c = 0 137 | ;['::1', '::', '127.0.0.1', 'localhost'].forEach(function (host) { 138 | if(!has_ipv6 && /:/.test(host)) return 139 | 140 | ;[9002, 9001].forEach(function (port) { 141 | ;[true, false].forEach(function (asConfig) { 142 | var opts = { 143 | host: host, 144 | port: 9001, 145 | ws: { port: 9002 } 146 | } 147 | // if(c++) return 148 | test('run bin.js server with ' + 149 | (asConfig ? 'a config file' : 'command line options') + 150 | ':'+JSON.stringify(opts)+' then connect to port:'+port 151 | , function(t) { 152 | testSsbServer(t, opts, true, function (err) { 153 | t.error(err, 'Successfully connect eventually') 154 | }) 155 | }) 156 | }) 157 | }) 158 | }) 159 | 160 | test('ssbServer should have websockets and http server by default', function(t) { 161 | var path = '/tmp/ssbServer_binjstest_' + Date.now() 162 | var caps = crypto.randomBytes(32).toString('base64') 163 | var end = ssbServer(t, [ 164 | 'start', 165 | '--host=127.0.0.1', 166 | '--port=9001', 167 | '--ws.port=9002', 168 | '--path', path, 169 | '--caps.shs', caps 170 | ]) 171 | 172 | try_often(10, function work(cb) { 173 | exec([ 174 | join(__dirname, '../bin.js'), 175 | 'getAddress', 176 | 'device', 177 | '--', 178 | '--host=127.0.0.1', 179 | '--port=9001', 180 | '--path', path, 181 | '--caps.shs', caps 182 | ].join(' '), { 183 | env: Object.assign({}, process.env, {ssb_appname: 'test'}) 184 | }, function(err, stdout, sderr) { 185 | if (err) return cb(err) 186 | cb(null, JSON.parse(stdout)) // remove quotes 187 | }) 188 | }, function(err, addr) { 189 | t.error(err, 'ssbServer getAdress succeeds eventually') 190 | if (err) return end() 191 | t.ok(addr, 'address is not null') 192 | t.comment('result of ssb-server getAddress: ' + addr) 193 | 194 | var remotes = ma.decode(addr) 195 | console.log('remotes', remotes, addr) 196 | ws_remotes = remotes.filter(function(a) { 197 | return a.find(function(component) { 198 | return component.name == 'ws' 199 | }) 200 | }) 201 | t.equal(ws_remotes.length, 1, 'has one ws remote') 202 | var remote = ma.encode([ws_remotes[0]]) 203 | // this breaks if multiserver address encoding changes 204 | t.ok(remote.indexOf('9002') > 0, 'ws address contains expected port') 205 | 206 | // this is a bit annoying. we can't signal ssb-client to load the secret from .path 207 | // it either has to be the first argument, already loaded 208 | var key = require('ssb-keys').loadOrCreateSync(join(path, 'secret')) 209 | require('ssb-client')(key, { 210 | path: path, 211 | caps: { shs: caps }, // has to be set when setting any config 212 | remote: remote 213 | }, function(err, ssb) { 214 | t.error(err, 'ssb-client returns no error') 215 | t.ok(ssb.manifest, 'got manifest from api') 216 | t.ok(ssb.version, 'got version from api') 217 | ssb.whoami(function(err, feed) { 218 | t.error(err, 'ssb.whoami succeeds') 219 | t.equal(feed.id[0], '@', 'feed.id has @ sigil') 220 | end() 221 | }) 222 | }) 223 | }) 224 | }) 225 | 226 | test('ssb-server client should work without options', function(t) { 227 | var path = '/tmp/ssb-server_binjstest_' + Date.now() 228 | mkdirp.sync(path) 229 | fs.writeFileSync(path+'/config', 230 | JSON.stringify({ 231 | port: 43293, ws: { port: 43294 } 232 | }) 233 | ) 234 | var caps = crypto.randomBytes(32).toString('base64') 235 | var end = ssbServer(t, [ 236 | 'start', 237 | '--path', path, 238 | '--config', path+'/config', 239 | '--caps.shs', caps 240 | ]) 241 | 242 | try_often(10, function work(cb) { 243 | exec([ 244 | join(__dirname, '../bin.js'), 245 | 'getAddress', 246 | 'device', 247 | '--path', path, 248 | '--config', path+'/config', 249 | '--caps.shs', caps 250 | ].join(' '), { 251 | env: Object.assign({}, process.env, {ssb_appname: 'test'}) 252 | }, function(err, stdout, sderr) { 253 | if (err) return cb(err) 254 | cb(null, JSON.parse(stdout)) // remove quotes 255 | }) 256 | }, function(err, addr) { 257 | t.error(err, 'ssb-server getAddress succeeds eventually') 258 | if (err) return end() 259 | t.ok(addr) 260 | 261 | t.comment('result of ssb-server getAddress: ' + addr) 262 | end() 263 | }) 264 | }) 265 | 266 | 267 | -------------------------------------------------------------------------------- /test/data/gossip.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts": 1458983392551, 3 | "peers":[ 4 | { 5 | "host": "188.166.252.233", 6 | "port": 8008, 7 | "key": "@uRECWB4KIeKoNMis2UYWyB2aQPvWmS3OePQvBj2zClg=.ed25519", 8 | "source": "pub", 9 | "announcers": 73, 10 | "duration": { 11 | "mean": 31904.760869565216, 12 | "count": 46, 13 | "stdev": 4283.4027066299295 14 | }, 15 | "client": true, 16 | "time": { 17 | "connect": 1458983141389, 18 | "hangup": 1458983172624, 19 | "attempt": 1458983106552 20 | }, 21 | "stateChange": 1458983172624, 22 | "ping": { 23 | "rtt": { 24 | "mean": 0, 25 | "count": 0, 26 | "stdev": null 27 | }, 28 | "skew": { 29 | "mean": 0, 30 | "count": 0, 31 | "stdev": null 32 | } 33 | }, 34 | "active": true, 35 | "failure": 0 36 | }, 37 | { 38 | "host": "176.58.117.63", 39 | "port": 8008, 40 | "key": "@J+0DGLgRn8H5tVLCcRUfN7NfUcTGEZKqML3krEOJjDY=.ed25519", 41 | "source": "pub", 42 | "announcers": 61, 43 | "duration": { 44 | "mean": 0, 45 | "count": 1, 46 | "stdev": 0 47 | }, 48 | "time": { 49 | "attempt": 1458953075788, 50 | "hangup": 1458953080790 51 | }, 52 | "stateChange": 1458953080790, 53 | "active": false, 54 | "failure": 1 55 | }, 56 | { 57 | "host": "88.198.115.222", 58 | "port": 8008, 59 | "key": "@im4Qn0fCzpD3YfsegHFLJzkNXYUb/nYnlfuCf+LmPuM=.ed25519", 60 | "source": "pub", 61 | "announcers": 20, 62 | "duration": { 63 | "mean": 0, 64 | "count": 4, 65 | "stdev": 0 66 | }, 67 | "time": { 68 | "attempt": 1458953251559, 69 | "hangup": 1458953251864 70 | }, 71 | "stateChange": 1458953251864, 72 | "active": false, 73 | "failure": 4 74 | }, 75 | { 76 | "port": 8008, 77 | "key": "@J+0DGLgRn8H5tVLCcRUfN7NfUcTGEZKqML3krEOJjDY=.ed25519", 78 | "host": "localhost", 79 | "source": "pub", 80 | "announcers": 2, 81 | "duration": { 82 | "mean": 0, 83 | "count": 1, 84 | "stdev": 0 85 | }, 86 | "time": { 87 | "attempt": 1458953201000, 88 | "hangup": 1458953201006 89 | }, 90 | "stateChange": 1458953201006, 91 | "active": false, 92 | "failure": 1 93 | }, 94 | { 95 | "host": "131.72.139.47", 96 | "port": 8008, 97 | "key": "@V+sfZ+X/MSUb+cO3GVI4gn1cCVVwz1+t6B+J9DlUkQs=.ed25519", 98 | "source": "pub", 99 | "announcers": 36, 100 | "duration": { 101 | "mean": 32998.13636363636, 102 | "count": 22, 103 | "stdev": 5928.972188219304 104 | }, 105 | "time": { 106 | "attempt": 1458983355392, 107 | "connect": 1458983356298, 108 | "hangup": 1458983280114 109 | }, 110 | "stateChange": 1458983356298, 111 | "state": "connected", 112 | "client": true, 113 | "ping": { 114 | "rtt": { 115 | "mean": 0, 116 | "count": 0, 117 | "stdev": null 118 | }, 119 | "skew": { 120 | "mean": 0, 121 | "count": 0, 122 | "stdev": null 123 | } 124 | }, 125 | "active": true, 126 | "failure": 0 127 | }, 128 | { 129 | "host": "24.75.24.253", 130 | "port": 8008, 131 | "key": "@JMnMSsDHjwZfUlC2J8ZiIAOoxM5KJfsvewmVe39/wSM=.ed25519", 132 | "source": "pub", 133 | "announcers": 7, 134 | "duration": { 135 | "mean": 0, 136 | "count": 1, 137 | "stdev": 0 138 | }, 139 | "time": { 140 | "attempt": 1458953190929, 141 | "hangup": 1458953195931 142 | }, 143 | "stateChange": 1458953195931, 144 | "active": false, 145 | "failure": 1 146 | }, 147 | { 148 | "host": "45.33.29.124", 149 | "port": 8008, 150 | "key": "@0GLMsG6IgXdv+GjG0U5UnZlwxHnomlfmrlWugx8i4dg=.ed25519", 151 | "source": "pub", 152 | "announcers": 28, 153 | "duration": { 154 | "mean": 0, 155 | "count": 2, 156 | "stdev": 0 157 | }, 158 | "time": { 159 | "attempt": 1458957207322, 160 | "hangup": 1458957208010 161 | }, 162 | "stateChange": 1458957208010, 163 | "active": false, 164 | "failure": 2 165 | }, 166 | { 167 | "host": "74.207.246.247", 168 | "port": 8008, 169 | "key": "@omgyp7Pnrw+Qm0I6T6Fh5VvnKmodMXwnxTIesW2DgMg=.ed25519", 170 | "source": "pub", 171 | "announcers": 9, 172 | "duration": { 173 | "mean": 0, 174 | "count": 3, 175 | "stdev": 0 176 | }, 177 | "time": { 178 | "attempt": 1458971210200, 179 | "hangup": 1458971215201 180 | }, 181 | "stateChange": 1458971215201, 182 | "active": false, 183 | "failure": 3 184 | }, 185 | { 186 | "host": "188.166.107.197", 187 | "port": 8008, 188 | "key": "@/RM1Id8j05uitIt6iwMpiivnCqHcbcC1IHyi5FrvLLQ=.ed25519", 189 | "source": "pub", 190 | "announcers": 7, 191 | "duration": { 192 | "mean": 0, 193 | "count": 1, 194 | "stdev": 0 195 | }, 196 | "time": { 197 | "attempt": 1458971090648, 198 | "hangup": 1458971090943 199 | }, 200 | "stateChange": 1458971090943, 201 | "active": false, 202 | "failure": 1 203 | }, 204 | { 205 | "host": "24.75.24.253", 206 | "port": 8008, 207 | "key": "@WzvzUh2KMhAfWeJEpkI01BGlTtLXOei7GoB/dLVjcjE=.ed25519", 208 | "source": "pub", 209 | "announcers": 3, 210 | "duration": { 211 | "mean": 0, 212 | "count": 2, 213 | "stdev": 0 214 | }, 215 | "time": { 216 | "attempt": 1458953330661, 217 | "hangup": 1458953335663 218 | }, 219 | "stateChange": 1458953335663, 220 | "active": false, 221 | "failure": 2 222 | }, 223 | { 224 | "host": "9ithub.com", 225 | "port": 8008, 226 | "key": "@GLH9VPzvvU2KcnnUu2n5oxOqaTUtzw+Rk6fd/Kb9Si0=.ed25519", 227 | "source": "pub", 228 | "announcers": 3, 229 | "duration": { 230 | "mean": 0, 231 | "count": 2, 232 | "stdev": 0 233 | }, 234 | "time": { 235 | "attempt": 1458971894621, 236 | "hangup": 1458971899623 237 | }, 238 | "stateChange": 1458971899623, 239 | "active": false, 240 | "failure": 2 241 | }, 242 | { 243 | "host": "9ithub.com", 244 | "port": 8008, 245 | "key": "@D0GsAaMyt96Ze3q1YiiuzWhPkyou2fVTUgw8Xr+G7Jo=.ed25519", 246 | "source": "pub", 247 | "announcers": 6, 248 | "duration": { 249 | "mean": 0, 250 | "count": 1, 251 | "stdev": 0 252 | }, 253 | "time": { 254 | "attempt": 1458961886104, 255 | "hangup": 1458961891107 256 | }, 257 | "stateChange": 1458961891107, 258 | "active": false, 259 | "failure": 1 260 | }, 261 | { 262 | "host": "104.236.72.67", 263 | "port": 8008, 264 | "key": "@xBZ783+eCc9+vc/CHxR03y1nAcfULJUAOgd3zWsy1uY=.ed25519", 265 | "source": "pub", 266 | "announcers": 6, 267 | "duration": { 268 | "mean": 0, 269 | "count": 4, 270 | "stdev": 0 271 | }, 272 | "time": { 273 | "attempt": 1458953376539, 274 | "hangup": 1458953376759 275 | }, 276 | "stateChange": 1458953376759, 277 | "active": false, 278 | "failure": 4 279 | }, 280 | { 281 | "host": "newpi.ffhh", 282 | "port": 8080, 283 | "key": "@gYCJpN4eGDjHFnWW2Fcusj8O4QYbVDUW6rNYh7nNEnc=.ed25519", 284 | "source": "pub", 285 | "announcers": 2, 286 | "duration": { 287 | "mean": 0, 288 | "count": 1, 289 | "stdev": 0 290 | }, 291 | "time": { 292 | "attempt": 1458950587731, 293 | "hangup": 1458950588413 294 | }, 295 | "stateChange": 1458950588413, 296 | "active": false, 297 | "failure": 1 298 | }, 299 | { 300 | "host": "acab.mobi", 301 | "port": 5228, 302 | "key": "@Ia0xWQGJSTjRfYjHDDAFizXR9e8l5RQctTqYcbtR+Es=.ed25519", 303 | "source": "pub", 304 | "announcers": 9, 305 | "duration": { 306 | "mean": 0, 307 | "count": 2, 308 | "stdev": 0 309 | }, 310 | "time": { 311 | "attempt": 1458961704834, 312 | "hangup": 1458961705136 313 | }, 314 | "stateChange": 1458961705136, 315 | "active": false, 316 | "failure": 2 317 | }, 318 | { 319 | "host": "pi.bret.io", 320 | "port": 8008, 321 | "key": "@j3qWwQrWPzTM9zNgk0SI0FcqP1ULGquuINYEWfL330g=.ed25519", 322 | "source": "pub", 323 | "announcers": 50, 324 | "duration": { 325 | "mean": 32263.5625, 326 | "count": 32, 327 | "stdev": 1058.4539709376832 328 | }, 329 | "time": { 330 | "attempt": 1458983320400, 331 | "connect": 1458983321181, 332 | "hangup": 1458983352907 333 | }, 334 | "stateChange": 1458983352907, 335 | "client": true, 336 | "ping": { 337 | "rtt": { 338 | "mean": 0, 339 | "count": 0, 340 | "stdev": null 341 | }, 342 | "skew": { 343 | "mean": 0, 344 | "count": 0, 345 | "stdev": null 346 | } 347 | }, 348 | "active": true, 349 | "failure": 0 350 | }, 351 | { 352 | "host": "drinkbot.org", 353 | "port": 8008, 354 | "key": "@kOK9sfSLeFrQMtYaqLQ3nZE19v2IDiEwlpEdAqep3bw=.ed25519", 355 | "source": "pub", 356 | "announcers": 22, 357 | "duration": { 358 | "mean": 31111.65, 359 | "count": 20, 360 | "stdev": 278.72231252617365 361 | }, 362 | "time": { 363 | "attempt": 1458983212281, 364 | "connect": 1458983212774, 365 | "hangup": 1458983243562 366 | }, 367 | "stateChange": 1458983243562, 368 | "client": true, 369 | "ping": { 370 | "rtt": { 371 | "mean": 0, 372 | "count": 0, 373 | "stdev": null 374 | }, 375 | "skew": { 376 | "mean": 0, 377 | "count": 0, 378 | "stdev": null 379 | } 380 | }, 381 | "active": true, 382 | "failure": 0 383 | }, 384 | { 385 | "host": "128.199.132.182", 386 | "port": 8008, 387 | "key": "@DTNmX+4SjsgZ7xyDh5xxmNtFqa6pWi5Qtw7cE8aR9TQ=.ed25519", 388 | "source": "pub", 389 | "announcers": 22, 390 | "duration": { 391 | "mean": 3632843.1666666665, 392 | "count": 18, 393 | "stdev": 1312601.5419976406 394 | }, 395 | "time": { 396 | "attempt": 1458983143359, 397 | "connect": 1458983143915, 398 | "hangup": 1458982814101 399 | }, 400 | "stateChange": 1458983143915, 401 | "state": "connected", 402 | "client": true, 403 | "ping": { 404 | "rtt": { 405 | "mean": 234, 406 | "count": 1, 407 | "stdev": 0 408 | }, 409 | "skew": { 410 | "mean": -4, 411 | "count": 1, 412 | "stdev": 0 413 | } 414 | }, 415 | "active": true, 416 | "failure": 0 417 | }, 418 | { 419 | "host": "mindeco.de", 420 | "port": 110, 421 | "key": "@Uki1+Hds2kkx4rOWl202SPfcsgsdaLHJ/Y6OfPnK1xk=.ed25519", 422 | "source": "pub", 423 | "announcers": 11, 424 | "duration": { 425 | "mean": 30915.4, 426 | "count": 25, 427 | "stdev": 4021.505538974173 428 | }, 429 | "time": { 430 | "attempt": 1458982853190, 431 | "connect": 1458983037094, 432 | "hangup": 1458983068852 433 | }, 434 | "stateChange": 1458983068852, 435 | "client": true, 436 | "ping": { 437 | "rtt": { 438 | "mean": 0, 439 | "count": 0, 440 | "stdev": null 441 | }, 442 | "skew": { 443 | "mean": 0, 444 | "count": 0, 445 | "stdev": null 446 | }, 447 | "fail": true 448 | }, 449 | "active": true, 450 | "failure": 0 451 | }, 452 | { 453 | "host": "pub.mixmix.io", 454 | "port": 8008, 455 | "key": "@uRECWB4KIeKoNMis2UYWyB2aQPvWmS3OePQvBj2zClg=.ed25519", 456 | "source": "pub", 457 | "announcers": 35, 458 | "duration": { 459 | "mean": 0, 460 | "count": 0, 461 | "stdev": null 462 | }, 463 | "time": { 464 | "attempt": 1458983140788 465 | }, 466 | "stateChange": 1458983140788, 467 | "state": "connected", 468 | "active": true, 469 | "failure": 0 470 | }, 471 | { 472 | "host": "178.62.206.163", 473 | "port": 110, 474 | "key": "@Uki1+Hds2kkx4rOWl202SPfcsgsdaLHJ/Y6OfPnK1xk=.ed25519", 475 | "source": "pub", 476 | "announcers": 23, 477 | "duration": { 478 | "mean": 0, 479 | "count": 0, 480 | "stdev": null 481 | }, 482 | "time": { 483 | "attempt": 1458983036198 484 | }, 485 | "stateChange": 1458983036198, 486 | "state": "connected", 487 | "active": true, 488 | "failure": 0 489 | }, 490 | { 491 | "host": "evbogue.com", 492 | "port": 8008, 493 | "key": "@9sCXwCJZJ9doPcx7oZ1gm7HNZapO2Z9iZ0FJHJdROio=.ed25519", 494 | "source": "pub", 495 | "announcers": 9, 496 | "duration": { 497 | "mean": 37290.125, 498 | "count": 8, 499 | "stdev": 8451.830414731177 500 | }, 501 | "time": { 502 | "attempt": 1458982816595, 503 | "connect": 1458982818525, 504 | "hangup": 1458982851463 505 | }, 506 | "stateChange": 1458982851463, 507 | "client": true, 508 | "ping": { 509 | "rtt": { 510 | "mean": 0, 511 | "count": 0, 512 | "stdev": null 513 | }, 514 | "skew": { 515 | "mean": 0, 516 | "count": 0, 517 | "stdev": null 518 | }, 519 | "fail": true 520 | }, 521 | "active": true, 522 | "failure": 0 523 | }, 524 | { 525 | "host": "104.131.122.139", 526 | "port": 8008, 527 | "key": "@kZ9Ra80lKWlmzfjxh5PFAjJWYlCHEPxbqxNajdzhPF8=.ed25519", 528 | "source": "pub", 529 | "announcers": 3, 530 | "duration": { 531 | "mean": 0, 532 | "count": 2, 533 | "stdev": 0 534 | }, 535 | "time": { 536 | "attempt": 1458953214645, 537 | "hangup": 1458953214865 538 | }, 539 | "stateChange": 1458953214865, 540 | "active": false, 541 | "failure": 2 542 | }, 543 | { 544 | "host": "45.33.29.124", 545 | "port": 8008, 546 | "key": "@HogU/jcIAkSD/Owzjwv4X140CEW0FIco7QxPM47BVa8=.ed25519", 547 | "source": "pub", 548 | "announcers": 27, 549 | "duration": { 550 | "mean": 31529.136363636364, 551 | "count": 22, 552 | "stdev": 427.74847063697473 553 | }, 554 | "time": { 555 | "attempt": 1458983282932, 556 | "connect": 1458983283615, 557 | "hangup": 1458983315097 558 | }, 559 | "stateChange": 1458983315097, 560 | "client": true, 561 | "ping": { 562 | "rtt": { 563 | "mean": 0, 564 | "count": 0, 565 | "stdev": null 566 | }, 567 | "skew": { 568 | "mean": 0, 569 | "count": 0, 570 | "stdev": null 571 | } 572 | }, 573 | "active": true, 574 | "failure": 0 575 | } 576 | ] 577 | } 578 | -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | # ssb-server 2 | 3 | Secure-scuttlebutt API server 4 | 5 | 6 | 7 | ## get: async 8 | 9 | Get a message by its hash-id. 10 | 11 | ```bash 12 | get {msgid} 13 | ``` 14 | 15 | ```js 16 | get(msgid, cb) 17 | ``` 18 | 19 | 20 | 21 | ## createFeedStream: source 22 | 23 | (feed) Fetch messages ordered by their claimed timestamps. 24 | 25 | ```bash 26 | feed [--live] [--gt index] [--gte index] [--lt index] [--lte index] [--reverse] [--keys] [--values] [--limit n] 27 | ``` 28 | 29 | ```js 30 | createFeedStream({ live:, gt:, gte:, lt:, lte:, reverse:, keys:, values:, limit:, fillCache:, keyEncoding:, valueEncoding: }) 31 | ``` 32 | 33 | Create a stream of the data in the database, ordered by the timestamp claimed by the author. 34 | NOTE - the timestamp is not verified, and may be incorrect. 35 | The range queries (gt, gte, lt, lte) filter against this claimed timestap. 36 | 37 | - `live` (boolean, default: `false`): Keep the stream open and emit new messages as they are received. 38 | - `gt` (greater than), `gte` (greater than or equal) define the lower bound of the range to be streamed. Only records where the key is greater than (or equal to) this option will be included in the range. When `reverse=true` the order will be reversed, but the records streamed will be the same. 39 | - `lt` (less than), `lte` (less than or equal) define the higher bound of the range to be streamed. Only key/value pairs where the key is less than (or equal to) this option will be included in the range. When `reverse=true` the order will be reversed, but the records streamed will be the same. 40 | - `reverse` (boolean, default: `false`): a boolean, set true and the stream output will be reversed. Beware that due to the way LevelDB works, a reverse seek will be slower than a forward seek. 41 | - `keys` (boolean, default: `true`): whether the `data` event should contain keys. If set to `true` and `values` set to `false` then `data` events will simply be keys, rather than objects with a `key` property. 42 | - `values` (boolean, default: `true`): whether the `data` event should contain values. If set to `true` and `keys` set to `false` then `data` events will simply be values, rather than objects with a `value` property. 43 | - `limit` (number, default: `-1`): limit the number of results collected by this stream. This number represents a *maximum* number of results and may not be reached if you get to the end of the data first. A value of `-1` means there is no limit. When `reverse=true` the highest keys will be returned instead of the lowest keys. 44 | - `fillCache` (boolean, default: `false`): wheather LevelDB's LRU-cache should be filled with data read. 45 | - `keyEncoding` / `valueEncoding` (string): the encoding applied to each read piece of data. 46 | 47 | 48 | 49 | ## createLogStream: source 50 | 51 | (log) Fetch messages ordered by the time received. 52 | 53 | ```bash 54 | log [--live] [--gt index] [--gte index] [--lt index] [--lte index] [--reverse] [--keys] [--values] [--limit n] 55 | ``` 56 | 57 | ```js 58 | createLogStream({ live:, gt:, gte:, lt:, lte:, reverse:, keys:, values:, limit:, fillCache:, keyEncoding:, valueEncoding: }) 59 | ``` 60 | 61 | Creates a stream of the messages that have been written to this instance, in the order they arrived. 62 | The objects in this stream will be of the form: 63 | 64 | ``` 65 | { key: Hash, value: Message, timestamp: timestamp } 66 | ``` 67 | 68 | `timestamp` is the time which the message was received. 69 | It is generated by [monotonic-timestamp](https://github.com/dominictarr/monotonic-timestamp). 70 | The range queries (gt, gte, lt, lte) filter against this receive timestap. 71 | 72 | 73 | - `live` (boolean, default: `false`): Keep the stream open and emit new messages as they are received. 74 | - `gt` (greater than), `gte` (greater than or equal) define the lower bound of the range to be streamed. Only records where the key is greater than (or equal to) this option will be included in the range. When `reverse=true` the order will be reversed, but the records streamed will be the same. 75 | - `lt` (less than), `lte` (less than or equal) define the higher bound of the range to be streamed. Only key/value pairs where the key is less than (or equal to) this option will be included in the range. When `reverse=true` the order will be reversed, but the records streamed will be the same. 76 | - `reverse` (boolean, default: `false`): a boolean, set true and the stream output will be reversed. Beware that due to the way LevelDB works, a reverse seek will be slower than a forward seek. 77 | - `keys` (boolean, default: `true`): whether the `data` event should contain keys. If set to `true` and `values` set to `false` then `data` events will simply be keys, rather than objects with a `key` property. 78 | - `values` (boolean, default: `false`): whether the `data` event should contain values. If set to `true` and `keys` set to `false` then `data` events will simply be values, rather than objects with a `value` property. 79 | - `limit` (number, default: `-1`): limit the number of results collected by this stream. This number represents a *maximum* number of results and may not be reached if you get to the end of the data first. A value of `-1` means there is no limit. When `reverse=true` the highest keys will be returned instead of the lowest keys. 80 | - `fillCache` (boolean, default: `false`): wheather LevelDB's LRU-cache should be filled with data read. 81 | - `keyEncoding` / `valueEncoding` (string): the encoding applied to each read piece of data. 82 | 83 | 84 | 85 | ## messagesByType: source 86 | 87 | (logt) Retrieve messages with a given type, ordered by receive-time. 88 | 89 | 90 | ```bash 91 | logt --type {type} [--live] [--gt index] [--gte index] [--lt index] [--lte index] [--reverse] [--keys] [--values] [--limit n] 92 | ``` 93 | 94 | ```js 95 | messagesByType({ type:, live:, gt:, gte:, lt:, lte:, reverse:, keys:, values:, limit:, fillCache:, keyEncoding:, valueEncoding: }) 96 | ``` 97 | 98 | All messages must have a type, so this is a good way to select messages that an application might use. 99 | Like in createLogStream, the range queries (gt, gte, lt, lte) filter against the receive timestap. 100 | 101 | - `type` (string): The type of the messages to emit. 102 | - `live` (boolean, default: `false`): Keep the stream open and emit new messages as they are received. 103 | - `gt` (greater than), `gte` (greater than or equal) define the lower bound of the range to be streamed. Only records where the key is greater than (or equal to) this option will be included in the range. When `reverse=true` the order will be reversed, but the records streamed will be the same. 104 | - `lt` (less than), `lte` (less than or equal) define the higher bound of the range to be streamed. Only key/value pairs where the key is less than (or equal to) this option will be included in the range. When `reverse=true` the order will be reversed, but the records streamed will be the same. 105 | - `reverse` (boolean, default: `false`): a boolean, set true and the stream output will be reversed. Beware that due to the way LevelDB works, a reverse seek will be slower than a forward seek. 106 | - `keys` (boolean, default: `true`): whether the `data` event should contain keys. If set to `true` and `values` set to `false` then `data` events will simply be keys, rather than objects with a `key` property. 107 | - `values` (boolean, default: `true`): whether the `data` event should contain values. If set to `true` and `keys` set to `false` then `data` events will simply be values, rather than objects with a `value` property. 108 | - `limit` (number, default: `-1`): limit the number of results collected by this stream. This number represents a *maximum* number of results and may not be reached if you get to the end of the data first. A value of `-1` means there is no limit. When `reverse=true` the highest keys will be returned instead of the lowest keys. 109 | - `fillCache` (boolean, default: `false`): wheather LevelDB's LRU-cache should be filled with data read. 110 | - `keyEncoding` / `valueEncoding` (string): the encoding applied to each read piece of data. 111 | 112 | 113 | 114 | ## createHistoryStream: source 115 | 116 | (hist) Fetch messages from a specific user, ordered by sequence numbers. 117 | 118 | ```bash 119 | hist {feedid} [seq] [live] 120 | hist --id {feedid} [--seq n] [--live] [--limit n] [--keys] [--values] 121 | ``` 122 | 123 | ```js 124 | createHistoryStream(id, seq, live) 125 | createHistoryStream({ id:, seq:, live:, limit:, keys:, values: }) 126 | ``` 127 | 128 | `createHistoryStream` and `createUserStream` serve the same purpose. 129 | `createHistoryStream` exists as a separate call because it provides fewer range parameters, which makes it safer for RPC between untrusted peers. 130 | 131 | - `id` (FeedID, required): The id of the feed to fetch. 132 | - `seq` (number, default: `0`): If `seq > 0`, then only stream messages with sequence numbers greater than `seq`. 133 | - `live` (boolean, default: `false`): Keep the stream open and emit new messages as they are received. 134 | - `keys` (boolean, default: `true`): whether the `data` event should contain keys. If set to `true` and `values` set to `false` then `data` events will simply be keys, rather than objects with a `key` property. 135 | - `values` (boolean, default: `true`): whether the `data` event should contain values. If set to `true` and `keys` set to `false` then `data` events will simply be values, rather than objects with a `value` property. 136 | - `limit` (number, default: `-1`): limit the number of results collected by this stream. This number represents a *maximum* number of results and may not be reached if you get to the end of the data first. A value of `-1` means there is no limit. When `reverse=true` the highest keys will be returned instead of the lowest keys. 137 | 138 | 139 | ## createUserStream: source 140 | 141 | Fetch messages from a specific user, ordered by sequence numbers. 142 | 143 | ```bash 144 | createUserStream --id {feedid} [--live] [--gt index] [--gte index] [--lt index] [--lte index] [--reverse] [--keys] [--values] [--limit n] 145 | ``` 146 | 147 | ```js 148 | createUserStream({ id:, live:, gt:, gte:, lt:, lte:, reverse:, keys:, values:, limit:, fillCache:, keyEncoding:, valueEncoding: }) 149 | ``` 150 | 151 | `createHistoryStream` and `createUserStream` serve the same purpose. 152 | `createHistoryStream` exists as a separate call because it provides fewer range parameters, which makes it safer for RPC between untrusted peers. 153 | 154 | The range queries (gt, gte, lt, lte) filter against the sequence number. 155 | 156 | - `id` (FeedID, required): The id of the feed to fetch. 157 | - `live` (boolean, default: `false`): Keep the stream open and emit new messages as they are received. 158 | - `gt` (greater than), `gte` (greater than or equal) define the lower bound of the range to be streamed. Only records where the key is greater than (or equal to) this option will be included in the range. When `reverse=true` the order will be reversed, but the records streamed will be the same. 159 | - `lt` (less than), `lte` (less than or equal) define the higher bound of the range to be streamed. Only key/value pairs where the key is less than (or equal to) this option will be included in the range. When `reverse=true` the order will be reversed, but the records streamed will be the same. 160 | - `reverse` (boolean, default: `false`): a boolean, set true and the stream output will be reversed. Beware that due to the way LevelDB works, a reverse seek will be slower than a forward seek. 161 | - `keys` (boolean, default: `true`): whether the `data` event should contain keys. If set to `true` and `values` set to `false` then `data` events will simply be keys, rather than objects with a `key` property. 162 | - `values` (boolean, default: `true`): whether the `data` event should contain values. If set to `true` and `keys` set to `false` then `data` events will simply be values, rather than objects with a `value` property. 163 | - `limit` (number, default: `-1`): limit the number of results collected by this stream. This number represents a *maximum* number of results and may not be reached if you get to the end of the data first. A value of `-1` means there is no limit. When `reverse=true` the highest keys will be returned instead of the lowest keys. 164 | - `fillCache` (boolean, default: `false`): wheather LevelDB's LRU-cache should be filled with data read. 165 | - `keyEncoding` / `valueEncoding` (string): the encoding applied to each read piece of data. 166 | 167 | 168 | ## createWriteStream: sink 169 | 170 | write a number of messages to the local store. 171 | will error if messages are not valid, but will accept 172 | messages that the ssb-server doesn't replicate. 173 | 174 | 175 | ## links: source 176 | 177 | Get a stream of messages, feeds, or blobs that are linked to/from an id. 178 | 179 | ```bash 180 | links [--source id|filter] [--dest id|filter] [--rel value] [--keys] [--values] [--live] [--reverse] 181 | ``` 182 | 183 | ```js 184 | links({ source:, dest:, rel:, keys:, values:, live:, reverse: }) 185 | ``` 186 | 187 | The objects in this stream will be of the form: 188 | 189 | ``` 190 | { source: ID, rel: String, dest: ID, key: MsgID } 191 | ``` 192 | 193 | - `source` (string, optional): An id or filter, specifying where the link should originate from. To filter, just use the sigil of the type you want: `@` for feeds, `%` for messages, and `&` for blobs. 194 | - `dest` (string, optional): An id or filter, specifying where the link should point to. To filter, just use the sigil of the type you want: `@` for feeds, `%` for messages, and `&` for blobs. 195 | - `rel` (string, optional): Filters the links by the relation string. 196 | - `live` (boolean, default: `false`): Keep the stream open and emit new messages as they are received. 197 | - `reverse` (boolean, default: `false`): a boolean, set true and the stream output will be reversed. Beware that due to the way LevelDB works, a reverse seek will be slower than a forward seek. 198 | - `keys` (boolean, default: `true`): whether the `data` event should contain keys. If set to `true` and `values` set to `false` then `data` events will simply be keys, rather than objects with a `key` property. 199 | - `values` (boolean, default: `true`): whether the `data` event should contain values. If set to `true` and `keys` set to `false` then `data` events will simply be values, rather than objects with a `value` property. 200 | 201 | 202 | ## add: async 203 | 204 | Add a well-formed message to the database. 205 | 206 | ```bash 207 | cat ./message.json | add 208 | add --author {feedid} --sequence {number} --previous {msgid} --timestamp {number} --hash sha256 --signature {sig} --content.type {type} --content.{...} 209 | ``` 210 | 211 | ```js 212 | add({ author:, sequence:, previous: timestamp:, hash: 'sha256', signature:, content: { type:, ... } }, cb) 213 | ``` 214 | 215 | - `author` (FeedID): Public key of the author of the message. 216 | - `sequence` (number): Sequence number of the message. (Starts from 1.) 217 | - `previous` (MsgID): Hash-id of the previous message in the feed (null for seq=1). 218 | - `timestamp` (number): Unix timestamp for the publish time. 219 | - `hash` (string): The hash algorithm used in the message, should always be `sha256`. 220 | - `signature` (string): A signature computed using the author pubkey and the content of the message (less the `signature` attribute). 221 | - `content` (object): The content of the message. 222 | - `.type` (string): The object's type. 223 | 224 | 225 | ## publish: async 226 | 227 | Construct a message using ssb-server's current user, and add it to the DB. 228 | 229 | ```bash 230 | cat ./message-content.json | publish 231 | publish --type {string} [--other-attributes...] 232 | ``` 233 | 234 | ```js 235 | publish({ type:, ... }, cb) 236 | ``` 237 | 238 | This is the recommended method for publishing new messages, as it handles the tasks of correctly setting the message's timestamp, sequence number, previous-hash, and signature. 239 | 240 | - `content` (object): The content of the message. 241 | - `.type` (string): The object's type. 242 | 243 | 244 | 245 | 246 | ## getAddress: sync 247 | 248 | Get the address of the server. Default scope is public. 249 | 250 | ```bash 251 | getAddress {scope} 252 | ``` 253 | 254 | ```js 255 | getAddress(scope, cb) 256 | ``` 257 | 258 | 259 | 260 | ## getLatest: async 261 | 262 | Get the latest message in the database by the given feedid. 263 | 264 | ```bash 265 | getLatest {feedid} 266 | ``` 267 | 268 | ```js 269 | getLatest(id, cb) 270 | ``` 271 | 272 | 273 | 274 | ## latest: source 275 | 276 | Get the seq numbers of the latest messages of all users in the database. 277 | 278 | ```bash 279 | latest 280 | ``` 281 | 282 | ```js 283 | latest() 284 | ``` 285 | 286 | 287 | 288 | ## latestSequence: async 289 | 290 | Get the sequence and local timestamp of the last received message from 291 | a given `feedId`. 292 | 293 | ```bash 294 | latestSequence {feedId} 295 | ``` 296 | 297 | ```js 298 | latest({feedId}) 299 | ``` 300 | 301 | 302 | 303 | ## whoami: sync 304 | 305 | Get information about the current ssb-server user. 306 | 307 | ```bash 308 | whoami 309 | ``` 310 | 311 | ```js 312 | whoami(cb) 313 | ``` 314 | 315 | Outputs information in the following form: 316 | 317 | ``` 318 | { id: FeedID } 319 | ``` 320 | 321 | 322 | 323 | ## progress: sync 324 | 325 | returns an object reflecting the progress state of various plugins. 326 | the return value is a `{}` with subobjects showing `{start,current,target}` 327 | to represent progress. Currently implemented are `migration` (legacy->flume) 328 | migration progress and `indexes` (index regeneration). 329 | 330 | 331 | ## status: sync 332 | 333 | returns an object reflecting the status of various ssb operations, 334 | such as db read activity, connection statuses, etc, etc. The purpose is to provide 335 | an overview of how ssb is working. 336 | 337 | ## getVectorClock: async 338 | 339 | ## version: sync 340 | 341 | return the current version number of the running server 342 | --------------------------------------------------------------------------------